# Topcoat > Rust server-rendered web framework. Async components produce HTML; runtime expressions compile to Rust + JavaScript, without client WASM. Experimental. Apps depend on the `topcoat` facade. The linked guides describe the APIs in this checkout. See `examples//src/` for runnable examples and `demos/admin/` for a complete app. Read the linked guide or source for details. ## Start Install `cargo install topcoat-cli`; create an app with `topcoat new app --recommended`; `cd app`. The recommended preset includes a Toasty/SQLite todo example: run `cargo run -- toasty migration generate` and `cargo run -- toasty migration apply`, then `topcoat dev` (build/bundle/reload). Edit `src/app.rs`. Use `--minimal` for a small app without database models, or omit the preset for the interactive wizard. The destination must not exist. [Getting started](docs/getting_started.md), [command options](docs/cli/new.md). For [manual setup](docs/manual_setup.md): `cargo new app`; `cd app`; `cargo add topcoat`; `cargo add tokio --features rt-multi-thread,macros`. The minimal entry point below runs with `cargo run` or `topcoat dev`. `start` defaults to `127.0.0.1:3000`; override `HOST`/`PORT`. ```rust use topcoat::{Result, context::Cx, router::{module_router, page}, view::{View, view}}; #[tokio::main] async fn main() { topcoat::start(module_router!().build()).await.unwrap(); } #[page] async fn home(cx: &Cx) -> Result { Ok(view! { topcoat::dev::script()

