place

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

ts
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.

ts
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.

ts
// 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 }.

ts
load: async ({ params }) => ({ post: await db.posts.findOne(params.id), })

Validates URL query params before view(). Pair with useSearch<T>(props) in the view to get a typed accessor at the call site.

ts
search: shape({ page: 'number', tag: 'string?' })

url

Pure URL-derived props. Runs on both server and client. Use for params extraction or transforms.

ts
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).

ts
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 a def throws at runtime with "the second argument (definition) is required."
  • page('foo', def) (path doesn't start with /) throws at runtime.
  • page(def) with an on: dict throws — the on: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 client
  • layout() — composable layout primitive
  • action() — standalone action factory (when on: is the wrong shape)