Page wrappers
page.load and page.actions wrap +page.server.c exports with container resolution, form parsing and valibot validation.
page is auto-imported in server files. It mirrors route() with two differences that match SvelteKit's form conventions: load returns its result as plain data, and actions return fail(400, ...) on validation errors instead of throwing.
import { notes } from '$lib/norns/notes/server/public'
import { createNoteSchema } from '$lib/norns/notes/shared/schema'
export load := page.load
handler: async ({ container }) =>
notes: await notes(container).list()
export actions := page.actions
create:
input: createNoteSchema
run: async ({ input, container }) =>
id := await notes(container).create input
throw redirect 303, `/notes/${id}`#page.load({ handler })
The handler receives:
| Field | Value |
|---|---|
container |
the request-scoped container (event.locals.container) |
event |
the raw SvelteKit ServerLoadEvent |
params, url |
shortcuts for event.params and event.url |
user |
event.locals.user, whatever your auth handle put there |
Whatever the handler returns is the page's data. Throw error(404, ...) or redirect(...) as in plain SvelteKit.
#page.actions({ name: { input?, run } })
Each action is { input?, run }:
- When
inputis present the form body is read withrequest.formData(), flattened withObject.fromEntries, and validated. On failure the action returnsfail(400, { errors, values }):errorsis the issue list,valuesis the raw form data echoed back so the page can re-render it. runreceives{ input, container, event, user }.inputis the validated value (orundefinedwhen there is no schema).
A route with a dynamic parameter reads it from the event:
export actions := page.actions
update:
input: updateNoteSchema
run: async ({ input, container, event }) =>
id := Number event.params.id
await notes(container).update id, input
{ saved: true }
delete:
run: async ({ container, event }) =>
await notes(container).remove Number(event.params.id)
throw redirect 303, '/notes'#Rendering errors
The fail(400, { errors, values }) shape is what norns-ui's <Form form={form}> reads. Each <Field name="..."> inside looks up its own message from errors by path, and values lets inputs keep what the user typed:
Form(action="?/create" form!="{form}")
Field(label="Title" name="title" required)
Input(name="title" value!="{form?.values?.title ?? ''}" required)
Btn(type="submit" variant="primary") CreateNote
Actions only see form-encoded bodies. A client that posts JSON must call a +server.c endpoint wrapped in route(), not a form action.