"Hello"

}) } ``` Defaults include router/view/runtime/serve/discover, asset/compression/cookie/session/font/icon; `full` enables all. Other feature names appear below. Import `RouterBuilder*Ext` traits for extension methods. Features expose APIs; builder calls install services. Exact flags: `crates/topcoat/Cargo.toml`. When formatting with `cargo fmt`, also run [`topcoat fmt`](docs/cli/fmt.md) to format macro bodies such as `view!`, unless the project says otherwise. `--rustfmt` runs rustfmt first; `--stdin --rustfmt` supports editor overrides. `--check` reports inputs needing formatting without writing; exits nonzero for differences or errors. Rustfmt must be on PATH; set `edition` in `rustfmt.toml` because input is piped to rustfmt. ## Routing and HTTP Handler and component macros require `cx: &Cx` as the first parameter, even when unused. Topcoat supplies it when calling the function. See [context](docs/context.md). - [Module routing](docs/router/module.md): `module_router!()` maps its enclosing module to `/`; descendants add kebab-case segments. Declare children with `mod`; no filesystem scan. Function names do not affect URLs. `_group` modules scope layouts/layers without URL segments. `segment!(rename: "...")` or `kind: Static|Group|Param|CatchAll` overrides a segment. - `#[page] async fn ... -> Result` serves GET. `#[route(POST)] async fn ... -> Result` serves `T: AsyncIntoResponse` (includes IntoResponse). After the required `cx: &Cx`, both accept one optional `FromRequest` argument, e.g. `Form(input): Form`. Methods also accept `[GET, POST]` or `*`; specific beats `*`. GET supplies HEAD unless overridden. - `#[page("./export")]` extends the module URL. Absolute paths, e.g. `#[route(GET "/health")]`, require `.route(handler)`/`.page(handler)` or `.discover()`. `module_router!()` collects module-derived handlers; `.discover()` adds absolute handlers, procedures, shards, fonts. Both require `discover` (default). Duplicate path+method panics; trailing-slash mismatches redirect. - `#[layout] async fn shell(cx: &Cx, slot: Slot<'_>) -> Result` wraps pages via `(slot)`. `#[layer] async fn wrap(cx: &Cx, body: Body, next: Next<'_>) -> Result` calls `next.run(cx, body).await`. Both match registered logical path prefixes, including groups/parameter names. Least specific is outermost; same-path explicit layers run last-registered first. Discovered layers/layouts need unique paths. Only pathless layers (`path() == None`) wrap routing misses. [Router](docs/router.md). - [Route context](docs/context.md#route-context): `#[page("/about", with(Title("About")))]` attaches values that layers and outer layouts can read, e.g. a page title or nav section. `#[route]`/`#[layout]`/`#[layer]` take `with(...)` after methods/path too; one value per entry, repeat for more. Use a layer for values shared by every route under a path, since layouts only wrap pages. Built once per router, so they cannot read the request; for per-request values store a `BoxCxFn`. Present once a route matches, none for unmatched requests (pathless layers included); read with `context`/`iter_context`. Each level shadows the previous: router builder values, outer to inner layers, outer to inner layouts, route/page, request values (`cx.with`). Hand-built `RouteFn`/`PageFn`/`LayoutFn`/`LayerFn` use `.with(value)`; `route.context()` reads them without a request. On a built router, `router.routes()` lists every registered route with its served `path()`, `methods()` and combined `context()` (layer, layout and own values), and `router.context()` holds the builder values. - [Parameters](docs/router/path_param.md): `path_param!(pub post_id: u64 else bad_request)` declares `{post_id}` for a handler path and generates `fn post_id(cx)`. For module routing, prefer [`module_param!`](docs/router/module_param.md), which makes the same declaration and also sets the module's URL segment to `{post_id}`. Name the module after the parameter, use `#[page]` in it, and read the value with `post_id(cx)?` to get `&u64`. Both macros accept the same options. Without a type, `path_param!(slug)` gives a decoded `&str`. With a type but no `else` clause, reading returns a Result that borrows the parsed value or error. `path_param!(*rest)` reads decoded segments, while `*rest: T` reads a parsed slice. A catch-all must come last and match at least one segment. A module can have one `module_param!` or one `segment!`, but not both, and any number of `path_param!` declarations. Reading a parameter missing from the matched route panics. - [Queries](docs/router/query_params.md): `#[query_params(error = bad_request)] struct Query { page: Option }`; `query_params::(cx)?` borrows cached data. Macro derives Deserialize; missing/empty optional values become None. Path/query parsing is memoized per request. - [Links](docs/router/href.md): always `href!(handler, post_id: id)` for internal links/actions/redirects; one `name: value` per path parameter, any order. Inside the declaring module, or the parent of a `module_param!` module, use the plain name; elsewhere use its path, e.g. `crate::app::posts::post_id: id`. Wrap `Path` constants in parens: `href!((POSTS))`. Render directly with `href=(href!(handler))` or pass directly to `link` component; use `.resolve(cx)` only when a string is needed, e.g. for redirects. `.query(serializable)`, `.fragment("id")`, `.is_current(cx)`, `.absolute()` (needs `.with(BaseUrl::new(...)?)`). Nav tables: store handlers as `&(dyn HrefTarget + Sync)`, loop with `href!(*target)`. Segments are encoded; empty/`.`/`..` or mismatched parameters panic. External URLs/standalone fragments are strings. - [Content](docs/router/content.md): `router::content::{Form, Json}`; Form reads query on GET/HEAD, URL-encoded body otherwise. Option extractors allow missing input, not malformed input. Buffered limit: 2 MiB; override via context: `Router::builder().with(BodyLimit::max(bytes))` app-wide, `with(BodyLimit::max(bytes))` on a route/page/layout/layer. Raw Body bypasses limits; custom extractors should buffer via `request::Bytes`. Response tuples: optional status, headers/extensions, body last. Route HTML: `ViewHandle` from `.single().await?`, or `BoxView<'static>` for streaming. - [Errors](docs/router/error.md): `router::error::RouterErrorExt` gives Option/Result `ok_or_not_found()?`, `ok_or_forbidden()?`, etc. `Ok(see_other(href!(page).resolve(cx)))` gives POST/Redirect/GET (303); `redirect` is 307. Unknown errors become 500; `bad_request` messages are public. Error boundaries need an explicit status in fallback. `not_found!()` adds a catch-all page to render misses through layouts; prefix itself needs its own page. - `Err(rewrite(target, body).method(Method::GET).with(value).into())` redispatches with fresh context/cache, discarding pending response changes. Request helpers see rewritten values; `original_*` sees incoming values. Default OriginPolicy checks cross-origin mutations/WebSockets before layers, not authentication. Override it via context: `Router::builder().with(OriginPolicy::trust_origins([...]))` app-wide, `with(OriginPolicy::dangerous_disable())` on a route/page/layout/layer (a layer covers every route under its path). Use POST for mutations. `client_ip(cx)` ignores forwarding headers unless `.trusted_proxies(...)` is configured. `serve(listener, router)`/`Router::handle(request)` support custom hosting. Cloudflare Workers: drop `serve`, buffer the request body, call `Router::handle` from the fetch handler ([example](examples/cloudflare-worker)); no runtime WebSockets. ## Views and state - [Views](docs/view/view.md): `view! {

