@getforma/router is an optional package. It retains compatible layouts while changing the URL and destination content. Native applications can keep rendering HTML and typed props in Rust; navigation does not require moving rendering to JavaScript. Use the same coordinated 3.2.0-rc.2 candidate across the stack.
Choose one adapter per document#
| Application | Adapter | Destination |
|---|---|---|
| Typed Rust pages with islands | createNativeAdapter(bootstrap) | A policy-bound typed owner branch |
| Browser-rendered application | createBrowserAdapter() | Your route's Forma render function |
| Reviewed generated static pages | createHtmlAdapter({ head, pages }) | A registered, hashed HTML content region |
| Ordinary links, forms or no-JS pages | None | A complete document response |
Crossing these profiles takes a document navigation. Keep one coordinator per document. Ordinary pages do not need the router bundle. Remove competing history/click handlers before migrating an existing navigator.
Browser routes#
Mount on an unowned root. Components run once; use accessors for reactive values.
import { h, createSignal } from "@getforma/core";
import {
createRouter,
createBrowserAdapter,
Link,
Outlet,
} from "@getforma/router";
const router = createRouter({
root: document.querySelector<HTMLElement>("#app")!,
adapter: createBrowserAdapter(),
routes: [
{
id: "workspace",
render({ outlet }) {
const [count, setCount] = createSignal(0);
return h(
"section",
null,
h("button", { onClick: () => setCount(count() + 1) }, () => count()),
Link({ href: "/projects/alpha", children: "Alpha" }),
Link({ href: "/projects/beta", children: "Beta" }),
Outlet({ outlet }),
);
},
children: [
{
id: "project",
path: "projects/:name",
render({ params }) {
return h("h1", null, params.name);
},
},
],
},
],
});
await router.ready;
await router.navigate("/projects/beta");Child parameters do not recreate the parent. Route definitions support static segments, named parameters, a terminal splat, pathless parents and index leaves; static segments rank first. See the router declarations and route grammar for bounds.
A prepared browser owner has its own lifetime signal. Cancellation aborts an uncommitted owner; retaining a layout preserves its signal across child navigation. Register listeners, timers and other resources with ordinary Forma ownership cleanup. Disposing the router releases its work and restores browser scroll restoration.
Native pages and islands#
Start from the connected native template. Declare a navigation group in the build with one shell, its leaves and a client initialization entry. The compiler verifies the literal child outlet is outside all island-owned subtrees. The group shares CSS/fonts and a closed, integrity-checked executable graph.
Enable forma-server's navigation feature. Bind finalized typed page handles and use NavigationBranch to compose the ordered owners. Initial requests return the complete document; enhanced requests return typed navigation data. createNativeAdapter validates build, assets, policy, context and owner contracts before replacing the changed suffix. It adopts fresh leaf islands and retains only the compatible common prefix. It does not replay scripts from fetched HTML.
Return the complete response with its CSP, nonce, Vary and no-store policy. Account for middleware and proxies before declaring SealedNavigationPolicy::page_response_only; that declaration cannot describe policies applied elsewhere by guesswork. Never manufacture ownership metadata or concatenate unchecked fragments. Follow the native API walkthrough.
Generated documents#
Generate a full-document SHA-256 registry after producing the final HTML. Every admitted page must match the build-owned head and contain one marked content region. The adapter validates the full response before parsing that region. Its bounded profile excludes executable/resource-loading nodes, SVG, custom elements, inline styles and native ownership markers. Unsupported pages retain document navigation.
Application widgets still need lifecycle ownership: connect editors, search results, observers and workers to the adapter session, and dispose departing page resources. A persistent header does not justify retaining a departing page's worker. Read the generated HTML contract before adding a registry.
Forms, recovery and context#
Ordinary anchors work before readiness and without JavaScript. Modified clicks, external URLs, downloads, targets and data-no-router keep native browser behavior. Programmatic navigation reports explicit outcomes; a document fallback is different from a committed enhanced transition.
Native enhanced forms use declared canonical server endpoints. At most one enhanced POST is sent at a time, and the router never retries it automatically. A lost response creates an unknown outcome; a durable attempt marker survives document reload. Resolve uncertainty using application status or idempotency evidence. A successful GET alone cannot prove whether the prior mutation succeeded.
Validation needs application field markup as well as the router's summary. Give
the summary data-forma-form-error and role="alert". Associate each field with
its error using aria-describedby; return the same error and aria-invalid
state in no-JavaScript responses. For native templates, compute that attribute
as a declared scalar prop in Rust.
For a server contract returning fields.name, supply this onAction option:
onAction(outcome, form) {
if (outcome.outcome !== 'validation') return;
const input = form.querySelector('[name="name"]');
const error = form.querySelector('#name-error');
const message = outcome.data.fields?.name;
if (input && error) {
const text = typeof message === 'string' ? message : '';
input.setAttribute('aria-invalid', text ? 'true' : 'false');
input.setAttribute('aria-describedby', 'name-error');
error.textContent = text;
}
}The router focuses the summary; the application owns field associations and safe value preservation. Render errors as text and avoid echoing secrets. Verify the actual spoken feedback with assistive technology.
Invalidate retained context before login, logout or account/role changes. Rust still owns authentication, authorization, CSRF and transactions. A build, policy, asset or context mismatch takes bounded document recovery, which can discard layout state. Deploy and roll back matching server, manifest, group assets and HTML together.
Verify your application#
Test actual document and layout-node identity, fresh leaf props, direct entry and refresh, Back/Forward, cancellation, focus/scroll, no-JS links/forms and cleanup. Test incompatible builds as explicit fallbacks. Use keyboard and assistive technology against the actual UI. The live native router lab and downloadable sample provide a small connected starting point, not a substitute for your application's acceptance tests.