page()
Declares a route. Each page() call produces a value (a Page object) that app()'s pages array registers; the framework derives the routes table from each page's path field.
Signature
page<U, L, S>(
path: string,
def: PageDef<U, L, S>,
): Page<U, L, S>
U is URL-derived props (from url()), L is load-data shape, S is the typed search schema return. All three default to sensible empty shapes when omitted.
Options
view (required)
The render function. Receives the merged { ...urlProps, ...loadData, search } object. Return a View — JSX or any el()/factory call.
view: (props) => <article>{props.title}</article>
meta
Document metadata. Static value or a function of the merged props for dynamic titles. Server-side only — the framework emits the head tags in the SSR'd HTML.
// Three accepted shapes:
meta: 'My page' // string shorthand → { title }
meta: { title: 'My page', og: { ... } } // full object
meta: (props) => `${props.title} — My site` // function returning either
// Or drop `meta:` entirely — the framework auto-promotes the first
// <h1> in the body. With a layout that declares
// `titleTemplate: '%s · my site'`, this is all you need:
view: () => <article><h1>My page</h1>…</article>
load
Server-only data loader. Result is serialized into the SSR'd HTML and read back by the client at boot. Sync or async. Receives a LoadCtx with { req, url, params }.
load: async ({ params }) => ({
post: await db.posts.findOne(params.id),
})
search
Validates URL query params before view(). Pair with useSearch<T>(props) in the view to get a typed accessor at the call site.
search: shape({ page: 'number', tag: 'string?' })
url
Pure URL-derived props. Runs on both server and client. Use for params extraction or transforms.
url: (url, params) => ({ id: params.id })
on
Co-located actions. Each entry becomes a typed caller pageRef.{key}(input) + an auto-registered POST {path}/_action/{key} endpoint with the full security pipeline (auto-CSRF, same-origin, body-limit, proto-pollution).
on: {
save: async (input: { title: string }, { params }) => {
await db.posts.update(params.id, input)
return { ok: true }
},
}
onError + onNotFound
Per-page error views. onError renders when load() or view() throws; onNotFound renders on a thrown notFound() signal. Falls back to the global serve-level handlers when absent.
layout
Layout chain wrapping this page. Layouts compose outside-in: layout: [rootLayout, sectionLayout]. Each layout's load() runs in chain order; merged load-data flows into all layouts' view/meta plus the page's.
styles · headers · revalidate · streaming
Stylesheets (inline or external), extra response headers, ISR config (lazy SWR), and streaming-SSR opt-in. See the source for full type definitions; defaults are chosen so the simple case requires nothing.
Failure modes
page('/x')without adefthrows at runtime with "the second argument (definition) is required."page('foo', def)(path doesn't start with/) throws at runtime.page(def)with anon:dict throws — theon:handlers need a path to compose with.- Two pages with the same path →
app()throws on registration with "duplicate path".
See also
- Getting started — five-minute walkthrough
app()— registers pages, dispatches to server or clientlayout()— composable layout primitiveaction()— standalone action factory (whenon:is the wrong shape)