"text " (expr)

}` captures the surrounding `cx` binding (`Cx` or `&Cx`). To add or override context values, shadow it before constructing the view: `let cx = cx.with(value); view! { ... }`. Void tags have no close, others must close. Quoted text; `(expr)` interpolates values/names. Rust if/for/match/let works in children/attributes; multi-node match arms need braces. Values are escaped; unescaped APIs require trusted markup. - Rendering performance: keep fixed text/attributes in `view!` and fixed attributes in `attributes!` as literals so escaping happens at compile time. Interpolate numbers directly, e.g. `(count)`, instead of allocating strings with `.to_string()` or `format!`. - [Components](docs/view/component.md): `#[component] async fn panel(cx: &Cx, title: &str, #[default] child: Child<'_>) -> Result`; call `panel(title: "Hi",

"Child"

)`. Named props, a comma, then children without commas between them. Calls omit `cx`; the view supplies it automatically. `#[default]`/`#[default(expr)]`/`#[into]` control props; `#[derive(Props)]` provides these builder rules. Reusable markup: call a component `card(item: item)`, not a helper `(card(cx, item))`. Box recursive/different view types with `ViewExt::boxed()`. - Views lazily capture by move, including `cx`: an owned `Cx` moves into the view, while `&Cx` remains a borrow. The macro does not clone the context automatically. Components run concurrently, output in source order. Do ordered side effects before constructing views. `view.single().await?.render(cx)` (ViewExt) renders non-live HTML; `.single()` panics on live views, `.first()` discards later updates. - `disabled=(bool)` includes/omits a boolean attribute; `title=(Option)` omits `None`. Literal `disabled="false"` is still present. Enumerated/ARIA booleans need strings (`"true"`/`"false"`). - Fixed `class="a b"`; composed `class=(class!("base", optional, "on" if active else "off"))`. Render `class!` values directly to avoid building an intermediate string. Outside `view!`, prefer `let classes = class!("a b")` for literal class lists: literals are escaped at compile time and stored as promoted strings, avoiding runtime escaping and copying. Use `StaticClass` for constants. Absent entries skipped, empty class omitted. `attributes! { id="x" }` spreads via `
`, consuming attrs. Merge forwarded classes: `class!("base", attrs.remove("class"))`, then `(attrs)`. No Tailwind conflict resolution. [Classes](docs/view/class.md), [attributes](docs/view/attributes.md). - `#[key(item.id)] for item in items { ... }` establishes identity for stateful components/shards/live regions; missing keys can error. Also supply stable HTML IDs for DOM reordering. Ordinary helpers need explicitly derived `cx.keyed(item.id)` for distinct identities. Use `view::element_id(cx)` to connect labels to inputs or buttons to popovers. Each component instance gets its own HTML IDs based on its identity and where the function is called. The IDs stay the same when the component renders again. - `(StatusCode::NOT_FOUND)`, `(HeaderMap)`, `((HeaderName, HeaderValue))` set response metadata. First in markup order wins per status/header name; before `(slot)` overrides, after supplies defaults. Cannot change once streaming starts. - [Context](docs/context.md): `Cx` holds values for a request by their exact Rust type. `context::(cx)` borrows the closest value and panics if there is none. `try_context::(cx)` returns an Option. `cx.with(value)` creates a child context. A new value shadows older values of its type instead of removing them, and the parent stays unchanged. `cx.with_many((a, b))` adds several values in one child, which is cheaper than chaining `with`. `iter_context::(cx)` visits every value of `T`, closest first, for collected types such as breadcrumbs. Share `Send + Sync + 'static` values across requests with router builder `.with(value)`. Layers can pass a child to `next.run(&cx, body)`, but prefer helper functions for data that code can load itself. Without a router, use `Cx::new().with_shared(Arc)`. Tests can start with `Cx::new()`. Clone Cx for streams and tasks. Read HTTP data through `router::request`. To store a function that computes a value per request, such as in route context, accept `impl AsyncCxFn` (an async closure or `async fn` taking `&Cx` with a `Send` future; `R` is any return type, e.g. `Result`) and keep it as `BoxCxFn`; run it with `.call(cx).await`. `AsyncCxFn1`/`BoxCxFn1` and `AsyncCxFn2`/`BoxCxFn2` take extra arguments; `BoxCxFn::boxed` accepts capturing closures returning `Box::pin(async move { ... })`. Calls are not memoized; load data through `#[memoize]` functions. - Prefer composable `helper(cx: &Cx)` guards called wherever protection is needed. [Memoize](docs/context/memoize.md): `#[memoize]` caches sync/async results per request; requires `cx: &Cx` first even when unused, Hash arguments, owned `Send + Sync + 'static` output; returns `&T`. Callers pass `cx` explicitly. `#[memoize(as_ref)]` borrows Option/Result contents; `?` converts `&topcoat::Error` by cloning. Concurrent calls share work. A result is reused while the function would find the same context values; shadowing a value it read makes it run again, interior mutations do not. `iter_context` depends on every value of the type, including the caller's values below ones the function adds itself. Same-argument recursion panics. ## Browser runtime and streaming Setup: `module_router!().discover().assets(AssetBundle::load()?).layer(RuntimeLayer::new()).build()`; include `topcoat::runtime::script()` in ``. Add application layers before the `RuntimeLayer`. `RuntimeLayer::new().max_runs_per_connection(n)` caps connected renders per WebSocket (default 64; extra requests get 429). Procedures/shards need discovery or `.route(name)`. [Runtime](docs/runtime.md). - [Navigation](docs/runtime/link.md): `runtime::link(href: href!(page), "Page")` opens server-rendered pages without a full reload and preserves signals shared by both pages. Prefetch defaults to `PrefetchMode::Intent` (hover/focus/touch); `Viewport` loads visible links, `Never` waits for navigation. Override per link with `prefetch:`, per scope with `cx.with(mode)`, or per app with `.with(mode)` on the router. `prefetch_mode(cx)` reads the context default; `link_attrs(cx, href!(page), prefetch_mode(cx))` adds the same behavior to custom anchors. Plain `` links retain normal navigation. ```rust // Inside a component/page with cx: &Cx; import runtime::{signal, Event}. let query = signal(cx, String::new); Ok(view! {

