void 0.1.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_PROMPT.md +15 -0
- package/README.md +62 -123
- package/dist/auth-BdsJ0Aff.d.mts +43 -0
- package/dist/auth-cmd-Dx8oPKZC.mjs +43 -0
- package/dist/auth-migrations-BAtAck2g.mjs +117 -0
- package/dist/better-auth-shared-C9_GHSkR.d.mts +71 -0
- package/dist/better-auth-shared-CdYmQGry.mjs +163 -0
- package/dist/cache-W82I8ihI.mjs +47 -0
- package/dist/cancel-deploy-BOBTqqh0.mjs +59 -0
- package/dist/cf-access-Dee5cXxL.mjs +22 -0
- package/dist/chunk-DJd-R1mw.mjs +34 -0
- package/dist/cli/cli.d.mts +1 -0
- package/dist/cli/cli.mjs +1807 -0
- package/dist/client-snXOjrp1.mjs +565 -0
- package/dist/collect-CjeZgz5D.mjs +55 -0
- package/dist/config-BIa9HwVX.mjs +573 -0
- package/dist/config-BzM9Dy7T.mjs +37 -0
- package/dist/config-CvHtTM0q.mjs +30 -0
- package/dist/create-project-BIA15W7z.mjs +90 -0
- package/dist/db-DsRoMcfN.mjs +895 -0
- package/dist/defer-DcxEsVH1.mjs +49 -0
- package/dist/delete-DAP6yDc7.mjs +64 -0
- package/dist/deploy-BPKblFx6.mjs +2424 -0
- package/dist/discover-B7FkXBLB.mjs +40 -0
- package/dist/dist-DUyXJLkq.mjs +2667 -0
- package/dist/dist-Dayj3gCK.mjs +1287 -0
- package/dist/domain-BGofcQ6I.mjs +79 -0
- package/dist/dotenv-DwO4ti0Z.mjs +173 -0
- package/dist/drizzle-NnudE_UN.mjs +232 -0
- package/dist/env-CyG3tvU0.mjs +301 -0
- package/dist/env-helpers-Dr9Y7RnE.d.mts +52 -0
- package/dist/env-raw-BDL4TvdN.mjs +32 -0
- package/dist/env-types-DknSA4SO.mjs +64 -0
- package/dist/env-validation-DJKjR_8q.mjs +163 -0
- package/dist/fetch-error-BQ8sZ5Nd.mjs +266 -0
- package/dist/fetch-error-CVZ5CGA-.d.mts +20 -0
- package/dist/gen-U0Ktr4Zd.mjs +761 -0
- package/dist/handler-B0ds0OHJ.d.mts +269 -0
- package/dist/head-P-egrtFE.d.mts +45 -0
- package/dist/headers-DCXc7mDs.mjs +279 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.mjs +4695 -0
- package/dist/init-C7wS5iGP.mjs +2625 -0
- package/dist/link-p2R6NbgN.mjs +49 -0
- package/dist/list-Bfel-QLc.mjs +113 -0
- package/dist/log-DXdqnmhF.mjs +26 -0
- package/dist/login-CkcXUiIu.mjs +72 -0
- package/dist/logs-DmkrRvx6.mjs +98 -0
- package/dist/magic-string.es-D6g9UnIy.mjs +1011 -0
- package/dist/mcp-CaQzfeUi.mjs +373 -0
- package/dist/node-DDfXj10V.mjs +54 -0
- package/dist/output-BwlcIYSR.mjs +139 -0
- package/dist/pages/client.d.mts +198 -0
- package/dist/pages/client.mjs +980 -0
- package/dist/pages/head-client.d.mts +15 -0
- package/dist/pages/head-client.mjs +90 -0
- package/dist/pages/head.d.mts +2 -0
- package/dist/pages/head.mjs +112 -0
- package/dist/pages/index.d.mts +38 -0
- package/dist/pages/index.mjs +76 -0
- package/dist/pages/islands-plugin.d.mts +50 -0
- package/dist/pages/islands-plugin.mjs +195 -0
- package/dist/pages/prefetch.d.mts +31 -0
- package/dist/pages/prefetch.mjs +90 -0
- package/dist/pages/protocol.d.mts +3 -0
- package/dist/pages/protocol.mjs +193 -0
- package/dist/pages/serialize.d.mts +10 -0
- package/dist/pages/serialize.mjs +14 -0
- package/dist/pathe.M-eThtNZ-D-kmWkCS.mjs +150 -0
- package/dist/plugin-inference-oZ6Ybu2_.mjs +2447 -0
- package/dist/prepare-BAtWufvm.mjs +99 -0
- package/dist/preset-D4I73kT4.mjs +221 -0
- package/dist/project-TqORyHn8.mjs +72 -0
- package/dist/project-cmd-B7lQp3F3.mjs +67 -0
- package/dist/project-slug-CKam8lF9.mjs +11 -0
- package/dist/project-tsconfig-DfkESbDL.mjs +63 -0
- package/dist/protocol-BWzXs2A2.d.mts +34 -0
- package/dist/providers-B3aMxWzP.mjs +67 -0
- package/dist/resolve-project-Br5BR03U.mjs +29 -0
- package/dist/rollback-gyC59l7U.mjs +92 -0
- package/dist/route-types-DReF1gUY.mjs +255 -0
- package/dist/routes-stub.d.mts +55 -0
- package/dist/routes-stub.mjs +1 -0
- package/dist/runner-6Ep3fNQu.mjs +123 -0
- package/dist/runner-pg-D0wWHYnr.mjs +57 -0
- package/dist/runtime/ai.d.mts +127 -0
- package/dist/runtime/ai.mjs +348 -0
- package/dist/runtime/auth-client-react.d.mts +8 -0
- package/dist/runtime/auth-client-react.mjs +6 -0
- package/dist/runtime/auth-client-solid.d.mts +8 -0
- package/dist/runtime/auth-client-solid.mjs +6 -0
- package/dist/runtime/auth-client-svelte.d.mts +8 -0
- package/dist/runtime/auth-client-svelte.mjs +6 -0
- package/dist/runtime/auth-client-vue.d.mts +8 -0
- package/dist/runtime/auth-client-vue.mjs +6 -0
- package/dist/runtime/auth-client.d.mts +8 -0
- package/dist/runtime/auth-client.mjs +6 -0
- package/dist/runtime/auth.d.mts +2 -0
- package/dist/runtime/auth.mjs +22 -0
- package/dist/runtime/better-auth-pg.d.mts +11 -0
- package/dist/runtime/better-auth-pg.mjs +51 -0
- package/dist/runtime/better-auth.d.mts +11 -0
- package/dist/runtime/better-auth.mjs +33 -0
- package/dist/runtime/client.d.mts +6 -0
- package/dist/runtime/client.mjs +5 -0
- package/dist/runtime/db-pg.d.mts +2 -0
- package/dist/runtime/db-pg.mjs +1 -0
- package/dist/runtime/db.d.mts +17 -0
- package/dist/runtime/db.mjs +30 -0
- package/dist/runtime/drizzle-arktype.d.mts +1 -0
- package/dist/runtime/drizzle-arktype.mjs +2 -0
- package/dist/runtime/drizzle-valibot.d.mts +1 -0
- package/dist/runtime/drizzle-valibot.mjs +2 -0
- package/dist/runtime/drizzle-zod.d.mts +1 -0
- package/dist/runtime/drizzle-zod.mjs +2 -0
- package/dist/runtime/env-helpers.d.mts +2 -0
- package/dist/runtime/env-helpers.mjs +173 -0
- package/dist/runtime/env-public-client.d.mts +22 -0
- package/dist/runtime/env-public-client.mjs +54 -0
- package/dist/runtime/env-public.d.mts +143 -0
- package/dist/runtime/env-public.mjs +366 -0
- package/dist/runtime/env.d.mts +13 -0
- package/dist/runtime/env.mjs +51 -0
- package/dist/runtime/fetch-stream.d.mts +51 -0
- package/dist/runtime/fetch-stream.mjs +81 -0
- package/dist/runtime/fetch.d.mts +59 -0
- package/dist/runtime/fetch.mjs +18 -0
- package/dist/runtime/handler.d.mts +3 -0
- package/dist/runtime/handler.mjs +85 -0
- package/dist/runtime/isr.d.mts +26 -0
- package/dist/runtime/isr.mjs +43 -0
- package/dist/runtime/kv.d.mts +48 -0
- package/dist/runtime/kv.mjs +106 -0
- package/dist/runtime/log.d.mts +24 -0
- package/dist/runtime/log.mjs +31 -0
- package/dist/runtime/migration-handler-pg.d.mts +6 -0
- package/dist/runtime/migration-handler-pg.mjs +85 -0
- package/dist/runtime/migration-handler.d.mts +19 -0
- package/dist/runtime/migration-handler.mjs +92 -0
- package/dist/runtime/queues.d.mts +7 -0
- package/dist/runtime/queues.mjs +8 -0
- package/dist/runtime/remote/binding-handler.d.mts +15 -0
- package/dist/runtime/remote/binding-handler.mjs +208 -0
- package/dist/runtime/remote/index.d.mts +8 -0
- package/dist/runtime/remote/index.mjs +461 -0
- package/dist/runtime/response.d.mts +14 -0
- package/dist/runtime/response.mjs +30 -0
- package/dist/runtime/sandbox.d.mts +17 -0
- package/dist/runtime/sandbox.mjs +19 -0
- package/dist/runtime/schema-d1.d.mts +1 -0
- package/dist/runtime/schema-d1.mjs +2 -0
- package/dist/runtime/schema-pg.d.mts +1 -0
- package/dist/runtime/schema-pg.mjs +2 -0
- package/dist/runtime/seed.d.mts +30 -0
- package/dist/runtime/seed.mjs +6 -0
- package/dist/runtime/storage.d.mts +7 -0
- package/dist/runtime/storage.mjs +14 -0
- package/dist/runtime/validator.d.mts +2 -0
- package/dist/runtime/validator.mjs +72 -0
- package/dist/runtime/ws-server.d.mts +26 -0
- package/dist/runtime/ws-server.mjs +296 -0
- package/dist/runtime/ws.d.mts +123 -0
- package/dist/runtime/ws.mjs +103 -0
- package/dist/scan-Ba4hFwlH.mjs +324 -0
- package/dist/scan-C6HMEIdW.mjs +318 -0
- package/dist/secret-CeRSukgM.mjs +109 -0
- package/dist/skills-ipldjlKE.mjs +62 -0
- package/dist/standard-schema-9CRjx-uR.d.mts +42 -0
- package/dist/subcommand-prompt-BKjuNAPb.mjs +349 -0
- package/dist/sveltekit.d.mts +20 -0
- package/dist/sveltekit.mjs +61 -0
- package/dist/types-mHOEwpW4.d.mts +57 -0
- package/dist/validate-CaMavMxu.mjs +146 -0
- package/dist/yarn-pnp-BFqMV_bl.mjs +196 -0
- package/getting-started-prompt.txt +26 -0
- package/package.json +322 -30
- package/schema.json +364 -0
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +175 -0
- package/skills/void/SKILL.md +75 -0
- package/skills/void/command/void.md +7 -0
- package/skills/void/docs/guide/ai.md +235 -0
- package/skills/void/docs/guide/app-types.md +103 -0
- package/skills/void/docs/guide/auth.md +257 -0
- package/skills/void/docs/guide/database/d1.md +106 -0
- package/skills/void/docs/guide/database/postgresql.md +106 -0
- package/skills/void/docs/guide/database.md +418 -0
- package/skills/void/docs/guide/deployment.md +98 -0
- package/skills/void/docs/guide/edge/headers.md +79 -0
- package/skills/void/docs/guide/edge/prerendering.md +83 -0
- package/skills/void/docs/guide/edge/redirects.md +116 -0
- package/skills/void/docs/guide/edge/revalidation.md +131 -0
- package/skills/void/docs/guide/edge/rewrites.md +354 -0
- package/skills/void/docs/guide/edge/static-assets.md +72 -0
- package/skills/void/docs/guide/env-vars.md +298 -0
- package/skills/void/docs/guide/index.md +80 -0
- package/skills/void/docs/guide/jobs.md +91 -0
- package/skills/void/docs/guide/kv.md +107 -0
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +419 -0
- package/skills/void/docs/guide/pages-routing/head.md +130 -0
- package/skills/void/docs/guide/pages-routing/islands.md +405 -0
- package/skills/void/docs/guide/pages-routing/layouts.md +362 -0
- package/skills/void/docs/guide/pages-routing/loaders.md +267 -0
- package/skills/void/docs/guide/pages-routing/markdown.md +625 -0
- package/skills/void/docs/guide/pages-routing/overview.md +236 -0
- package/skills/void/docs/guide/pages-routing/view-transitions.md +140 -0
- package/skills/void/docs/guide/queues.md +140 -0
- package/skills/void/docs/guide/quickstart.md +233 -0
- package/skills/void/docs/guide/remote-dev.md +67 -0
- package/skills/void/docs/guide/sandboxes.md +82 -0
- package/skills/void/docs/guide/server-routing.md +246 -0
- package/skills/void/docs/guide/ssg.md +74 -0
- package/skills/void/docs/guide/ssr.md +105 -0
- package/skills/void/docs/guide/storage.md +67 -0
- package/skills/void/docs/guide/type-safety.md +179 -0
- package/skills/void/docs/guide/typed-fetch.md +113 -0
- package/skills/void/docs/guide/websockets.md +190 -0
- package/skills/void/docs/index.md +48 -0
- package/skills/void/docs/integrations/agents.md +84 -0
- package/skills/void/docs/integrations/cloudflare.md +284 -0
- package/skills/void/docs/integrations/frameworks/analog.md +182 -0
- package/skills/void/docs/integrations/frameworks/astro.md +197 -0
- package/skills/void/docs/integrations/frameworks/nuxt.md +164 -0
- package/skills/void/docs/integrations/frameworks/overview.md +136 -0
- package/skills/void/docs/integrations/frameworks/react-router.md +137 -0
- package/skills/void/docs/integrations/frameworks/sveltekit.md +191 -0
- package/skills/void/docs/integrations/frameworks/tanstack-start.md +140 -0
- package/skills/void/docs/integrations/hono.md +97 -0
- package/skills/void/docs/integrations/nodejs-bun-deno.md +210 -0
- package/skills/void/docs/node_modules/@iconify/vue/README.md +408 -0
- package/skills/void/docs/node_modules/@iconify/vue/offline/readme.md +5 -0
- package/skills/void/docs/node_modules/@voidzero-dev/vitepress-theme/README.md +103 -0
- package/skills/void/docs/node_modules/oxc-minify/README.md +78 -0
- package/skills/void/docs/node_modules/reka-ui/README.md +80 -0
- package/skills/void/docs/node_modules/vitepress/README.md +28 -0
- package/skills/void/docs/node_modules/vitepress/template/api-examples.md +49 -0
- package/skills/void/docs/node_modules/vitepress/template/index.md +28 -0
- package/skills/void/docs/node_modules/vitepress/template/markdown-examples.md +85 -0
- package/skills/void/docs/node_modules/vitepress-plugin-group-icons/README.md +101 -0
- package/skills/void/docs/node_modules/void/AGENTS.md +204 -0
- package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +15 -0
- package/skills/void/docs/node_modules/void/README.md +89 -0
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/CHANGELOG.md +591 -0
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/README.md +375 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/sandbox/README.md +174 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/vite-plugin/README.md +37 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/skills/void/docs/node_modules/void/node_modules/@electric-sql/pglite/README.md +189 -0
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/CHANGELOG.md +143 -0
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/README.md +1272 -0
- package/skills/void/docs/node_modules/void/node_modules/@napi-rs/keyring/README.md +19 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/better-sqlite3/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/node/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/pg/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/README.md +22 -0
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/vendor/vscode-jsonrpc/README.md +69 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/README.md +152 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/engine-javascript/README.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/transformers/README.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@types/node/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/CHANGELOG.md +24 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/README.md +565 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-exit/README.md +124 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-anchor/README.md +600 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-attrs/README.md +386 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-container/README.md +95 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-emoji/README.md +101 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathe/README.md +73 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/shiki/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tinyglobby/README.md +25 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tsdown/README.md +55 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite/LICENSE.md +2230 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite/README.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vue/README.md +58 -0
- package/skills/void/docs/node_modules/void/node_modules/arktype/README.md +165 -0
- package/skills/void/docs/node_modules/void/node_modules/better-auth/LICENSE.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/better-auth/README.md +32 -0
- package/skills/void/docs/node_modules/void/node_modules/better-sqlite3/README.md +99 -0
- package/skills/void/docs/node_modules/void/node_modules/blake3-jit/README.md +108 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-arktype/README.md +51 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-kit/README.md +79 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-orm/README.md +44 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-valibot/README.md +51 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-zod/README.md +65 -0
- package/skills/void/docs/node_modules/void/node_modules/es-module-lexer/README.md +390 -0
- package/skills/void/docs/node_modules/void/node_modules/estree-walker/README.md +48 -0
- package/skills/void/docs/node_modules/void/node_modules/hono/README.md +85 -0
- package/skills/void/docs/node_modules/void/node_modules/ignore/README.md +452 -0
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/{LICENSE → skills/void/docs/node_modules/void/node_modules/jsonc-parser/LICENSE.md} +21 -21
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/README.md +364 -0
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/skills/void/docs/node_modules/void/node_modules/magic-string/README.md +325 -0
- package/skills/void/docs/node_modules/void/node_modules/ofetch/README.md +398 -0
- package/skills/void/docs/node_modules/void/node_modules/pathe/README.md +73 -0
- package/skills/void/docs/node_modules/void/node_modules/pg/README.md +95 -0
- package/skills/void/docs/node_modules/void/node_modules/pglite-server/LICENSE.md +21 -0
- package/skills/void/docs/node_modules/void/node_modules/pglite-server/README.md +135 -0
- package/skills/void/docs/node_modules/void/node_modules/picocolors/README.md +21 -0
- package/skills/void/docs/node_modules/void/node_modules/tinyglobby/README.md +25 -0
- package/skills/void/docs/node_modules/void/node_modules/tsdown/README.md +55 -0
- package/skills/void/docs/node_modules/void/node_modules/valibot/LICENSE.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/valibot/README.md +94 -0
- package/skills/void/docs/node_modules/void/node_modules/vite/LICENSE.md +2230 -0
- package/skills/void/docs/node_modules/void/node_modules/vite/README.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/wrangler/README.md +63 -0
- package/skills/void/docs/node_modules/void/node_modules/zod/README.md +191 -0
- package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +175 -0
- package/skills/void/docs/node_modules/void/skills/void/SKILL.md +75 -0
- package/skills/void/docs/node_modules/void/skills/void/command/void.md +7 -0
- package/skills/void/docs/reference/api.md +917 -0
- package/skills/void/docs/reference/cli.md +561 -0
- package/skills/void/docs/reference/config.md +408 -0
- package/skills/void/docs/reference/resource-inference.md +149 -0
- package/skills/void/docs/reference/structure.md +176 -0
- package/.npmignore +0 -29
- package/.travis.yml +0 -9
- package/favicon.ico +0 -0
- package/index.js +0 -14
- package/lib/Job.js +0 -150
- package/lib/Void.js +0 -99
- package/lib/scan.js +0 -19
- package/test/credentials.js +0 -20
- package/test/job.js +0 -64
- package/test/static/dir1/test6.html +0 -0
- package/test/static/dir2/test7.html +0 -0
- package/test/static/dir2/test8.html +0 -0
- package/test/static/dir2/test9.html +0 -0
- package/test/static/test1.html +0 -0
- package/test/static/test2.html +0 -0
- package/test/static/test3.html +0 -0
- package/test/void.js +0 -31
- /package/{test/static/dir1/test4.html → skills/void/docs/integrations/auth-providers.md} +0 -0
- /package/{test/static/dir1/test5.html → skills/void/docs/integrations/payment-processors.md} +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
---
|
|
2
|
+
outline: deep
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Actions & Forms
|
|
6
|
+
|
|
7
|
+
Actions handle mutations (POST, PUT, PATCH, DELETE) in companion `.server.ts` files. They use the same `defineHandler` API as [loaders](./loaders) and [server routes](../server-routing.md), with the same typed `c.env` bindings, `withValidator()` support, and Hono context methods.
|
|
8
|
+
|
|
9
|
+
## Defining an Action
|
|
10
|
+
|
|
11
|
+
Export `action` from a `.server.ts` file. Use `withValidator()` with a [schema-derived validator](../database.md#schema-derived-validators) to validate the request body:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// pages/users/index.server.ts
|
|
15
|
+
import { defineHandler } from 'void';
|
|
16
|
+
import { db } from 'void/db';
|
|
17
|
+
import { users, insertUserSchema } from '@schema';
|
|
18
|
+
|
|
19
|
+
export const action = defineHandler.withValidator({
|
|
20
|
+
body: insertUserSchema,
|
|
21
|
+
})(async (c, { body }) => {
|
|
22
|
+
await db.insert(users).values(body);
|
|
23
|
+
// No return → re-runs loader, page re-renders with fresh data
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Action returns | Behavior |
|
|
28
|
+
| --------------------- | ----------------------------------------------------- |
|
|
29
|
+
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
|
|
30
|
+
| `c.redirect('/path')` | Navigates to another page. |
|
|
31
|
+
|
|
32
|
+
## Named Actions
|
|
33
|
+
|
|
34
|
+
When a page needs multiple mutations, such as updating and deleting a user on the same page, export `actions` (plural) instead of `action`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// pages/users/edit.server.ts
|
|
38
|
+
import { defineHandler } from 'void';
|
|
39
|
+
import { db, eq } from 'void/db';
|
|
40
|
+
import { users } from '@schema';
|
|
41
|
+
|
|
42
|
+
export const actions = {
|
|
43
|
+
update: defineHandler.withValidator({
|
|
44
|
+
body: updateUserSchema,
|
|
45
|
+
})(async (c, { body }) => {
|
|
46
|
+
await db
|
|
47
|
+
.update(users)
|
|
48
|
+
.set(body)
|
|
49
|
+
.where(eq(users.id, c.req.param('id')));
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
delete: defineHandler(async (c) => {
|
|
53
|
+
const { id } = await c.req.json<{ id: string }>();
|
|
54
|
+
await db.delete(users).where(eq(users.id, id));
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Named actions are dispatched via a `?actionName` suffix on the URL (e.g. `/users/edit?update`). The client primitives `useForm` and `action()` handle this automatically.
|
|
60
|
+
|
|
61
|
+
`export const action` (singular) still works for pages that only need one mutation. Named actions are opt-in. Exporting both `action` and `actions` from the same file is an error.
|
|
62
|
+
|
|
63
|
+
You can also define an `actions.default` key for the action that runs when no name is specified (i.e. a bare POST to the page URL):
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const actions = {
|
|
67
|
+
default: defineHandler(async (c) => {
|
|
68
|
+
// runs on POST /users/edit (no ?suffix)
|
|
69
|
+
}),
|
|
70
|
+
delete: defineHandler(async (c) => {
|
|
71
|
+
// runs on POST /users/edit?delete
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `useForm`
|
|
77
|
+
|
|
78
|
+
`useForm` handles form submissions, loading state, and validation errors. It is **fully typed end to end**. The URL autocompletes to pages that have an action, `form.data` matches the action validator schema, and `form.errors` keys are constrained to the body field names.
|
|
79
|
+
|
|
80
|
+
First, define the action in your server handler:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// pages/users/create.server.ts
|
|
84
|
+
import { defineHandler } from 'void';
|
|
85
|
+
import { db } from 'void/db';
|
|
86
|
+
import { users } from '@schema';
|
|
87
|
+
import * as v from 'valibot';
|
|
88
|
+
|
|
89
|
+
const createUserSchema = v.object({
|
|
90
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
91
|
+
email: v.pipe(v.string(), v.email()),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const action = defineHandler.withValidator({
|
|
95
|
+
body: createUserSchema,
|
|
96
|
+
})(async (c, { body }) => {
|
|
97
|
+
await db.insert(users).values(body);
|
|
98
|
+
return c.redirect('/users');
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then, use `useForm` in the page component to submit to this action:
|
|
103
|
+
|
|
104
|
+
::: code-group
|
|
105
|
+
|
|
106
|
+
```tsx [React]
|
|
107
|
+
// pages/users/create.tsx
|
|
108
|
+
import { useForm } from '@void/react';
|
|
109
|
+
import { useFormStatus } from 'react-dom';
|
|
110
|
+
|
|
111
|
+
function SubmitButton() {
|
|
112
|
+
const { pending } = useFormStatus();
|
|
113
|
+
return <button disabled={pending}>Create</button>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default function CreateUser() {
|
|
117
|
+
// "/users/create" autocompletes; { name, email } is typed from the action's validator
|
|
118
|
+
const form = useForm('/users/create', { name: '', email: '' });
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<form action={form.post}>
|
|
122
|
+
<input
|
|
123
|
+
name="name"
|
|
124
|
+
value={form.data.name}
|
|
125
|
+
onChange={(e) => form.setData('name', e.target.value)}
|
|
126
|
+
/>
|
|
127
|
+
{form.errors.name && <span>{form.errors.name}</span>}
|
|
128
|
+
|
|
129
|
+
<input
|
|
130
|
+
name="email"
|
|
131
|
+
value={form.data.email}
|
|
132
|
+
onChange={(e) => form.setData('email', e.target.value)}
|
|
133
|
+
/>
|
|
134
|
+
{form.errors.email && <span>{form.errors.email}</span>}
|
|
135
|
+
|
|
136
|
+
{form.error && <p>{form.error.message}</p>}
|
|
137
|
+
|
|
138
|
+
<SubmitButton />
|
|
139
|
+
</form>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```vue [Vue]
|
|
145
|
+
<!-- pages/users/create.vue -->
|
|
146
|
+
<script setup lang="ts">
|
|
147
|
+
import { useForm } from '@void/vue';
|
|
148
|
+
|
|
149
|
+
// "/users/create" autocompletes; { name, email } is typed from the action's validator
|
|
150
|
+
const form = useForm('/users/create', { name: '', email: '' });
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<form @submit.prevent="form.post()">
|
|
155
|
+
<input v-model="form.data.name" />
|
|
156
|
+
<span v-if="form.errors.name">{{ form.errors.name }}</span>
|
|
157
|
+
|
|
158
|
+
<input v-model="form.data.email" />
|
|
159
|
+
<span v-if="form.errors.email">{{ form.errors.email }}</span>
|
|
160
|
+
|
|
161
|
+
<button :disabled="form.pending">Create</button>
|
|
162
|
+
</form>
|
|
163
|
+
</template>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```svelte [Svelte]
|
|
167
|
+
<!-- pages/users/create.svelte -->
|
|
168
|
+
<script>
|
|
169
|
+
import { useForm } from "@void/svelte";
|
|
170
|
+
|
|
171
|
+
// "/users/create" autocompletes; { name, email } is typed from the action's validator
|
|
172
|
+
const form = useForm("/users/create", { name: "", email: "" });
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<form onsubmit={(e) => { e.preventDefault(); return form.post(); }}>
|
|
176
|
+
<input bind:value={form.data.name} />
|
|
177
|
+
{#if form.errors.name}<span>{form.errors.name}</span>{/if}
|
|
178
|
+
|
|
179
|
+
<input bind:value={form.data.email} />
|
|
180
|
+
{#if form.errors.email}<span>{form.errors.email}</span>{/if}
|
|
181
|
+
|
|
182
|
+
<button disabled={form.pending}>Create</button>
|
|
183
|
+
</form>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```tsx [Solid]
|
|
187
|
+
// pages/users/create.tsx
|
|
188
|
+
import { useForm } from '@void/solid';
|
|
189
|
+
|
|
190
|
+
export default function CreateUser() {
|
|
191
|
+
// "/users/create" autocompletes; { name, email } is typed from the action's validator
|
|
192
|
+
const form = useForm('/users/create', { name: '', email: '' });
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<form
|
|
196
|
+
onSubmit={(e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
return form.post();
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<input value={form.data.name} onInput={(e) => form.setData('name', e.target.value)} />
|
|
202
|
+
{form.errors.name && <span>{form.errors.name}</span>}
|
|
203
|
+
|
|
204
|
+
<input value={form.data.email} onInput={(e) => form.setData('email', e.target.value)} />
|
|
205
|
+
{form.errors.email && <span>{form.errors.email}</span>}
|
|
206
|
+
|
|
207
|
+
<button disabled={form.pending}>Create</button>
|
|
208
|
+
</form>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
:::
|
|
214
|
+
|
|
215
|
+
The types are inferred from your action's `withValidator()` schema in the companion `.server.ts` file. If no validator is defined, the body type falls back to `Record<string, unknown>`, and you still get URL autocomplete.
|
|
216
|
+
|
|
217
|
+
### `useForm` API
|
|
218
|
+
|
|
219
|
+
`useForm` returns a reactive object with:
|
|
220
|
+
|
|
221
|
+
| Property / Method | Purpose |
|
|
222
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
223
|
+
| Property / Method | Purpose |
|
|
224
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
225
|
+
| `data` / `setData` | Current form values, typed to match the action's body schema. |
|
|
226
|
+
| `errors` | Field-level validation errors, keys typed to body field names. |
|
|
227
|
+
| `error` | Non-validation call-site action error, or `null`. |
|
|
228
|
+
| `post`, `put`, `patch`, `delete` | Submit the form with that method. In React these are native form action callbacks. |
|
|
229
|
+
| `pending` | `true` while the submission is in flight. |
|
|
230
|
+
| `hasChanges` | `true` if form data differs from initial values. |
|
|
231
|
+
| `wasSuccessful` | `true` after a successful submission. Stays `true` until the next submission. |
|
|
232
|
+
| `recentlySuccessful` | `true` for 2 seconds after a successful submission. Useful for flash messages. |
|
|
233
|
+
| `reset(...fields)` | Reset form data to initial values. Field names autocomplete. |
|
|
234
|
+
| `clearErrors(...fields)` | Clear validation errors. Field names autocomplete. |
|
|
235
|
+
| `clearError()` | Clear the non-validation call-site error. |
|
|
236
|
+
|
|
237
|
+
In React, prefer the native Action form:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
<form action={form.post}>{/* controlled inputs update form.data */}</form>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Use `form.put`, `form.patch`, or `form.delete` as the form action for alternate HTTP methods. Use the `action()` helper when you need an awaitable imperative mutation.
|
|
244
|
+
|
|
245
|
+
In Vue, Svelte, and Solid, the same helpers return `Promise<void>` so
|
|
246
|
+
boundary-class failures can propagate through framework async error handling or
|
|
247
|
+
explicit `catch` handlers.
|
|
248
|
+
|
|
249
|
+
For dynamic routes, pass `params` in the options:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// pages/users/[id].server.ts has an action
|
|
253
|
+
const form = useForm('/users/:id', { name: '' }, { params: { id: '42' } });
|
|
254
|
+
<form action={form.put}>{/* submits to /users/42 */}</form>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Named Actions with `useForm`
|
|
258
|
+
|
|
259
|
+
When a page exports named actions, append `?actionName` to the URL:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const form = useForm('/users/:id?update', { name: '' }, { params: { id } });
|
|
263
|
+
<form action={form.put}>{/* submits to /users/42?update */}</form>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The URL, body fields, and error keys are typed per action. Each named action gets its own validator schema. See [Type Safety: Action -> useForm](../type-safety#action-useform) for the full typing story.
|
|
267
|
+
|
|
268
|
+
## `action()` Helper
|
|
269
|
+
|
|
270
|
+
For one-shot mutations that do not need form state such as dirty tracking, reset, or field-level errors, use `action()` instead of `useForm`. It sends a request to a page action and triggers an Inertia page update, just like `useForm`, but without the reactive form object:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { useForm, action } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid"
|
|
274
|
+
|
|
275
|
+
// Form with state + Inertia page update
|
|
276
|
+
const form = useForm('/?create-user', { name: '', email: '' });
|
|
277
|
+
|
|
278
|
+
// Programmatic call + Inertia page update (no form state)
|
|
279
|
+
const result = await action('/?delete-user', {
|
|
280
|
+
data: { id: 42 },
|
|
281
|
+
method: 'DELETE',
|
|
282
|
+
});
|
|
283
|
+
if (!result.ok) {
|
|
284
|
+
showToast(result.error.message);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`action()` is useful in event handlers such as button clicks, confirmation dialogs, or any place where you want to call a server action without managing form state. It uses `POST` by default and accepts `{ data, method, params }`, where `method` can be `'PUT'`, `'PATCH'`, or `'DELETE'` for alternate HTTP methods. It returns `{ ok: true, pageData }` for successful actions and `{ ok: false, error }` for call-site errors such as validation, conflicts, or missing resources.
|
|
289
|
+
|
|
290
|
+
## Validation Errors
|
|
291
|
+
|
|
292
|
+
When an action throws a `ValidationError`, or validation fails through `withValidator`, the errors are automatically available on `form.errors`. You do not need to wire that up manually.
|
|
293
|
+
|
|
294
|
+
Void separates action failures into call-site errors and boundary errors. Call-site errors are expected local failures such as `400`, `404`, `409`, `422`, and `429`; `useForm` stores them in `form.errors` or `form.error`, and `action()` returns `{ ok: false, error }`. Boundary errors such as `401`, `403`, `500`, `502`, and unknown network/protocol failures are thrown so React error boundaries, or your framework's error handling, can handle them at a higher level.
|
|
295
|
+
|
|
296
|
+
Actions can throw `ValidationError` for custom validation logic:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { defineHandler, ValidationError } from 'void';
|
|
300
|
+
|
|
301
|
+
export const action = defineHandler(async (c) => {
|
|
302
|
+
const body = await c.req.json();
|
|
303
|
+
if (await emailExists(body.email)) {
|
|
304
|
+
throw new ValidationError({ email: 'Email already taken' });
|
|
305
|
+
}
|
|
306
|
+
// ...
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Or use `withValidator()` for automatic schema-based validation. Errors are returned in the same format.
|
|
311
|
+
|
|
312
|
+
## File Uploads
|
|
313
|
+
|
|
314
|
+
`useForm` automatically detects `File`, `Blob`, and `FileList` values in form data and sends the request as `multipart/form-data` instead of JSON. No extra configuration is needed. Set a file on the form data and submit.
|
|
315
|
+
|
|
316
|
+
::: code-group
|
|
317
|
+
|
|
318
|
+
```tsx [React]
|
|
319
|
+
import { useForm } from '@void/react';
|
|
320
|
+
|
|
321
|
+
export default function Upload() {
|
|
322
|
+
const form = useForm('/photos', { title: '', photo: null as File | null });
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<form action={form.post}>
|
|
326
|
+
<input value={form.data.title} onChange={(e) => form.setData('title', e.target.value)} />
|
|
327
|
+
<input type="file" onChange={(e) => form.setData('photo', e.target.files?.[0] ?? null)} />
|
|
328
|
+
<button disabled={form.pending}>Upload</button>
|
|
329
|
+
</form>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
```vue [Vue]
|
|
335
|
+
<script setup lang="ts">
|
|
336
|
+
import { ref } from 'vue';
|
|
337
|
+
import { useForm } from '@void/vue';
|
|
338
|
+
|
|
339
|
+
const form = useForm('/photos', { title: '', photo: null as File | null });
|
|
340
|
+
const fileInput = ref<HTMLInputElement>();
|
|
341
|
+
|
|
342
|
+
function onFileChange() {
|
|
343
|
+
form.data.photo = fileInput.value?.files?.[0] ?? null;
|
|
344
|
+
}
|
|
345
|
+
</script>
|
|
346
|
+
|
|
347
|
+
<template>
|
|
348
|
+
<form @submit.prevent="form.post()">
|
|
349
|
+
<input v-model="form.data.title" />
|
|
350
|
+
<input type="file" ref="fileInput" @change="onFileChange" />
|
|
351
|
+
<button :disabled="form.pending">Upload</button>
|
|
352
|
+
</form>
|
|
353
|
+
</template>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```svelte [Svelte]
|
|
357
|
+
<script>
|
|
358
|
+
import { useForm } from "@void/svelte";
|
|
359
|
+
|
|
360
|
+
const form = useForm("/photos", { title: "", photo: null });
|
|
361
|
+
</script>
|
|
362
|
+
|
|
363
|
+
<form onsubmit={(e) => { e.preventDefault(); return form.post(); }}>
|
|
364
|
+
<input bind:value={form.data.title} />
|
|
365
|
+
<input type="file" onchange={(e) => { form.data.photo = e.target.files?.[0] ?? null; }} />
|
|
366
|
+
<button disabled={form.pending}>Upload</button>
|
|
367
|
+
</form>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```tsx [Solid]
|
|
371
|
+
import { useForm } from '@void/solid';
|
|
372
|
+
|
|
373
|
+
export default function Upload() {
|
|
374
|
+
const form = useForm('/photos', { title: '', photo: null as File | null });
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<form
|
|
378
|
+
onSubmit={(e) => {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
return form.post();
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<input value={form.data.title} onInput={(e) => form.setData('title', e.target.value)} />
|
|
384
|
+
<input type="file" onChange={(e) => form.setData('photo', e.target.files?.[0] ?? null)} />
|
|
385
|
+
<button disabled={form.pending}>Upload</button>
|
|
386
|
+
</form>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
:::
|
|
392
|
+
|
|
393
|
+
On the server, use `c.req.parseBody()` to access the uploaded file:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// pages/photos.server.ts
|
|
397
|
+
import { defineHandler } from 'void';
|
|
398
|
+
import { storage } from 'void/storage';
|
|
399
|
+
|
|
400
|
+
export const action = defineHandler(async (c) => {
|
|
401
|
+
const body = await c.req.parseBody();
|
|
402
|
+
const file = body['photo'] as File;
|
|
403
|
+
if (file && file.size > 0) {
|
|
404
|
+
await storage.put(file.name, file.stream(), {
|
|
405
|
+
httpMetadata: { contentType: file.type },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Choosing a Primitive
|
|
412
|
+
|
|
413
|
+
| Primitive | Inertia page update | Form state | Framework-specific |
|
|
414
|
+
| ---------- | ------------------- | -------------------------- | ------------------ |
|
|
415
|
+
| `useForm` | Yes | Yes (errors, dirty, reset) | Yes |
|
|
416
|
+
| `action()` | Yes | No | Yes |
|
|
417
|
+
| `fetch()` | No (raw response) | No | No |
|
|
418
|
+
|
|
419
|
+
Use `useForm` when you have a form with inputs. Use `action()` for programmatic mutations (delete buttons, toggles), optionally passing `{ method }` for non-POST actions. Use `fetch()` when you need the raw response and don't want Inertia page updates.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
outline: deep
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Head Management
|
|
6
|
+
|
|
7
|
+
Pages mode manages `<head>` tags (title, meta, links, scripts, HTML/body attributes) across three layers with clear precedence: **page > middleware > config**.
|
|
8
|
+
|
|
9
|
+
## Page `head()` Export
|
|
10
|
+
|
|
11
|
+
Export a `head()` function from your `.server.ts` file. It receives the Hono context and the resolved loader props, and returns a `HeadDescriptor`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// pages/posts/[slug].server.ts
|
|
15
|
+
import { defineHandler, defineHead } from 'void';
|
|
16
|
+
|
|
17
|
+
export interface Props {
|
|
18
|
+
post: { title: string; excerpt: string; slug: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const loader = defineHandler<Props>(async (c) => {
|
|
22
|
+
const post = await getPost(c.req.param('slug'));
|
|
23
|
+
return { post };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const head = defineHead<Props>((c, props) => {
|
|
27
|
+
// [!code highlight]
|
|
28
|
+
return {
|
|
29
|
+
title: props.post.title,
|
|
30
|
+
meta: [
|
|
31
|
+
{ name: 'description', content: props.post.excerpt },
|
|
32
|
+
{ property: 'og:title', content: props.post.title },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`head()` runs server-side only, after the loader resolves.
|
|
39
|
+
|
|
40
|
+
## Config Defaults
|
|
41
|
+
|
|
42
|
+
Set site-wide head defaults in `void.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"head": {
|
|
47
|
+
"titleTemplate": "%s | My Site",
|
|
48
|
+
"htmlAttrs": { "lang": "en" },
|
|
49
|
+
"meta": [
|
|
50
|
+
{ "charset": "utf-8" },
|
|
51
|
+
{ "name": "viewport", "content": "width=device-width, initial-scale=1" }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`titleTemplate` wraps the page title. `%s` is replaced with whatever `head()` returns as `title`. A page returning `{ title: "About" }` with the template above produces `<title>About | My Site</title>`.
|
|
58
|
+
|
|
59
|
+
## Middleware Defaults
|
|
60
|
+
|
|
61
|
+
Middleware can inject head defaults that apply to every page. This is useful for things like a theme script that must run before first paint:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// middleware/01.head.ts
|
|
65
|
+
import { defineMiddleware } from 'void';
|
|
66
|
+
|
|
67
|
+
export default defineMiddleware(async (c, next) => {
|
|
68
|
+
c.set('headDefaults', {
|
|
69
|
+
script: [
|
|
70
|
+
{
|
|
71
|
+
innerHTML: `const t = localStorage.getItem("theme");
|
|
72
|
+
if (t) document.documentElement.dataset.theme = t;`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
await next();
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Merge Precedence
|
|
81
|
+
|
|
82
|
+
When multiple layers provide head data, they merge with this precedence:
|
|
83
|
+
|
|
84
|
+
| Field | Behavior |
|
|
85
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `title` | Page wins. `titleTemplate` from config wraps the winning title. |
|
|
87
|
+
| `meta` | Deduplicates by `name` or `property`. Page entries override matching keys, and non-conflicting entries are appended. |
|
|
88
|
+
| `link` | Concatenated: config first, then middleware, then page. |
|
|
89
|
+
| `script` | Concatenated: config first, then middleware, then page. |
|
|
90
|
+
| `htmlAttrs` | Shallow merge, page wins conflicts. |
|
|
91
|
+
| `bodyAttrs` | Shallow merge, page wins conflicts. |
|
|
92
|
+
|
|
93
|
+
## HeadDescriptor Shape
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
interface HeadDescriptor {
|
|
97
|
+
title?: string;
|
|
98
|
+
meta?: Array<{
|
|
99
|
+
name?: string;
|
|
100
|
+
property?: string;
|
|
101
|
+
content?: string;
|
|
102
|
+
charset?: string;
|
|
103
|
+
}>;
|
|
104
|
+
link?: Array<{ rel: string; href: string; [key: string]: string | undefined }>;
|
|
105
|
+
script?: Array<{ src?: string; innerHTML?: string; [key: string]: string | undefined }>;
|
|
106
|
+
htmlAttrs?: Record<string, string>;
|
|
107
|
+
bodyAttrs?: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Markdown Auto-Head
|
|
112
|
+
|
|
113
|
+
Markdown pages (`.md` files) automatically generate head tags from frontmatter. You do not need a `.server.ts` file:
|
|
114
|
+
|
|
115
|
+
```md
|
|
116
|
+
---
|
|
117
|
+
title: Getting Started
|
|
118
|
+
description: Learn how to use Void
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
Your content here...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This produces `<title>Getting Started</title>` and `<meta name="description" content="Learn how to use Void">`, with `titleTemplate` applied if configured.
|
|
125
|
+
|
|
126
|
+
## Client-Side Updates
|
|
127
|
+
|
|
128
|
+
On SPA navigation, head tags update automatically. The framework tracks managed tags with a `data-void-head` attribute and preserves matching tags across navigations, so unchanged stylesheet and preload links are not torn down and re-added. Tags that are no longer present are removed, and new tags are inserted.
|
|
129
|
+
|
|
130
|
+
`script`, `htmlAttrs`, and `bodyAttrs` are SSR-only. They are not re-applied on client-side navigation.
|