# slhx — Semantic, Laterally HX slhx does not compete with React by becoming a better frontend framework. slhx competes with React by making frontend frameworks unnecessary for most apps. > **hemplate owns syntax. hemplate emits surface. slhx consumes surface. slhx owns semantics. JS executes bytecode.** --- ## laws ### req: law/001 001 A feature belongs in core only if it can be expressed as typed resources plus EffectBatch ops. ### req: law/002 002 A feature belongs in an integration crate if it depends on transport, framework, auth, storage, browser capability, or deployment policy. ### req: law/003 003 A feature belongs in generated API if it improves author ergonomics without adding runtime semantics. ### req: law/004 004 A feature belongs in user code if it is business logic, domain validation, routing policy, authorization policy, persistence, or layout choice. ### req: law/005 005 Add one primitive only if it removes at least five special cases. --- ## pitch ### req: pitch/001 001 slhx is checked hypermedia for Rust. Write `.heml`, write `#[slhx::handler]`, return commands. The compiler checks every cross-file reference. The browser runtime only sees ids and effect bytes. No JS app code required. [north_star] ### req: pitch/002 002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No component hydration. SSR state bootstrap via `data-slhx-st` is allowed, but the browser never reconstructs a component tree. [north_star] ### req: pitch/003 003 slhx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, slhx knows the effects, the browser only executes commands. [north_star] ### req: pitch/004 004 The north-star feel: **Svelte at the call site, Rust at the boundary.** Short handler bodies, compile-checked HTML contracts, one language for server and client logic. [north_star] --- ## modes ### req: mode/001 001 slhx has two happy paths: Page Enhancer and Interaction Handler. ### req: mode/002 002 Page Enhancer mode replaces minimal HTMX page swapping. Authors use real anchors with `data-slhx-nav` or `data-slhx-boost`; no user-authored handler is required. ### req: mode/003 003 Interaction Handler mode handles forms, buttons, typed params, and targeted updates through `#[slhx::handler]`. ### req: mode/004 004 Beginner docs must teach Page Enhancer first, Interaction Handler second, Atoms third, client-local/WASM fourth, sync last. --- ## dx ### req: dx/001 001 The common case must feel like writing a Svelte/Vue component: template, state, handlers, and targeted updates. Users should not need to understand Surface IR, ResourceId, EffectWriter, postcard, or runtime opcodes for basic apps. [north_star] ### req: dx/002 002 The happy path is: write `.heml`, write `#[slhx::handler]`, return generated slot/atom commands. No manual ids, no manual registry, no manual serialization, no manual JavaScript. [north_star] ### req: dx/003 003 Public APIs are generated around the user's names. If the template declares `data-slhx-slot="todo_list"`, the user gets `slots::todo_list`, not `SlotId(12)`. ### req: dx/004 004 Common handlers must fit in a small function. Advanced contexts (`EffectWriter`, raw ops, custom encoders) exist but are not part of the beginner path. ### req: dx/005 005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”. ### req: dx/006 006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. These return `impl IntoEffect`. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star] ### req: dx/007 007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path. ### req: dx/008 008 User-authored JavaScript is never required for standard forms, lists, navigation, optimistic actions, or server push. Custom JS is only needed at opaque leaf boundaries such as charts, maps, editors, and Web Components. --- ## ceremony ### req: ceremony/001 001 A minimal counter app requires one `.heml` file, one Rust state struct, and one handler function. No manual registry, no manual route table, no manual JS. Under 50 lines of user-authored Rust plus one template. ### req: ceremony/002 002 Generated modules are imported through a prelude or component namespace. Users should not manually include `$OUT_DIR` files in normal apps. ### req: ceremony/003 003 `build.rs` must be a one-liner for the common case: `fn main() { slhx_build::app().run().unwrap(); }` ### req: ceremony/004 004 No API may require users to write numeric ids, raw ResourceIds, raw opcodes, or serialized payloads in normal code. --- ## progressive_disclosure ### req: pd/001 001 A beginner can build CRUD with only: `.heml`, `#[slhx::handler]`, `Form`, generated `slots::*` methods, and `impl IntoEffect`. ### req: pd/002 002 Atoms are not required for basic server-first apps. They appear only when client-local state, SSR bootstrapped state, or WASM handlers are used. ### req: pd/003 003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples. ### req: pd/004 004 Documentation must present three levels: server-first, client-local, hybrid-sync. Each level introduces only the new primitive it needs. --- ## page_swap ### req: page_swap/001 001 Minimal page swapping is a first-class slhx-axum happy path. Authors mark real anchors with `data-slhx-nav`; links keep valid `href` and work without JS. ### req: page_swap/002 002 A `data-slhx-nav` click fetches the target URL as a slhx partial request. The response updates the canonical content slot, optionally navigation and title, then applies a `Navigate` effect. ### req: page_swap/003 003 Page swapping uses generated slots, not CSS selectors. The default content target is the slot named `content`, not `#content`. ### req: page_swap/004 004 Minimal page swap must not require user-authored `#[slhx::handler]`. Explicit navigation handlers are available only when custom application logic is needed. ### req: page_swap/005 005 Browser back/forward is supported. On `popstate`, slhx fetches the URL as a partial request and applies the same page-swap update without pushing a new history entry. ### req: page_swap/006 006 If a page lacks the expected `content` slot, slhx-axum falls back to normal browser navigation in production and emits a diagnostic in development. ### req: page_swap/007 007 `data-slhx-boost` progressively enhances descendant same-origin anchors and forms. Links behave like `data-slhx-nav`; forms behave like slhx form submissions. External links, downloads, new-tab links, and modified-clicks preserve native browser behavior. --- ## htmx_equivalents ### req: htmx/001 001 slhx replaces common HTMX use-cases through typed equivalents, not HTMX syntax. ### req: htmx/002 002 Easy equivalents must exist for: boosted links/forms, page swap, form submit, targeted replacement, append/prepend/remove, loading indicators, confirmation, debounce/throttle, polling, history navigation, multi-target updates, response events, SSE/push, and validation errors. ### req: htmx/003 003 slhx core deliberately does not clone selector-based HTMX features: `hx-target` selectors, `hx-select`, `hx-include` selectors, `closest/find/this` target strings, or trigger mini-languages. Equivalent patterns use generated slots, typed params, forms, and explicit handlers. --- ## component ### req: component/001 001 The primary authoring unit is a hemplate component plus adjacent Rust handlers. A component owns a template root, generated slots, generated handles, generated form checks, and source spans. ### req: component/002 002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup. ### req: component/003 003 Generated APIs are component-namespaced by default: `ui::slots::todo_row`, `ui::handles::create`, `ui::forms::create`. Global exports are opt-in only. ### req: component/004 004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source. ### req: component/005 005 An optional `#[slhx::component]` macro may validate that every handle declared in the template Surface has a corresponding `#[slhx::handler]` within the annotated module. This is the only macro with cross-handler visibility inside a single module; it remains strictly local. Missing handlers without `#[slhx::component]` are caught at app mount or test time, not `cargo check`. ### req: component/006 006 `#[derive(Hemplate)]` structs are natural component boundaries. slhx_build discovers them automatically; no additional configuration is required for most apps. --- ## surface ### req: surface/001 001 `hemplate_build` scans `.heml` files and emits `$OUT_DIR/hemplate.surface.postcard` (postcard-encoded, deterministic, versioned). ### req: surface/002 002 The Surface contains: nodes (NodeId, parent, scope, element, attrs, source span), scopes (ScopeKind: Root | If | Match | Case | For { binding, key_expr }), forms (form controls with raw HTML types), and component uses. ### req: surface/003 003 Node identity is `NodeId` in a parent/scope graph. No `css_path` is used as a primary identifier. An optional `debug_path` string may exist for diagnostics only. ### req: surface/004 004 Form controls in the Surface carry raw HTML facts: `ControlKind::Text`, `ControlKind::Number { min, max, step }`, `ControlKind::Checkbox`, `ControlKind::Select { multiple, options }`, etc. No Rust type mapping lives in hemplate. ### req: surface/005 005 Loop scopes expose the binding name and an optional `key_expr` (e.g. `todo.id`). hemplate does not enforce key usage; it only records it for consumers. slhx_build enforces key presence only when a slhx-addressable node appears inside the loop. ### req: surface/006 006 Surface schema is versioned (`schema_version: u32`). Postcard encoding, no JSON. `no_std`-compatible schema definition so any tool can read it without heavy dependencies. ### req: surface/007 007 `hemplate-derive` does not write Surface files. Surface generation is a `build.rs` / `hemplate_build` concern, proc-macro side-effect free. ### req: surface/008 008 The Surface records hemplate structural directives as first-class facts: `h-for`, `h-key`, `h-if`, `h-else-if`, `h-else`, `h-match`, `h-case`, dynamic `+attr` bindings, and interpolated attr/text expressions. slhx consumes these facts; it never parses `.heml` source directly. ### req: surface/009 009 Raw/pre-rendered HTML insertions are opaque Surface holes. The parent element is present; slhx_build emits the appropriate rendering call. ### req: surface/010 010 Attribute values preserve their origin: static literal, dynamic `+attr` binding, or interpolated expression. slhx-build uses this to determine whether a `data-*` handle param is statically known or runtime-extracted. --- ## codegen ### req: codegen/001 001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star] ### req: codegen/002 002 Generated module `slots` exposes ergonomic methods: `Slot::render(value)`, `Slot::text(value)`, `KeyedSlot::append(key, value)`, `KeyedSlot::prepend(key, value)`, `KeyedSlot::replace(key, value)`, `KeyedSlot::remove(key)`. Methods return `impl IntoEffect`. ### req: codegen/003 003 Generated module `handles` exports typed constants: `Handle` where `I` is `Form`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation. ### req: codegen/004 004 Generated module `forms` exports `FormContract` metadata (field names, HTML control kinds, required). `#[slhx::handler]` compares the `Form` type against the contract. Domain types (`Email`, `TodoId`) remain user-authored; no auto-generated structs. ### req: codegen/005 005 Generated module `atoms` exports `Atom` for values that must be addressable, bootstrapped, or synced. Ordinary Rust fields on app/components are not automatically atoms. --- ## public_api ### req: public_api/001 001 The generated API is the primary public authoring API. Most user code should return generated slot/atom/form/nav commands, not raw `Effect` constructors. ### req: public_api/002 002 `Effect`, `EffectWriter`, `ResourceId`, `ResourceRef`, and raw opcodes are advanced APIs. They must not appear in beginner docs, generated examples, or common diagnostics. ### req: public_api/003 003 Every generated command returns `impl IntoEffect` and composes through tuple composition. ### req: public_api/004 004 If a common UI operation requires raw `EffectWriter`, the public API is considered incomplete. --- ## effect_algebra ### req: effect_algebra/001 001 The canonical op set is minimal and closed: `Put`, `Insert`, `Remove`, `Move`, `Focus`, `Navigate`, `Emit`. ### req: effect_algebra/002 002 `Put` replaces the payload of a resource. For a Slot, this means replacing its rendered contents. For an Atom, this means replacing its stored value. ### req: effect_algebra/003 003 `Insert`, `Remove`, and `Move` operate on keyed collection resources. They require a key type checked by generated `KeyedSlot` wrappers. ### req: effect_algebra/004 004 `Navigate` changes browser history or represents a server redirect. Route matching remains outside slhx core. ### req: effect_algebra/005 005 `Emit` dispatches a native `CustomEvent` and is the only raw JS interop primitive in core. ### req: effect_algebra/006 006 DOM-specific operations such as `innerHTML`, `textContent`, class toggles, or keyed node lookup are runtime lowering details, not separate author-facing concepts. --- ## typed_id ### req: typed_id/001 001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32 }`. A concrete runtime target is a `ResourceRef { resource: ResourceId, scope: Option }`. Typed wrappers (`Slot`, `KeyedSlot`, `Atom`, `Handle`, `Form`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star] ### req: typed_id/002 002 `ResourceKind` is an internal closed enum (Slot, Atom, Handle, Form). Navigation is represented by `Navigate` effects, not by route resources. External crates may not add variants. Extensibility comes via `Effect::event` or custom `IntoEffect` implementations, never via new `ResourceKind` variants in core. `Effect::event` lowers to the canonical `Emit` opcode. --- ## scope ### req: scope/001 001 `Scope` is a first-class primitive. Keyed loops (`h-for`) create keyed dynamic scopes and require `h-key` for slhx-addressable nodes. Conditional branches (`h-if`, `h-else-if`, `h-else`, `h-match`, `h-case`) create optional presence scopes. Component instances, modals, tabs, and nested forms are scoped resources. Concrete runtime targets are addressed through `ResourceRef` `{ resource: ResourceId, scope: Option }`. [north_star] --- ## list ### req: list/001 001 Any `data-slhx-slot` or `data-slhx-handle` inside a hemplate `h-for` scope requires a stable key. Preferred syntax: ``. Without a key, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `ResourceRef { resource: ResourceId, scope: Some(ScopeKey::KeyValue(...)) }`. ### req: list/002 002 Slots inside a keyed loop receive a composite identity. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups. ### req: list/003 003 Effects on keyed slots: `replace_keyed(slot, key, value)`, `remove_keyed(slot, key)`, `append_keyed(slot, key, value)`, `prepend_keyed(slot, key, value)`. Mismatch between key type and slot key type is compile-time error. --- ## form ### req: form/001 001 Forms are source of truth in HTML. hemplate Surface exports form shape (controls, names, required, types). slhx checks compatibility with the Rust handler's `Form` type. No auto-generated structs; domain types (e.g. `Email`) are first-class. The Surface describes; Rust owns; slhx checks. ### req: form/002 002 The handle id is carried as `__h` in POST `application/x-www-form-urlencoded`. A JSON body is allowed at the integration boundary (`application/json`) only if the handler accepts it; core uses form encoding. ### req: form/003 003 Handler receives `form: Form`. Validation errors target `(FormId, field_name)` or generated control ids. The runtime maps them to originating form controls via control ids derived from Surface `NodeId`, not via slot ids. ### req: form/004 004 Form compatibility checks validate field presence, optionality, multiplicity, and parser availability. Domain validation remains Rust logic (`TryFrom`, custom validators, or handler code). ### req: form/005 005 HTML control facts are lower bounds, not complete domain semantics. `type="email"` may require a Rust `Email` parser, but hemplate never defines what a valid business email is. ### req: form/006 006 Generated diagnostics distinguish structure errors from validation errors: missing field / wrong optionality are compile-time issues; invalid submitted values are runtime form errors. --- ## form_effects ### req: form_effects/001 001 Generated form APIs provide common commands: `reset()`, `clear(field)`, `error(field, message)`, `focus(field)`, and `disable_while_pending()`. ### req: form_effects/002 002 Form effects target generated form/control ids, not CSS selectors. ### req: form_effects/003 003 Templates may declare error display targets with `data-slhx-error-for="field"`. Generated form error effects render into those targets when present and fall back to control validity APIs otherwise. --- ## wire ### req: wire/001 001 Authoring HTML uses symbolic `data-slhx-*` attributes. Rendered runtime HTML lowers these to compact numeric metadata: `data-hid`, `data-sid`, optional `data-key`, optional atom ids, optional form/control ids, and `data-slhx-st` for state bootstrap. The browser never sees handler or slot names. `data-slhx-root` marks a scoped root boundary. ### req: wire/002 002 POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path. ### req: wire/003 003 HTTP interaction responses may be `text/html` fragments containing `