$(query.get())

}) ``` - `$(...)` = `runtime::expr!(...)`: initial server value, reactive browser updates. `@event` accepts closure/JS string; `:attr` binds attribute; a static expression renders a plain attribute unless made `.dynamic()`. On reruns, plain `value`/`checked`/`selected` are defaults: user-edited controls keep their state; bind with a signal or `Expr::dynamic` to set the live value every render. Captures clone browser-visible snapshots; never capture secrets. [Expressions](docs/runtime/expr.md) support selected operations on numbers/bool/strings/Option/Result/collections/tuples/signals; blocks/if/closures/async/loop/while, not arbitrary Rust. Tuple literals include `()`, `(value,)`, and `(a, b)`; fields use `.0`, `.1`, etc. Unsuffixed integers are usize; overflow panics. - [Records](docs/runtime/record.md): add `#[record]` to a struct with named fields and no generics to use it in expressions, signals, and procedure inputs/outputs. Fields use expression vocabulary types, including nested records. Construct with `Todo { title, done: false }`; borrow a signal's field with `todo.read().title`. Derive `Clone` for `get`/`clone`. Render fields individually; records cannot be compared or rendered directly. Capturing exposes all fields, including private ones. Validate records received from the browser. - `raw!("${binding}.jsMethod()", rust_fallback)` escapes to JS with equivalent server evaluation. No fallback means browser-only; a fallback must read the signals its JS depends on. - Server `.get()`/`.read()` tracks enclosing page/shard for server rerenders; inside `$(...)` tracks browser updates. `_untracked` avoids server rerenders. Signal handles alone create no dependency. Validate all restored values as client input. - [Procedure](docs/runtime/procedure.md): `#[procedure] async fn save(cx: &Cx, value: String) -> Result`; browser async handler calls `save(query.get()).await`, omitting cx, receiving Ok value. Server Err fails expression; nested Result carries client-handled failures. Evaluating calls during server rendering panics. - [Shard](docs/runtime/shard.md): `#[shard] async fn results(cx: &Cx, query: String) -> Result`; `results(query: $(query.get()))`. Renders inline initially, requests HTML on input changes. Args use expression vocabulary; Signal props share handles. Endpoints bypass page/layout guards: authorize inside every shard/procedure and validate inputs. Generated POST paths are unstable; explicit paths allow no captures. - [Live](docs/view/live.md): `live! { emit! { "Loading" }?; let x = fetch().await?; emit! { (x) } }` replaces region per emission; returns Result. Must emit >=1 time; first emission blocks initial HTML. `suspense(fallback: view! { ... }, child())` handles loading; `error_boundary(fallback: |e| Ok(view! { ... }), child())` handles errors. HTTP swaps include their script automatically. - `runtime::connected(cx)` requests WebSocket after HTTP completes, reruns page/shard with true. Emit and finish on HTTP; indefinite waits only when connected. Reconnect restarts rendering, so reread state; start durable jobs elsewhere. `connected_untracked` checks without requesting connection. ## Assets and services - [Page metadata](docs/metadata.md): attach `Metadata` to layouts and pages with `with(...)`, then render `topcoat::metadata::tags()` first in ``. Pages override individual fields from their layouts. You can often pass `t!` and `asset!` values directly, such as `.title(t!(page_title))` or `.open_graph(OpenGraph::new().image(asset!("./preview.png")))`. Translations and asset URLs resolve when rendered, so these do not need `from_fn`. Use `Metadata::from_fn` when values need request-specific loading, such as looking up a product, and `metadata(cx).await?` to read the combined values. Canonical and Open Graph tags request absolute URLs, so add `.with(BaseUrl::new(...)?)` to the router when using site-relative URLs. The `metadata` feature is enabled by default. - [Assets](docs/asset.md): `asset!("./image.png")` renders hashed URL. `./`/`../` from source file, `{mod}/` from the module's directory (`src/about.rs` -> `src/about/`), `{out_dir}/` from the build script output, bare paths from crate root, HTTP(S) downloaded/cached. The path must be a string literal; a missing local file fails compilation. `topcoat dev` bundles; manual `topcoat asset bundle --release` must match profile/package. `.assets(AssetBundle::load()?)` loads `assets/` beside binary. Custom `--out` needs `load_dir`. Deploy matching binary+bundle; missing assets panic. `AssetConfig::hosted_at(base, catalog)` supports separately uploaded CDN files. - [Tailwind](docs/tailwind.md): enable `tailwind` on runtime/build dependencies (build: `default-features = false`). build.rs: `topcoat::tailwind::BuildConfig::new().render().unwrap()`; layout: ``. Needs bundle. Scans literal classes, not dynamically assembled names. `.input("styles.css")` selects CSS. - [Fonts](docs/font.md): `font!("Family", @font-face { src: url(...); ... })`, `font_face!` for one face. `.discover()`/`.font(FONT)` registers; `font::link(font: FONT)` loads CSS. `font-fontsource`: `fontsource_font!(GEIST, weight: [400, 700], host: Asset)` bundles files; default host is CDN. - [Icons](docs/icon.md): `icon(data: ICON, label: "Meaning")` renders SVG; unlabeled = decorative. `icon-iconify` on runtime/build dependencies; build.rs: `topcoat::icon::iconify::BuildConfig::new().icon_set("feather").stage().unwrap()`. `iconify::include!("feather")` creates `feather::TRASH_2`; `iconify_icon!("feather:trash-2")` yields one icon. - [UI](docs/ui.md), `ui`: configure Tailwind; `topcoat ui init`; `topcoat ui add button`. Compile installed styles.css; load its font. Import/edit copied `src/components`; commit components.toml. Props: variant/size enums, attrs, children. `ui list` shows updates; `add --overwrite` replaces local edits; `remove` deletes. `.dark` ancestor selects dark theme. Shadows are disabled by default. To enable them, replace `--shadow-xs` and `--shadow-sm` in both `:root` and `.dark` with the commented values in styles.css. - [Cookies](docs/cookie.md): add `.layer(CookieLayer::new())`, import `cookie::Cookies`, and call `get`, `add`, or `remove` on `cookies(cx)`. Pass a `(name, value)` tuple for a plain cookie, or build one with attributes using `Cookie::build((name, value)).path("/").secure(true).http_only(true).same_site(SameSite::Lax)`. Write before the response seals. Later writes panic, while errors and redirects keep pending writes. Remove a cookie using the same name, path, and domain. Prefix adapters accept names with or without the prefix and return names without it. Signed cookies detect tampering. Private cookies also encrypt the value. For the shared helpers, add a saved Key to the router with `.with(key)`. `cookie_store::(jar, "name").parse_or_default().update(...).commit()?` saves JSON in a cookie. Changes are discarded unless committed. - [Sessions](docs/session.md): `.layer(CookieLayer::new())`; the default token store uses the cookie. Change defaults with `.with(SessionLifetime(duration))` (default 30 days) or `.with(SessionTokenStore::new(store))`. Authenticate, then `session::start(cx).await?`; store token_hash/user/expiry, never raw token. `token_hash(cx).await?` needs application lookup+expiry validation. `stop` returns hash to delete; `refresh` requires validation+persisted expiry; `rotate` requires revoking old hash+persisting replacement. Default cookie: __Host-/Secure/HttpOnly/SameSite=Lax/Path=/. Mutate before streaming. TokenStore changes transport, not database storage. - [Mail](docs/mail.md), `mail`/`mail-smtp`: `.with(Mailer::new(transport))`; `Mail::builder().from(Mailbox::new("...")?).to([Mailbox::new("...")?]).subject("...").html(cx, view! { ... }).await?.build()?` (fails with `MailError` on a missing sender, recipients, or body, or an invalid attachment or header), then `send(cx, mail).await?`. Plain text derived from HTML; attachments supported. FileTransport for .eml, MemoryTransport for tests, SmtpTransport for delivery. - [Localization](docs/l10n.md), `l10n`: create a `Locale` with `locale!("de-CH")` and set it in a `#[layer]` with `next.run(&cx.with(locale), body)`. Read it with `locale(cx)` or `try_locale(cx)`. A context created in a layout does not reach the page. Choose a locale with `negotiate(accept_language(cx), &available)`, or read a `module_param!` with type `Locale` and reject unsupported values. `Locale` implements `Serialize`/`Deserialize` as its text, e.g. for cookies and forms. Set ``. [Formatting](docs/l10n.md#formatting): with `l10n-number`, render `Number::new(1234.5).min_fraction_digits(2)` in `view!` for the context's locale (options follow `Intl.NumberFormat`; no currency or percent). With `l10n-datetime`, render `DateTime::date(2026, 10, 12)?.at(14, 5, 0)?` with `.date_style(DateTimeStyle::Long)`/`.time_style(...)` (`Intl` `dateStyle`/`timeStyle`; one style hides the other part; no time zone names). The `jiff` feature adds `DateTime::from` for `jiff` civil values, `Zoned` (its own zone) and `Timestamp` (UTC). Both types have `.resolve(cx)`/`.resolve_in(&locale)`. [Fluent](docs/l10n.md#fluent), `l10n-fluent` (includes `l10n-number`): prefer feature-scoped translations with `messages!("{mod}/l10n/{locale}.ftl")` in each feature module; e.g. `src/admin.rs` loads `src/admin/l10n/en.ftl`. Other layouts are supported. Each call creates its own `t!` and `t::locales()`. Use `{locale}/**/*.ftl` for several files per locale. Pass `t::locales()` to `negotiate`. Translate with `t!(title)`, pass variables with `t!(greeting, name: value)` or `t!(greeting, name)`, and select attributes with `t!(search.placeholder)`. String keys are supported when Rust cannot tokenize the key, e.g. `t!("item-1e")`. rustfmt splits hyphenated keys into `t!(a - b)`, so use snake case IDs such as `search_field`, or quote hyphenated keys: `t!("search-field.placeholder")`. Hyphenated argument names need no quotes: `t!(greeting, user-name: name)`. In `view!`, render translations directly: `
(t!(title))
` or `placeholder=(t!(search.placeholder))`; the context's locale is used automatically. Use `.resolve(cx)` only when text is needed outside rendering, or `.resolve_in(&locale)` for an explicit locale. `Translation` is `Clone + Send + Sync`. Numbers in messages are formatted for the locale; set options with `NUMBER($n, minimumFractionDigits: 2)`. With `l10n-datetime`, pass a `DateTime` or `jiff` value and format it with `DATETIME($date, dateStyle: "long", timeStyle: "short")`. Regional locales can use parent messages, such as `de-CH` using `de`. Missing messages display `key(args)` without switching languages. Run `topcoat l10n check` to check keys, variables and references and warn about unused messages or arguments. It reloads message files before checking. `topcoat dev` warns about translation problems after each build. ## Optional HTTP integrations - `fs`: import `RouterBuilderDirectoryExt`; `.public_dir("./public")` or `.serve_dir("/files/{*rest}", dir)` for fixed file URLs. - [SSE](docs/router/content/sse.md), `sse`: `Sse::new(stream)` of Result; `.keep_alive(KeepAlive::new())`; Event data/id, `last_event_id(cx)` for resume. Own captures/clone Cx; use `+ use<>` to avoid implicit context borrowing. - [WebSocket](docs/router/content/websocket.md), `websocket`: GET takes WebSocketUpgrade; authenticate before on_upgrade, move owned captures, recv/send Message; configure protocols/limits. - [Multipart](docs/router/content/multipart.md), `multipart`: `next_field().await?`, consume/drop before next; bytes/text/chunk; BodyLimit applies across fields. - [Sitemap](docs/router/content/sitemap.md), `sitemap`: return `Sitemap::new().url(...)`; relative entries require `.with(BaseUrl::new(...)?)` on the router. `SitemapUrl` adds last-modified/frequency/priority. - [OpenAPI](docs/openapi.md), `openapi`: also `cargo add schemars` and derive `schemars::JsonSchema` on body types. Attach `with(Operation::new().summary("...").request::>().response_with::>(StatusCode::OK, "..."))` to a route; `.response(status, "...")` for no body. A route is documented only when an `Operation` is on it, on a wrapping layer/layout, or on the router builder; `.hidden(true)` excludes it. Closest setting wins; tags, parameters and responses add up, replaced per parameter/status. Operations are not checked against handler signatures. Path parameters are listed automatically as strings; type them with `.parameter(Parameter::path("id").schema::())`. `Parameter::query/header/cookie` are optional unless `.required(true)`. `.query::()` documents each field of a `#[query_params] #[derive(JsonSchema)]` struct. `Option>` is an optional request body; implement `Content` for custom body types. Serve with `#[route(GET "/openapi.json")] async fn spec(cx: &Cx) -> Result { Ok(OpenApi::new("Title", "1.0.0").generate(&router(cx))) }`; `generate(&router)` needs no request, `.to_json_pretty()` exports. `OpenApi::security_scheme("token", SecurityScheme::bearer())` plus `Operation::security("token")`/`.no_security()` document authentication only. `generate` panics on an unknown path parameter or security scheme, or a duplicate operation ID. Emits OpenAPI 3.1. - [Tower](docs/router/tower.md), `tower`: TowerRoute mounts services, TowerLayer wraps handling, TowerService exposes Topcoat. Full URI forwarded; StripPrefixLayer opts into stripping. Register catch-all prefix separately. No middleware retries. - [htmx](docs/htmx.md), `htmx`: `hx_request(cx)` chooses fragments; Hx* response parts precede body. [Alpine AJAX](docs/alpine_ajax.md), `alpine-ajax`: `ajax_request/ajax_targets` choose fragments; load plugin before Alpine, both deferred. [Datastar](docs/datastar.md), `datastar`: Signals reads GET query/non-GET body; PatchElements/PatchSignals emit SSE updates. Load each library's browser script separately.