# 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.** --- ## 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 hydration. No user-authored JavaScript for standard forms, lists, navigation, or server push. [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] --- ## 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 `key todo.id` to this loop”, 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::render`, `Effect::set`, 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. --- ## 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/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`. --- ## 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-*`, `@for 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::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, hydrated, or synced. Ordinary Rust fields on app/components are not automatically atoms. --- ## invariant ### req: invariant/001 001 User-authored references are symbolic at author time and numeric at runtime. ### req: invariant/002 002 The JS runtime never parses CSS selectors, expressions, or handler names. ### req: invariant/003 003 Rust handlers return effects; they do not imperatively mutate DOM. ### req: invariant/004 004 Cross-file references fail at `cargo check` with a precise span. ### req: invariant/005 005 slhx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns. --- ## boundary ### req: boundary/001 001 hemplate does not expose a slhx API. It exposes a stable, generic Template Surface IR. slhx is one consumer; a11y tools, test generators, and documentation generators are others. ### req: boundary/002 002 hemplate never interprets `data-slhx-*`, `slhx-*`, or any other tool-prefixed attribute. It records them as raw `name: value` pairs in the Surface. ### req: boundary/003 003 slhx never parses `.heml` directly. It consumes `hemplate.surface.postcard` emitted by `hemplate_build`. slhx interprets tool-specific conventions (`data-slhx-handle`, `data-slhx-slot`, etc.) from the generic Surface. --- ## 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 | 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. ### 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. --- ## build ### req: build/001 001 Build order: `.heml` → `hemplate_build` → `hemplate.surface.postcard` → `slhx_build` → `slhx.generated.rs` + `slhx.syms` + diagnostics. ### req: build/002 002 Proc-macros (`#[slhx::handler]`, `#[slhx::surface]`) are side-effect free. They read generated artifacts (`slhx.syms`, `slhx.generated.rs`) but never parse `.heml`, never process generic Surface IR, and never write files. Global codegen lives only in `build.rs` invoked by `slhx_build`. [north_star] ### req: build/003 003 `slhx-derive` (`#[slhx::handler]`) reads `slhx.syms` at expansion time to validate handle names, slot names, and form signatures. It generates only local glue (static fn-table entry) plus compile errors. ### req: build/004 004 `#slhx::surface]` reads `slhx.generated.rs` from `$OUT_DIR` and expands it into the annotated module. It is a pure include/bridge macro with no semantic analysis of its own. ### req: build/005 005 A `build.rs` failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion. ### req: build/006 006 Proc-macros are considered local: they have knowledge of the item they annotate, plus pre-generated symbol tables. They do not have global knowledge of all handlers across the crate. Global checks (e.g. every declared handle has an implementation) are either deferred to app-mount tests or enabled by an optional `#[slhx::component]` macro. --- ## effect ### req: effect/001 001 Rust handlers return `impl IntoEffect`, not a concrete `Vec`. `IntoEffect::encode(self, &mut EffectWriter)` may write directly into a response buffer, a test collector, or an event stream. Zero-allocation encoding is possible; allocation is not required. ### req: effect/002 002 `EffectWriter` has a fixed canonical op set: `Set`, `Patch`, `Insert`, `Remove`, `Move`, `Focus`, `Navigate`, `Emit`. The DOM is one backend; core does not hardcode DOM operations. Wire format is canonical postcard opcodes; Rust API is flexible. ### req: effect/003 003 Multiple effects are combined with tuple composition or a method chain on `EffectWriter`. No `!` call-syntax macros. ### req: effect/004 004 `Effect::render(slot, value)` is sugar for `Effect::set` on a Slot resource. `Effect::text(slot, value)` is sugar for `Effect::set` with a text payload. ### req: effect/005 005 effects may carry an opaque transition token, but core never interprets or implements transitions. `slhx-transition` provides transition semantics as an orthogonal integration. ### req: effect/006 006 `Effect::event(name, payload)` dispatches a native `CustomEvent` on the root element. Core interprets the payload as opaque bytes. Web Components, charts, editors, or legacy JS may listen without slhx knowing about them. [north_star] ### req: effect/007 007 `Effect::event` is the raw escape hatch. Common UI events such as toast, dialog close, focus, and clipboard may have typed helper wrappers (`toast("Saved")`, `nav::push("/dashboard")`) in optional crates. Helpers compile to `Effect::event` or canonical ops. ### req: effect/008 008 Small semantic helper types may implement `IntoEffect`: `Toast`, `Nav`, `FormError`, generated slot commands, generated atom commands. Users compose them by returning tuples. --- ## state ### req: state/001 001 Typed atoms with `Atom`: read via `atom.get()`, subscribe via `Effect::set`. No hidden global proxy / reactive graph. Atoms are explicit values in `struct App`. ### req: state/002 002 Subscription is explicit: `Effect::set(atoms::FOO, 42)` pushes the new value to all consumers. No automatic component re-render graph. ### req: state/003 003 JS runtime maintains a client-side atom store with identical API to the server: `get(atom)`, `set(atom, value)`, `subscribe(atom, callback)`. Type erased at runtime with `TypeId`. ### req: state/004 004 SSR pages carry a `data-slhx-st` base64url-encoded postcard blob on the document root. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip. ### req: state/005 005 Not all state is an Atom. Ordinary Rust fields are preferred unless the value must be independently addressed, bootstrapped, synced, or subscribed. Atoms are explicit resources, not the default state container. --- ## 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 are returned as `Effect::form_error(field, message)`, which the JS runtime maps back to the originating input via `data-sid`. --- ## list ### req: list/001 001 Any `data-slhx-slot` or `data-slhx-handle` inside `@for` requires an explicit `key` expression. Syntax: `@for item in items key item.id { ... }`. Without `key`, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(SlotId, KeyValue)`. ### req: list/002 002 Slots inside a keyed loop receive a composite identity: `(SlotId, KeyValue)`, not a flat id. 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)`. Mismatch between key type and slot key type is compile-time error. --- ## 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, key: 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, Route). External crates may not add variants. Extensibility comes via `Effect::event`, `Effect::emit`, or custom `IntoEffect` implementations, never via new `ResourceKind` variants in core. --- ## async_data ### req: async_data/001 001 Async remote data lives in `Resource` / `Query` / `Mutation`. These are optional, not core primitives. They provide loading/error/refresh semantics without client-side data libraries. ### req: async_data/002 002 `Effect::resource(res).reload()` triggers a re-fetch and re-render. The server sends a new EffectBatch when data is ready. --- ## scope ### req: scope/001 001 `Scope` is a first-class primitive. Keyed loops (`@for`), conditional branches (`@if`), component instances, modals, tabs, nested forms — all are scopes. A slhx-addressable node inside any dynamic scope must carry a stable `ScopeKey`. Composite identity is `(ResourceId, ScopeKey)`. [north_star] --- ## wire ### req: wire/001 001 HTML wire format is standard HTML with `data-hid` and `data-sid` attributes only. No custom markup, no hx-* attributes. ### 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 Responses are `text/html` fragments (or `application/slhx` for push streams). Fragments may contain `