toiljs 0.0.117 → 0.0.118

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.
@@ -79,7 +79,7 @@ function serverEntryFiles(root) {
79
79
  return [...result].sort();
80
80
  }
81
81
  function authEntryFiles(root, appHasUser) {
82
- const names = appHasUser ? ['AuthController.ts'] : ['AuthUser.ts', 'AuthController.ts'];
82
+ const names = [...(appHasUser ? ['AuthController.ts'] : ['AuthUser.ts', 'AuthController.ts']), 'SessionRevocations.ts', 'SessionEpochs.ts', 'FactorSettings.ts'];
83
83
  const primary = names.map((n) => path.posix.join('node_modules/toiljs/server/auth', n));
84
84
  if (primary.every((rel) => fs.existsSync(path.join(root, rel))))
85
85
  return primary;
@@ -1,6 +1,6 @@
1
1
  export const TOIL_DOCS = {
2
2
  "README.md": "# toiljs\n\ntoiljs is a full-stack web framework. You write your **frontend in React** and your **backend in\nTypeScript**, and toiljs turns the backend into a tiny, fast **WebAssembly** program that runs at the edge\n(on servers close to your users, all over the world). One language, one project, one deploy.\n\nIf you have used Next.js, this will feel familiar: file-based routes, server code next to client code, a dev\nserver with hot reload. The difference is what happens underneath. Your server code is compiled by\n**toilscript** (a TypeScript-to-WebAssembly compiler) into a sandboxed `.wasm` module, and it runs on the\n**Dacely edge** with a built-in worldwide database (**ToilDB**), streaming, background jobs, and auth all\nincluded.\n\n## The mental model\n\n```mermaid\nflowchart LR\n A[\"Your project<br/>(TypeScript + React)\"] -->|toiljs build| B[\"client bundle<br/>(React, runs in the browser)\"]\n A -->|toilscript compile| C[\"server.wasm<br/>(your backend, sandboxed)\"]\n B --> U[\"User's browser\"]\n C --> E[\"Dacely edge<br/>(worldwide)\"]\n E --> D[(\"ToilDB<br/>global database\")]\n U <-->|HTTP / WebTransport| E\n```\n\n- **client/** is your React app (pages, components, styles). It runs in the browser.\n- **server/** is your backend (routes, database, auth). It compiles to WebAssembly and runs on the edge.\n- **shared/** is the typed bridge: toiljs generates a client here so the browser calls your server with full\n type safety.\n\n## Hello, toiljs\n\n```ts\n// server/routes/Hello.ts (a backend HTTP route)\nimport { Response } from 'toiljs/server/runtime';\n\n@rest('hello')\nclass Hello {\n @get('/')\n public hi(): Response {\n return Response.text('Hello from the edge!\\n');\n }\n}\n```\n\n```tsx\n// client/routes/index.tsx (a frontend page)\nexport default function Home() {\n return <main><h1>Welcome</h1></main>;\n}\n```\n\nRun `toiljs dev`, open the browser, and both are live with hot reload. That is the whole loop.\n\n## Learn toiljs\n\n**Understand toil first**\n- [Understanding toil](./introduction/README.md): what toil is and the one big idea, then\n [why toil and who it is for](./introduction/why-toil.md),\n [the modern stack you get](./introduction/modern-stack.md),\n [how it works](./introduction/how-it-works.md),\n [what makes it hyper-scalable](./introduction/hyperscale.md),\n [how it is distributed](./introduction/distributed.md),\n [toil versus other frameworks](./introduction/vs-other-frameworks.md), and\n [why it is built this way](./introduction/design-principles.md).\n\n**Start here**\n- [Getting started](./getting-started/README.md): install, create a project, project structure, your first\n app, and migrating an existing React app.\n- [The CLI](./cli/README.md): `dev`, `build`, `create`, `doctor`, and every flag.\n- [Deploy](./getting-started/deploy.md): build for production, self-host it, and how the managed edge fits in.\n\n**Build the frontend**\n- [Frontend overview](./frontend/README.md), [Routing](./frontend/routing.md),\n [Navigation](./frontend/navigation.md), [Components](./frontend/components.md),\n [Rendering and SSR](./frontend/rendering.md), [Styling](./frontend/styling.md),\n [Images](./frontend/images.md), [Metadata and SEO](./frontend/metadata.md),\n [Fetching data](./frontend/data-fetching.md), [Scripts](./frontend/scripts.md),\n [Search](./frontend/search.md), [The Toil global (reference)](./frontend/toil-global.md).\n\n**Build the backend**\n- [Backend overview](./backend/README.md), [HTTP routes (`@rest`)](./backend/rest.md),\n [Typed RPC (`@service`/`@remote`)](./backend/rpc.md), [Data types (`@data`)](./backend/data.md).\n\n**The database (ToilDB)**\n- [Database overview and choosing a family](./database/README.md), [Setup (`@database`)](./database/setup.md),\n [Documents](./database/documents.md), [Unique](./database/unique.md), [Counters](./database/counters.md),\n [Events](./database/events.md), [Views and `@derive`](./database/views.md),\n [Membership](./database/membership.md), [Capacity](./database/capacity.md).\n\n**Auth**: [the full auth guide](./auth/README.md) covers post-quantum login, sessions, and `ToilUserId`;\n[customizing the auth emails](./auth/emails.md) shows how to brand the verification, reset, and 2FA mail.\n\n**Realtime and background**\n- [Streams](./realtime/README.md) and [channels](./realtime/channels.md), [Daemons and scheduled\n jobs](./background/daemons.md), [Derived views (`@derive`)](./background/derive.md).\n\n**Platform services**\n- [Caching](./services/caching.md), [Rate limiting](./services/ratelimit.md),\n [Environment and secrets](./services/environment.md), [Email and 2FA](./services/email.md),\n [Analytics](./services/analytics.md), [Crypto](./services/crypto.md), [Cookies](./services/cookies.md),\n [Time](./services/time.md).\n\n**Concepts and reference**\n- [Writing toiljs correctly (AI + human quick rules)](./concepts/ai-guide.md),\n [Compute tiers (L1 to L4)](./concepts/tiers.md), [Types (u64, u256, and friends)](./concepts/types.md),\n [Every decorator](./concepts/decorators.md), [Configuration](./concepts/config.md),\n [Security and SRI](./concepts/security.md).\n",
3
- "auth/configuration.md": "# Configuring auth for production\n\nBuilt-in auth runs locally with **zero configuration**, it falls back to published, insecure DEV secrets\nso `toiljs dev` Just Works. **A deployment MUST replace all three secrets and pin its KEM key.** This page\nis the checklist.\n\n## The secrets\n\nAuth reads these from the tenant environment store (locally, `.env.secrets`; on the edge, the per-host\nsecure env). They resolve **lazily** the first time auth runs, so no startup wiring is needed.\n\n| Key | What it is | Dev fallback |\n| --- | --- | --- |\n| `AUTH_SESSION_SECRET` | HMAC-SHA256 key that signs the session cookie. Must be identical on every edge instance (a cookie minted anywhere must verify everywhere). | a public constant, **anyone can forge a session** |\n| `AUTH_OPRF_SEED` | Master seed for the per-user OPRF salt key. Rotating it invalidates every password (users must re-register). | a hashed public constant |\n| `AUTH_KEM_SK` | The server's ML-KEM-768 **secret** key (hex). Its public half is what the client encapsulates to. | a pinned dev key pair |\n\n```bash\n# .env.secrets (gitignored; mode 0600 on the edge, NEVER under hosts/, NEVER in the .wasm)\nAUTH_SESSION_SECRET=…64 hex chars (32 bytes)…\nAUTH_OPRF_SEED=…64 hex chars (32 bytes)…\nAUTH_KEM_SK=…hex of an ML-KEM-768 secret key…\n```\n\n### Generating them\n\n`AUTH_SESSION_SECRET` and `AUTH_OPRF_SEED` are just 32 random bytes each:\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n```\n\n`AUTH_KEM_SK` is an ML-KEM-768 key pair. Generate it and keep BOTH halves, the secret goes in the env, the\npublic half is pinned in the client (next section):\n\n```ts\nimport { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem';\nconst { secretKey, publicKey } = ml_kem768.keygen();\nconsole.log('AUTH_KEM_SK =', Buffer.from(secretKey).toString('hex'));\nconsole.log('client serverKemPublicKey =', Buffer.from(publicKey).toString('hex'));\n```\n\n## Pin the client's KEM public key\n\nThe browser must know the server's genuine KEM public key to run the mutual-auth handshake, this is the\nanti-phishing anchor. The `toiljs/client` `Auth` helper ships with the **dev** key pinned, so a deployment\nMUST pass its own:\n\n```ts\nimport { Auth } from 'toiljs/client';\n\nconst SERVER_KEM_PUBLIC_KEY = /* the publicKey bytes from AUTH_KEM_SK */;\n\nawait Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\nawait Auth.register(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\n```\n\nShip the public key with your client bundle (it's public, safe to embed). If it doesn't match the server's\n`AUTH_KEM_SK`, login's `serverConfirm` check fails and the client aborts.\n\n## Optional: audience & domain\n\nBoth are optional and have sensible defaults; set them for stability across host aliases:\n\n| Key | Meaning | Default |\n| --- | --- | --- |\n| `TOIL_AUTH_AUDIENCE` | The service audience bound into the signed login message. | `\"toil\"` |\n| `TOIL_AUTH_DOMAIN` | The `domain` input of the stable `ToilUserId` (`sha256(pubkey ‖ username ‖ domain)`). | the request `Host` header, else `localhost` |\n\nSet `TOIL_AUTH_DOMAIN` explicitly if your site answers on multiple hostnames, otherwise the same user could\nget different `ToilUserId`s from different aliases. Once users exist, changing it changes everyone's id, so\npick it before launch.\n\n## Email confirmation\n\nTwo plain env vars (tenant-readable, like `PUBLIC_BASE_URL`) control the built-in email-verification flow. They are separate on purpose: sending a verify email at registration is one decision, blocking login until confirmed is another.\n\n| Key | What it does |\n| --- | --- |\n| `AUTH_EMAIL_CONFIRMATION` | Registration emails a one-time confirm link (`/confirm#token=…`) and stores the account **unconfirmed**. Login is **not** blocked: the user can sign in, and you nudge them to verify. Use this for \"verify at signup, but let people in\". |\n| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | The stricter form: does everything above AND **refuses login** until the email is confirmed (a distinguishable status the client surfaces as `EmailNotConfirmedError`, so the UI can prompt \"confirm your email / resend\"). Implies `AUTH_EMAIL_CONFIRMATION`. On the managed edge this is force-set from the per-domain Dacely setting. |\n\nWith neither set, accounts are auto-confirmed at registration and no confirm email is sent. The one-time link is delivered by email; in `toiljs dev` it is drawn in the console (no mailer needed). Both use the standard non-enumerating responses (a generic 200 that never reveals whether an address exists).\n\n## Argon2id strength (known limitation)\n\nThe built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it\nstays responsive in a browser tab. These are baked into the shipped controller today; **config-driven\ntuning is a planned follow-up.** For a high-value production deployment you should either wait for the\nconfig knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the\nprimary offline-attack resistance regardless, but raise these before protecting anything sensitive.\n\nThe client always derives against whatever params the server returns in `/login/start`, so when the config\nknob lands you can raise them server-side with **no client change**.\n\n## Deploy checklist\n\n- [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.\n- [ ] `AUTH_OPRF_SEED` set (32 random bytes).\n- [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via\n `serverKemPublicKey`.\n- [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).\n- [ ] (Recommended) Argon2id params reviewed for your threat model.\n\nThe CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.\n",
3
+ "auth/configuration.md": "# Configuring auth for production\n\nBuilt-in auth runs locally with **zero configuration**, it falls back to published, insecure DEV secrets\nso `toiljs dev` Just Works. **A deployment MUST replace all three secrets and pin its KEM key.** This page\nis the checklist.\n\n## The secrets\n\nAuth reads these from the tenant environment store (locally, `.env.secrets`; on the edge, the per-host\nsecure env). They resolve **lazily** the first time auth runs, so no startup wiring is needed.\n\n| Key | What it is | Dev fallback |\n| --- | --- | --- |\n| `AUTH_SESSION_SECRET` | HMAC-SHA256 key that signs the session cookie. Must be identical on every edge instance (a cookie minted anywhere must verify everywhere). | a public constant, **anyone can forge a session** |\n| `AUTH_OPRF_SEED` | Master seed for the per-user OPRF salt key. Rotating it invalidates every password (users must re-register). | a hashed public constant |\n| `AUTH_KEM_SK` | The server's ML-KEM-768 **secret** key (hex). Its public half is what the client encapsulates to. | a pinned dev key pair |\n\n```bash\n# .env.secrets (gitignored; mode 0600 on the edge, NEVER under hosts/, NEVER in the .wasm)\nAUTH_SESSION_SECRET=…64 hex chars (32 bytes)…\nAUTH_OPRF_SEED=…64 hex chars (32 bytes)…\nAUTH_KEM_SK=…hex of an ML-KEM-768 secret key…\n```\n\n### Generating them\n\n`AUTH_SESSION_SECRET` and `AUTH_OPRF_SEED` are just 32 random bytes each:\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n```\n\n`AUTH_KEM_SK` is an ML-KEM-768 key pair. Generate it and keep BOTH halves, the secret goes in the env, the\npublic half is pinned in the client (next section):\n\n```ts\nimport { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem';\nconst { secretKey, publicKey } = ml_kem768.keygen();\nconsole.log('AUTH_KEM_SK =', Buffer.from(secretKey).toString('hex'));\nconsole.log('client serverKemPublicKey =', Buffer.from(publicKey).toString('hex'));\n```\n\n## Pin the client's KEM public key\n\nThe browser must know the server's genuine KEM public key to run the mutual-auth handshake, this is the\nanti-phishing anchor. The `toiljs/client` `Auth` helper ships with the **dev** key pinned, so a deployment\nMUST pass its own:\n\n```ts\nimport { Auth } from 'toiljs/client';\n\nconst SERVER_KEM_PUBLIC_KEY = /* the publicKey bytes from AUTH_KEM_SK */;\n\nawait Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\nawait Auth.register(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });\n```\n\nShip the public key with your client bundle (it's public, safe to embed). If it doesn't match the server's\n`AUTH_KEM_SK`, login's `serverConfirm` check fails and the client aborts.\n\n## Optional: audience & domain\n\nBoth are optional and have sensible defaults; set them for stability across host aliases:\n\n| Key | Meaning | Default |\n| --- | --- | --- |\n| `TOIL_AUTH_AUDIENCE` | The service audience bound into the signed login message. | `\"toil\"` |\n| `TOIL_AUTH_DOMAIN` | The `domain` input of the stable `ToilUserId` (`sha256(pubkey ‖ username ‖ domain)`). | the request `Host` header, else `localhost` |\n\nSet `TOIL_AUTH_DOMAIN` explicitly if your site answers on multiple hostnames, otherwise the same user could\nget different `ToilUserId`s from different aliases. Once users exist, changing it changes everyone's id, so\npick it before launch.\n\n## Email confirmation\n\nTwo plain env vars (tenant-readable, like `PUBLIC_BASE_URL`) control the built-in email-verification flow. They are separate on purpose: sending a verify email at registration is one decision, blocking login until confirmed is another.\n\n| Key | What it does |\n| --- | --- |\n| `AUTH_EMAIL_CONFIRMATION` | Registration emails a one-time confirm link (`/confirm#token=…`) and stores the account **unconfirmed**. Login is **not** blocked: the user can sign in, and you nudge them to verify. Use this for \"verify at signup, but let people in\". |\n| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | The stricter form: does everything above AND **refuses login** until the email is confirmed (a distinguishable status the client surfaces as `EmailNotConfirmedError`, so the UI can prompt \"confirm your email / resend\"). Implies `AUTH_EMAIL_CONFIRMATION`. On the managed edge this is force-set from the per-domain Dacely setting. |\n\nWith neither set, accounts are auto-confirmed at registration and no confirm email is sent. The one-time link is delivered by email; in `toiljs dev` it is drawn in the console (no mailer needed). Both use the standard non-enumerating responses (a generic 200 that never reveals whether an address exists).\n\n## Argon2id strength (known limitation)\n\nThe built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it\nstays responsive in a browser tab. These are baked into the shipped controller today; **config-driven\ntuning is a planned follow-up.** For a high-value production deployment you should either wait for the\nconfig knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the\nprimary offline-attack resistance regardless, but raise these before protecting anything sensitive.\n\nThe client always derives against whatever params the server returns in `/login/start`, so when the config\nknob lands you can raise them server-side with **no client change**.\n\n## Deploy checklist\n\n- [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.\n- [ ] `AUTH_OPRF_SEED` set (32 random bytes).\n- [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via\n `serverKemPublicKey`.\n- [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).\n- [ ] (Recommended) Argon2id params reviewed for your threat model.\n\nThe CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.\n\n## Upgrading to 0.0.118\n\nLogin now requires `AuthOptions.serverKemPublicKey`, the 1184-byte public key matching\n`AUTH_KEM_SK`. There is no implicit fallback to the published development key. Keep\nexisting OPRF and KEM secrets when upgrading; replacing the OPRF seed invalidates\nexisting password credentials. Project-root `VITE_*` variables are loaded by default.\n\nSessions use a versioned signed payload with a random session identifier and a user\nrevocation generation. Pre-upgrade session cookies and pending login challenges are\nrejected, so users must sign in again. Single-session logout revokes the signed session;\n`POST /auth/logout-all`, password reset, and confirmed two-factor changes revoke all\nsessions. Two-factor changes require a fresh login after success. Existing account IDs\nremain stable after password reset. The compiler includes the new revocation and factor\nsettings database declarations automatically.\n\nAuthenticated factor operations accept `headers` and `onRequestFailed(status)` options\nfor application account guards. HTTP failures include their status in the client error.\n",
4
4
  "auth/emails.md": "# Customizing the auth emails\n\nBuilt-in auth sends three transactional emails: the email-verification link, the password-reset link, and the two-factor code. Each ships with a plain, working default. You can replace any of them with your own branded template, and you never touch the auth code to do it.\n\n## The three emails\n\n| Email | When it sends | The value it gives your template |\n| --- | --- | --- |\n| Email verification | On register, when email confirmation is on | `link` (the confirm URL) |\n| Password reset | On `POST /auth/reset/request` | `link` (the reset URL) |\n| Two-factor code | On login or 2FA setup, for the email method | `code` (the numeric code) and `action` (the flow phrase) |\n\nThe defaults are intentionally minimal so a fresh project sends real, correct mail with no setup. To match your brand, override them.\n\n## How to override\n\nDrop a React template in your project's `emails/` folder with the reserved name for that email. If the file exists, the build uses it; if not, the default stands. This is the same `emails/*.tsx` system you use for your own mail (see [Email](../services/email.md)), so you get full markup and inline CSS.\n\n| Reserved file | Overrides | Prop it must read |\n| --- | --- | --- |\n| `emails/auth-confirm.tsx` | Email verification | `link` |\n| `emails/auth-reset.tsx` | Password reset | `link` |\n| `emails/auth-2fa.tsx` | Two-factor code | `code`, `action` |\n\nEach template is a default-exported React component. Read the one prop it is given, and it becomes a `{{token}}` hole the edge fills per send. Reading any other prop renders empty, so the build warns you.\n\n```tsx\n// emails/auth-confirm.tsx -> overrides the email-verification message.\nexport const subject = 'Verify your Acme account';\n\nexport default function AuthConfirm({ link }: { link: string }) {\n return (\n <div style={{ fontFamily: 'system-ui', padding: 24 }}>\n <h1 style={{ color: '#cb9820' }}>Welcome to Acme</h1>\n <p>Tap below to verify your email and finish signing up.</p>\n <p>\n <a\n href={link}\n style={{ background: '#cb9820', color: '#fff', padding: '10px 18px', borderRadius: 8, textDecoration: 'none' }}\n >\n Verify my email\n </a>\n </p>\n <p style={{ color: '#666', fontSize: 13 }}>Or paste this link: {link}</p>\n </div>\n );\n}\n```\n\nThe reset template works the same way with the `link` prop.\n\n## The subject line\n\nFor verification and reset, set the subject with `export const subject = '...'`. Leave it out and the default subject is used (`Confirm your account`, `Reset your password`).\n\nThe two-factor email is different. It serves three flows (signing in, enabling 2FA, and turning 2FA off), so besides the `code` it also gets an `action` prop: the framework fills it with the flow phrase, `sign in`, `enable two-factor authentication`, or `turn off two-factor authentication`. Write it into a sentence like \"Enter this code to {action}.\" and the one template reads correctly for every flow. The subject is likewise set by the framework and stays contextual, so a `subject` export in this file is ignored.\n\n```tsx\n// emails/auth-2fa.tsx -> overrides the 2FA code message (subject is contextual).\nexport default function Auth2fa({ code, action }: { code: string; action: string }) {\n return (\n <div style={{ fontFamily: 'system-ui', padding: 24, textAlign: 'center' }}>\n <p>Your Acme verification code:</p>\n <p style={{ fontSize: 32, fontWeight: 700, letterSpacing: 6 }}>{code}</p>\n <p>Enter it to {action}.</p>\n <p style={{ color: '#666' }}>It expires in a few minutes.</p>\n </div>\n );\n}\n```\n\n## What you can and cannot put in\n\nEach template is rendered once, at build time, into a static string of HTML with your styles inlined, and the prop becomes a fill hole. So a fixed layout and inline styles work, but per-send logic does not: a `{items.map(...)}` or a conditional runs at build, not per email. For these three messages that is all you need, one link or one code.\n\nThe only values auth hands each template are the ones in the table above (`link`, or `code` plus `action` for 2FA). There is no user name, no app name, no expiry value. Write those into the template text directly.\n\nStyling rules are the same as any toiljs email: write inline `style={{ ... }}`, or import a stylesheet and its rules are inlined for you. A bare CSS import with no inlining has no effect in an email client. See [Email](../services/email.md) for the details.\n\n## Delivery stays safe\n\nYou cannot weaken the security of these flows by overriding a template. Every auth email is sent detached (fire-and-forget), so the response time never reveals whether an address maps to an account. That holds whether the message is the default or your override. Overriding only changes what the email looks like, never how it is sent.\n\n## Where the templates go\n\nThere is nothing to import and nothing to register. The build reads `emails/auth-*.tsx`, bakes the effective templates into the compiled server, and the auth controller sends them. Changing a template and rebuilding (or saving under `toiljs dev`) is all it takes.\n\n## Related\n\n- [Email](../services/email.md): the full `emails/*.tsx` system, `EmailService`, and `EmailTemplate`.\n- [Configuration](./configuration.md): the auth secrets and the email provider a deployment must set.\n- [Usage](./usage.md): enabling auth, the client API, and guarding routes.\n",
5
5
  "auth/extending.md": "# Extending & integrating auth\n\nBuilt-in auth is deliberately opinionated so the common case is one line. This page covers the identity\nyou build ON, `ToilUserId`, and how to go beyond the defaults: keying your own data on a user, a custom\nuser shape, and hand-writing auth from the same primitives.\n\n## `ToilUserId`\n\nThe stable, tenant-scoped user identity: `sha256(mldsaPublicKey ‖ identifier ‖ domain)`, a 256-bit value.\nIt's a **global** (no import), like `crypto`.\n\n```ts\n// Read the current user's id in any handler (gate on hasSession(), see the null note below).\nconst id: ToilUserId = AuthService.userId()!;\n\n// Or derive one yourself.\nconst id2 = ToilUserId.derive(mldsaPublicKey, 'alice@example.com', 'acme.dacely.com');\n```\n\n| Member | Description |\n| --- | --- |\n| `ToilUserId.derive(pk, identifier, domain)` | Derive from an ML-DSA public key + email/username + tenant domain. Deterministic. |\n| `ToilUserId.fromBytes(b)` | Rebuild from a 32-byte digest (from `toBytes()` or storage). |\n| `toBytes(): Uint8Array` | The 32 identity bytes. |\n| `toHex(): string` | Lowercase 64-char hex, a convenient string key. |\n| `isZero(): bool` | True for the unset / anonymous id. |\n| `equals(other): bool` | Value equality. |\n| `a == b` / `a != b` | Overloaded value comparison, **O(1)** (four `u64` word compares, no byte loop, no allocation). |\n\n```ts\nconst a = ToilUserId.derive(pk, 'alice', 'acme.com');\nconst b = ToilUserId.derive(pk, 'alice', 'acme.com');\nconst c = ToilUserId.derive(pk, 'bob', 'acme.com');\na == b; // true, same inputs, same id\na != c; // true, different user\n```\n\n> **Null-check gotcha:** because `ToilUserId` overloads `==`, `AuthService.userId() == null` does NOT\n> type-check (`==` expects a `ToilUserId`). Gate with `AuthService.hasSession()` and then `userId()!`, or\n> compare with `getUser()` (a plain nullable). `===` is reference identity in AssemblyScript and is not\n> overloadable, use `==` for value equality.\n\n## Keying your own data on the user\n\n`toilUserId` is the right key for per-user data, it's stable across sessions/devices and opaque. Use the\nhex as a string key, or the bytes in a `@data` key class:\n\n```ts\n@data\nclass UserKey {\n id: Uint8Array = new Uint8Array(0); // toilUserId bytes\n constructor(id: Uint8Array = new Uint8Array(0)) { this.id = id; }\n}\n\n@data class Profile { displayName: string = ''; bio: string = ''; }\n\n@database\nclass AppDb {\n @collection static profiles: Documents<UserKey, Profile>;\n}\n\n@rest('profile')\nclass ProfileApi {\n @auth\n @post('/')\n public save(ctx: RouteContext): Response {\n const key = new UserKey(AuthService.userId()!.toBytes());\n const p = Profile.decode(ctx.request.body);\n // Save this user's profile. create is insert-only, so the first save creates\n // the record and later saves overwrite the existing one with enqueue.\n if (!AppDb.profiles.create(key, p)) {\n AppDb.profiles.enqueue(key, p);\n }\n return Response.text('saved\\n');\n }\n}\n```\n\n## Extending the user: add your own fields\n\nBuilt-in auth reserves exactly two fields on the authenticated user: `toilUserId` and `username`. To carry\nmore (a role, a display name, a tenant), just declare your OWN `@user` in your server code while\n`server.auth` is on. The build detects it and **extends** it: it injects the reserved `toilUserId` +\n`username` as the first two fields and mints sessions for your shape automatically. You do not opt out of\nanything, and there is still exactly one `@user` per program.\n\n```ts\n@user\nclass Account {\n admin: bool = false;\n displayName: string = '';\n tenant: string = '';\n // toilUserId + username are INJECTED by the build; do not declare them here.\n}\n```\n\nRules:\n\n- Do **not** declare `toilUserId` or `username` yourself. They are reserved and injected; declaring either is\n a compile error (`'username' is reserved by built-in auth`).\n- Your `@user` must be default-constructible (give every field an initializer), like any `@data` class.\n\n`AuthService.getUser()` is now typed to YOUR shape:\n\n```ts\nconst user = AuthService.getUser(); // { toilUserId, username, admin, displayName, tenant } | null\n```\n\n**Populating your fields.** Login fills `toilUserId` + `username`; your extra fields start at their declared\ndefaults (`admin = false`, and so on). The built-in controller cannot know your business fields, so you set\nthem yourself on one of your own `@auth` routes by reading the user, updating it, and re-minting the session:\n\n```ts\n@rest('account')\nclass AccountApi {\n @auth @post('/promote')\n public promote(): Response {\n const user = AuthService.getUser()!;\n user.admin = true;\n const resp = Response.text('promoted\\n');\n resp.setCookie(AuthService.mintSession(user.encode())); // re-sign the session with the new fields\n resp.setCookie(AuthService.userCookie(user.encode())); // update the readable companion cookie\n return resp;\n }\n}\n```\n\nIf you would rather hand-write the whole controller instead, you still can: do **not** enable `server.auth`,\nand build your own `@user` + routes from the [AuthService primitives](#the-authservice-primitive-reference)\nbelow. But for most apps, extending is all you need.\n\n## Adding email verification / 2FA\n\nLayer a second factor on top of the session with `TwoFactor` (stateless email codes, no DB; see\n[email](../services/email.md)). Typical flow: after login, require a verified email before granting access to\nsensitive routes.\n\n```ts\n@rest('2fa')\nclass TwoFactorApi {\n // Step 1: email a code to the logged-in user, hand back the signed token.\n @auth @post('/send')\n public send(): Response {\n const email = /* the user's email, e.g. their username, or a stored profile field */;\n const ch = TwoFactor.send(email, 'login'); // emails the code, returns { token, status }\n return Response.bytes(new DataWriter().writeString(ch.token).toBytes());\n }\n\n // Step 2: verify the code the user typed against the token.\n @auth @post('/verify')\n public verify(ctx: RouteContext): Response {\n const r = new DataReader(ctx.request.body);\n const token = r.readString(); const email = r.readString(); const code = r.readString();\n if (!TwoFactor.verify(token, email, code)) return Response.text('bad code\\n', 401);\n // Mark this session 2FA-verified: re-mint the session with a flag in your own @user, or store a\n // per-user \"verified\" record keyed on AuthService.userId().\n return Response.text('verified\\n');\n }\n}\n```\n\n`TwoFactor` gives integrity + expiry but not single-use (a code re-verifies within its TTL); keep the TTL\nshort. For a branded email, use `TwoFactor.issue(...)` (returns the code without sending) + your own\n`Emails.*` template. Call `TwoFactor.setSecret(...)` once at startup in production.\n\n## The `AuthService` primitive reference\n\nEverything the built-in controller is built from, available for hand-written auth. All are ambient globals\n(no import).\n\n**Sessions & cookies**\n- `mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie`: the signed `__Host-toil_sess` cookie.\n- `userCookie(userData, ttlSecs?): Cookie`: the readable `__Secure-toil_user` companion.\n- `clearSession(): Cookie` / `clearUserCookie(): Cookie`.\n- `hasSession(): bool`: the `@auth` predicate.\n- `getSessionBytes(): Uint8Array | null`: the verified `@user` payload bytes.\n- `getUser(): <your @user> | null`: decoded, typed to your `@user`.\n- `userId(): ToilUserId | null`: the stable id (built-in `@user` layout).\n- `setSecret(secret: Uint8Array)`: override the session HMAC key programmatically.\n\n**Post-quantum login crypto**\n- `oprfEvaluate(username, blinded): Uint8Array`: server-keyed OPRF eval.\n- `buildRegisterMessage(username, pk)` / `verifyRegister(pk, msg, sig): bool`: proof-of-possession.\n- `buildLoginMessage(sub, aud, cid, nonce, iat, exp, ct, memKiB, iterations, parallelism, serverKemKeyId)`\n / `verifyLogin(pk, msg, sig): bool`.\n- `mlkemDecapsulate(ct): Uint8Array`, `serverKemKeyId(): Uint8Array`.\n- `deriveSessionKey(sharedSecret, transcriptHash)`, `serverConfirmTag(sessionKey, transcriptHash)`: the\n mutual-auth confirmation.\n- `sha256(data): Uint8Array`.\n- `setOprfSeed(seed)`, `setServerKemSecretKey(sk)`, `setServerKemPublicKey(pk)`: override the seeds/keys.\n- Sizes: `PUBLIC_KEY_LEN`, `SIGNATURE_LEN`, `OPRF_ELEMENT_LEN`, `KEM_CIPHERTEXT_LEN`, `SHARED_SECRET_LEN`, …\n\n**Related globals**, `TwoFactor` (email codes; see [email](../services/email.md)), `RateLimitService` (used by\n`@ratelimit`), `Environment` (the secret store).\n\n## Two ways in, one behavior\n\nThe config flag and the import are identical at build time, both make the build append the shipped\n`@user` + `@rest('auth')` controller to the toilscript **entry** set, where their decorators weave and the\n`@rest` class self-mounts. (A framework decorator source only weaves as an entry; that's why a plain\n`import` of the controller isn't enough on its own, the marker import is detected by the build, which does\nthe entry injection.) This is why built-in auth needs no runtime registration call.\n",
6
6
  "auth/how-it-works.md": "# How Toil auth works\n\nToil auth is a **post-quantum, password-based, mutually-authenticated** login. The design goal: the user\ntypes a password, but the server never sees it, never stores it, and can't be phished into leaking a\nverifier, and every primitive is quantum-resistant.\n\nThis page explains the protocol. You do not need to understand it to use auth (see [Usage](./usage.md)),\nbut you should understand the guarantees before you ship.\n\n## The building blocks\n\n| Primitive | Role |\n| --- | --- |\n| **OPRF** (RFC 9497, ristretto255-SHA512) | A *server-keyed* salt. The client blinds its password, the server evaluates it under a per-user key, the client unblinds. The result can't be computed without the server, so a stolen database can't be brute-forced offline. |\n| **Argon2id** | Stretches the OPRF output into key material (memory-hard, GPU/ASIC-resistant). Runs in the browser. |\n| **ML-DSA-44** (FIPS 204) | The user's identity key pair is *derived* from the Argon2id output. The server stores only the **public** key. Login is proved by an ML-DSA signature. |\n| **ML-KEM-768** (FIPS 203) | Key encapsulation on login: the server proves it holds its KEM secret by returning a confirmation tag only derivable from the decapsulated shared secret → **mutual** auth (anti-phishing). |\n| **HMAC-SHA256** | Signs the session cookie (`AUTH_SESSION_SECRET`). |\n\n> The whole thing is sometimes called an *aPAKE* (asymmetric password-authenticated key exchange). Do not\n> call it OPAQUE, this is Toil's own OPRF + KEM + signature construction.\n\n## What the server stores\n\nPer account, in ToilDB (`@database AuthDb`):\n\n- `username`, a deterministic `salt`, the **ML-DSA public key**, and the Argon2id params.\n\nThat's it. **No password, no password hash, no verifier that can be brute-forced without the OPRF key.**\nLogin challenges are a second collection, consumed exactly once (atomic `getDelete`).\n\n## Registration\n\n```\n Browser (client) Toil edge (server)\n ───────────────── ──────────────────\n blind(password) OPRF key = f(seed, username)\n │ username, blinded │\n │ ───────── POST /auth/register/start ───────────► │\n │ │ evaluated = OPRF(blinded)\n │ ◄──── {mem,iters,par, salt, evaluated} ───────── │ (salt is deterministic per user)\n │ │\n unblind → oprfOut │\n seed = Argon2id(oprfOut, salt, params) │\n (pk, sk) = ML-DSA-44.keygen(seed) │\n proof = ML-DSA.sign(sk, \"register|username|pk\") │\n │ username, pk, proof │\n │ ───────── POST /auth/register/finish ──────────► │\n │ │ verifyRegister(pk, msg, proof) ✓ proof-of-possession\n │ ◄──────────── {status: 0 ok | 1 taken} ──────── │ store AuthAccount{username, salt, pk, params}\n```\n\nThe server never learns the password or the secret key `sk`, only the public key `pk` and a proof the\nclient holds the matching `sk`. A duplicate username returns a **distinguishable** `status = 1` (so the UI\ncan say \"taken, log in instead\"); everything else fails generically.\n\n## Login (with mutual auth)\n\n```\n Browser (client) Toil edge (server)\n ───────────────── ──────────────────\n blind(password) │\n │ username, blinded │\n │ ───────── POST /auth/login/start ──────────────► │ evaluated = OPRF(blinded) (ALWAYS, even unknown user)\n │ │ if known: store Challenge{cid, nonce, iat, exp}\n │ ◄─ {cid, aud, params, salt, nonce, iat, exp, ── │ (identical response whether the user exists or not)\n │ evaluated} │\n unblind → Argon2id → (pk, sk) = ML-DSA.keygen │\n (ct, ssС) = ML-KEM-768.encapsulate(serverKemPublicKey) │\n msg = \"login|username|aud|cid|nonce|iat|exp|ct|params|kid\" │\n sig = ML-DSA.sign(sk, msg) │\n │ cid, ct, sig │\n │ ───────── POST /auth/login/finish ─────────────► │ ch = challenges.getDelete(cid) (consume once; check exp)\n │ │ rebuild msg from OUR stored values + ct\n │ │ verifyLogin(acct.pk, msg, sig) ✓ it's really this user\n │ │ ssS = ML-KEM.decapsulate(ct) ✓ we hold the KEM key\n │ │ K = deriveSessionKey(ssS, H(msg))\n │ ◄──── {0, sessionToken, serverConfirm} ──────── │ serverConfirm = tag(K, H(msg))\n │ + Set-Cookie: __Host-toil_sess=… │ mint session cookie\n verify serverConfirm using ssC ✓ the server is genuine │\n```\n\nTwo verifications, both required:\n1. **The server verifies the client**: the ML-DSA signature over a message bound to the challenge, the\n Argon2id params, and the server's KEM key id. Replays fail (the challenge is consumed).\n2. **The client verifies the server**: the `serverConfirm` tag is derivable only from the ML-KEM shared\n secret, which only the holder of the KEM secret key can decapsulate. A phishing site can't forge it.\n\n## Anti-enumeration\n\n`/login/start` behaves identically for a known and an unknown user: it always runs the OPRF (a decoy key\nfor unknown users), returns a **deterministic** per-user salt and constant params, and a fresh challenge.\nThe challenge is persisted only for a real account, and `/login/finish` fails generically at consume for an\nunknown user. So an attacker can't probe which usernames exist. Every failure path returns the same\n`401 auth: request failed`.\n\n## Sessions & cookies\n\nOn successful login the server mints **two** cookies:\n\n- **`__Host-toil_sess`**: the authoritative session. HMAC-SHA256 signed with `AUTH_SESSION_SECRET`,\n `HttpOnly`, `Secure`, `SameSite=Lax`. It carries the `@user` codec payload. `@auth` and\n `AuthService.getUser()` open + verify this cookie server-side; a forged or tampered cookie fails.\n- **`__Secure-toil_user`**: a **readable** companion carrying the same payload, so the browser can show\n \"logged in as …\" via the client's `getUser()`. The server **never trusts it**, it is display-only.\n\nEach request runs in a fresh wasm instance, but the signed cookie is self-contained, so no server-side\nsession store is needed. `AUTH_SESSION_SECRET` must be identical across every edge instance (it is, via the\nenv store) so a cookie minted anywhere verifies everywhere.\n\n## The stable user identity: `ToilUserId`\n\nAt login the server derives a stable, tenant-scoped id and stores it in the session (the first field of the\nbuilt-in `@user`):\n\n```\ntoilUserId = SHA-256( mldsaPublicKey ‖ username ‖ domain ) // 256 bits\n```\n\n- **Stable:** same login key + username on the same tenant `domain` → same id, forever, across sessions\n and devices. Key your own data on it.\n- **Opaque + one-way:** it's a hash: safe to store, log, or expose without leaking the key or the address.\n- Read it anywhere with `AuthService.userId()`. See [Extending](./extending.md#toiluserid) for the\n `ToilUserId` API (O(1) `==` / `!=`, `toHex()`, …).\n\n## Threat-model summary\n\n- **Server database stolen** → attacker gets public keys + salts, not passwords. Brute-forcing needs the\n OPRF key (server-side) *and* Argon2id work per guess.\n- **Server compromised / malicious** → still can't recover passwords (only public keys) and can't forge a\n past session without `AUTH_SESSION_SECRET`.\n- **Phishing site** → can't produce the `serverConfirm` tag (no KEM secret), so a correct client aborts.\n- **Quantum adversary** → ML-DSA + ML-KEM are post-quantum; the OPRF/Argon2id/HMAC pieces are classical but\n not the long-term identity or key-exchange.\n- **Replay** → challenges are single-use (`getDelete`) with a short TTL.\n\nResidual responsibilities are yours: set the [secrets](./configuration.md), pin your deployment's KEM\npublic key in the client, and raise the Argon2id params for production.\n",
@@ -33,7 +33,7 @@ export const TOIL_DOCS = {
33
33
  "frontend/data-fetching.md": "# Fetching data\n\nYour React frontend and your toiljs backend live in one project, so toiljs generates a **typed client** that lets the browser call your server with full type safety and no hand-written `fetch` boilerplate. This page covers loading data for a page, calling the backend directly, submitting forms, and reading who is logged in.\n\n## The generated `Server` client\n\nWhen you build the server, toiljs writes a file at `shared/server.ts` that contains your `@data` classes and a typed description of every backend endpoint. Importing anything from `shared/server` attaches the runtime clients to a global called `Server`. From then on you call your backend through `Server`, fully typed, with editor autocomplete.\n\nThere are two surfaces under `Server`:\n\n- **`Server.REST.<controller>.<route>(args)`**: a real, typed `fetch` client for your `@rest` HTTP controllers. This is the working, recommended way to call the backend today.\n- **`Server.<service>.<method>(args)` and `Server.<remote>(args)`**: the typed RPC surface for `@service` / `@remote` functions.\n\nBoth are generated from your server code, so if you rename a route or change an argument type, the call site is a compile error until you fix it.\n\n> **Rule: never call your own backend with raw `fetch`. Always go through `Server`.**\n> Use `Server.REST.<controller>.<route>(args)` for `@rest` HTTP controllers, or\n> `Server.<service>.<method>(args)` / `Server.<remote>(args)` for `@service` / `@remote`\n> RPC. Raw `fetch` throws away the type safety, the argument/return decoding, and the\n> binary wire format that the generated client handles for you, and it silently breaks\n> when a route is renamed. This applies to AI assistants generating code too.\n>\n> ```ts\n> // WRONG: raw fetch to your backend\n> await fetch('/account/session', { method: 'POST', credentials: 'same-origin' });\n>\n> // RIGHT: the typed client (renames become compile errors, args + result are typed)\n> await Server.REST.account.session();\n> ```\n>\n> `fetch` is only for URLs that are NOT your toiljs backend (a third-party API, a CDN).\n\n## Calling a REST endpoint\n\n`Server.REST` mirrors your `@rest` controllers. If your backend has a `players` controller with routes on it, you call them like this:\n\n```tsx\nimport { NewPlayer, ScoreDelta } from 'shared/server';\n\n// POST /players with a typed @data body -> typed Promise<Player>\nconst player = await Server.REST.players.create({ body: new NewPlayer('Ada') });\n\n// POST /players/:id/score with a path param AND a body\nconst updated = await Server.REST.players.addScore({\n params: { id: 1 },\n body: new ScoreDelta(5n),\n});\n\n// GET /leaderboard -> typed Promise<Standings>\nconst board = await Server.REST.leaderboard.top();\n```\n\nThe single argument is an object with up to four optional parts:\n\n| Key | What it is |\n| --- | --- |\n| `params` | Path parameters, e.g. `{ id: 1 }` for a `/players/:id` route. |\n| `body` | The request body, usually a `@data` class instance. |\n| `query` | Query-string values. |\n| `headers` | Extra request headers. |\n\nReturn values are typed and decoded for you. A route that returns a `@data` type hands you the parsed class instance. A route that returns a raw `Response` hands you the raw fetch `Response`, so you can inspect the status and headers yourself:\n\n```tsx\n// This route returns a Response, so you get the raw fetch Response.\nconst res = await Server.REST.players.get({ params: { id: 1 } });\nif (!res.ok) {\n console.log('status', res.status);\n} else {\n const p = await res.json();\n}\n```\n\n`@data` classes are the typed values that cross the wire. You import them from `shared/server` and construct them normally (`new NewPlayer('Ada')`). See [Data types](../backend/data.md) for how they are defined on the server.\n\n### Handling errors\n\nA failed call throws. The global `parseError` helper turns any caught value into a readable message, which is handy in a `catch`:\n\n```tsx\ntry {\n const board = await Server.REST.leaderboard.top();\n} catch (err) {\n console.error(parseError(err));\n}\n```\n\n## Loading data for a page (loaders)\n\nCalling the backend from a button handler (as above) is fine for actions. For the data a page needs to *render*, use a route **loader** instead of a `useEffect`. A loader runs on navigation, in parallel with loading the page's code, and the page suspends (showing its `loading.tsx`) until the data is ready:\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n`useLoaderData<typeof loader>()` is fully typed from the loader's return. This keeps data fetching declarative and out of effects, and it is what lets server-rendered pages seed the browser with the server's data so hydration stays clean.\n\n### Caching and revalidating loader data\n\nLoader results are cached by URL. You control how long with `export const revalidate`:\n\n| `revalidate` value | Behavior |\n| --- | --- |\n| `0` (default) | Re-run the loader on every navigation to the route. |\n| a number `n` | Reuse cached data for `n` seconds, then refetch. |\n| `false` | Cache until you invalidate it manually. |\n\nAfter a mutation (say you just POSTed a new comment), refresh the current page's data with `revalidate()` or the router:\n\n```tsx\nconst router = Toil.useRouter();\nawait Server.REST.comments.add({ body: new NewComment(text) });\nrouter.revalidate(); // refetch the active route's loader\n// or target another route: router.revalidate('/posts');\n```\n\n`router.refresh()` re-runs the current loader and clears the cache; `router.revalidate(href)` invalidates a specific route.\n\n### Invalidating loader data by hand\n\n`revalidate()` and the `<Toil.Form>` `revalidate` prop are both built on one lower-level primitive: `Toil.invalidateLoaderData(href?)`. It drops cached loader data so the next render refetches it, but it does **not** trigger the re-render itself (that is the difference from `revalidate()`, which does both):\n\n- `invalidateLoaderData()` with no argument clears every route's cached data.\n- `invalidateLoaderData('/posts')` clears just that one route's entry.\n\nYou rarely need it directly; `revalidate()` (which pairs it with a re-render) is what you usually want. Reach for the primitive when you want to mark data stale *now* but let a later navigation do the actual refetch:\n\n```tsx\n// After a background sync finished, mark the dashboard stale so its\n// next visit refetches, without disturbing the page the user is on.\nToil.invalidateLoaderData('/dashboard');\n```\n\nSignature: `invalidateLoaderData(href?: string): void`.\n\n## Forms\n\nFor the common \"submit a form, then refresh the page's data\" loop, use `Toil.Form`. It runs an async action on submit (no page reload), tracks pending and error state, and revalidates the route's loader data on success:\n\n```tsx\nimport { NewMessage } from 'shared/server';\n\nexport default function Guestbook() {\n const entries = Toil.useLoaderData<typeof loader>();\n\n const sign = async (data: FormData) => {\n const author = String(data.get('author'));\n const message = String(data.get('message'));\n await Server.REST.guestbook.sign({ body: new NewMessage(author, message) });\n };\n\n return (\n <Toil.Form action={sign} resetOnSuccess>\n {({ pending }) => (\n <>\n <input name=\"author\" />\n <input name=\"message\" />\n <button disabled={pending}>Sign</button>\n </>\n )}\n </Toil.Form>\n );\n}\n```\n\nKey `Toil.Form` props:\n\n| Prop | Default | What it does |\n| --- | --- | --- |\n| `action` | (required) | Runs on submit, receiving the form's `FormData`. May be async. |\n| `revalidate` | `true` (current route) | Which loader data to refetch after a successful submit. |\n| `resetOnSuccess` | `false` | Clear the form fields after success. |\n| `onSuccess` / `onError` | | Callbacks for the two outcomes. |\n\nPassing a **render function** as `children` gives you live submit state (`pending`, `error`), so you can disable the button while the request is in flight. On success, `Form` revalidates the loader, so the page's data updates automatically without a manual refetch.\n\n## Mutations without a form: `useAction`\n\n`Toil.Form` is convenient sugar over a more general primitive, `Toil.useAction`. Use `useAction` for any write that is not a form submit: a delete button, a \"like\" toggle, a \"mark as read\" action. It runs your async function on demand and tracks the whole lifecycle (`pending`, `error`, `data`), then revalidates the affected loader data on success so the page reflects the change, exactly like `Form` does.\n\nYou call it with the function to run and an optional options object, and it hands back a handle:\n\n```tsx\nexport default function PostRow({ id }: { id: number }) {\n const del = Toil.useAction(\n (postId: number) => Server.REST.posts.remove({ params: { id: postId } }),\n { revalidate: true, onError: (e) => alert(parseError(e)) },\n );\n\n return (\n <button disabled={del.pending} onClick={() => void del.run(id)}>\n {del.pending ? 'Deleting...' : 'Delete'}\n </button>\n );\n}\n```\n\nThe handle it returns:\n\n| Field | What it is |\n| --- | --- |\n| `run(input)` | Runs the action. Resolves to the result, or `undefined` if it threw. The error is captured in `error` rather than rejected, so a fire-and-forget `onClick` cannot leak an unhandled rejection. |\n| `pending` | `true` while a run is in flight. Drive a spinner or a disabled button off this. |\n| `error` | The error from the last failed run, or `undefined`. |\n| `data` | The value the last successful run returned, or `undefined`. |\n| `reset()` | Clear `pending` / `error` / `data` back to idle. |\n\nThe options mirror `Toil.Form`:\n\n| Option | Default | What it does |\n| --- | --- | --- |\n| `revalidate` | `true` (current route) | Which loader data to refetch after a successful run: `true` (the active route), an `href` or array of hrefs (those routes), or `false` (none). |\n| `onSuccess` | | Called with the action's return value after a successful run. |\n| `onError` | | Called with the thrown value when the action fails. |\n\n`Toil.Form` is just this hook wired to a `<form>`'s submit event, with the form's `FormData` passed as the input. Anything a form does, you can do by hand with `useAction`.\n\n## Reading who is logged in\n\nTo render \"logged in as ...\" you need the current user. toiljs generates a `getUser()` helper (from the backend's `@user` surface) that reads the current user from a readable session cookie with no network round-trip. It is instant, which makes it perfect for display, but it is **untrusted**: a value read on the client can be forged, so never gate anything security-sensitive on it. For a trusted check, call a guarded backend route that re-verifies the signed session.\n\nThe full auth flow (post-quantum login, sessions, `getUser()`, and guarding routes) is its own guide:\n\n- [Auth usage](../auth/usage.md): reading the session, `getUser()`, and guarding pages.\n\n## RPC (`@service` / `@remote`)\n\nAlongside REST, toiljs generates a typed RPC surface for `@service` methods and free `@remote` functions. These read like plain function calls with no URL:\n\n```tsx\nconst n = await Server.ping(10); // a free @remote\nconst count = await Server.stats.playerCount(); // a @service method\n```\n\nThe types come straight from your server, so `Server.ping` knows it takes a number and returns a number. Under the hood each call encodes its arguments and POSTs them to a single reserved endpoint (`/__toil_rpc`) with a compact method id, then decodes the typed result. Both the local dev server and the production edge dispatch this endpoint, so RPC works end to end.\n\nIf a call throws an \"unavailable\" error, it means the generated client has not attached yet: build the server (`npm run build:server`) to regenerate `shared/server.ts`, and import from `shared/server` so the client loads (see the gotchas below). See [Typed RPC](../backend/rpc.md) for the backend side and when to choose RPC over REST.\n\n## Gotchas\n\n- **Import from `shared/server` to attach the clients.** `Server.REST` (and the RPC client) attach when you import from `shared/server`. Importing your `@data` classes from there does it naturally; if `Server.REST.foo()` throws an \"unavailable\" error, make sure the server has been built (`npm run build:server`) and that `shared/server` is imported.\n- **The server runs a fresh instance per request.** In the examples, in-memory writes are previews that do not persist. To keep data, write it to the database (see [Database](../database/README.md)). This is a backend property, but it explains why a create returns a value that is not there on the next request.\n- **Fetch page data in a `loader`, not `useEffect`.** A loader runs in parallel with the route chunk and integrates with `loading.tsx`, suspense, caching, and SSR hydration. A `useEffect` fetch runs only after the page mounts (slower, and invisible to the server).\n- **`getUser()` is display-only.** It is fast because it does not verify anything. Do real authorization on the server.\n\n## Related\n\n- [Routing](./routing.md): loaders, `loading.tsx`, and navigation.\n- [Backend HTTP routes](../backend/rest.md): the `@rest` controllers behind `Server.REST`.\n- [Typed RPC](../backend/rpc.md): the `@service` / `@remote` surface behind `Server`.\n- [Data types](../backend/data.md): the `@data` classes that cross the wire.\n- [Auth usage](../auth/usage.md): sessions and `getUser()`.\n",
34
34
  "frontend/images.md": "# Images\n\n`Toil.Image` is a drop-in replacement for `<img>` that stops layout shift, lazy-loads by default, and can fade in from a blurred placeholder. Use it for content images instead of a raw `<img>`.\n\n## Why not just use `<img>`?\n\nTwo problems with a plain `<img>`:\n\n1. **Layout shift.** Before the image loads, the browser does not know how tall it is, so the page has no space reserved. When the image arrives, everything below it jumps down. That jump hurts the user experience and your Core Web Vitals score (specifically CLS, Cumulative Layout Shift).\n2. **Loading cost.** Every image loads eagerly by default, competing for bandwidth with the things the user actually needs first.\n\n`Toil.Image` fixes both: it reserves the right amount of space up front, and it lazy-loads everything except the images you mark as high priority.\n\n## The simplest usage\n\nGive it a `src`, an `alt`, and a `width` + `height`. The width and height are the image's real pixel dimensions, and they are what reserve space so nothing jumps:\n\n```tsx\nexport default function Post() {\n return (\n <Toil.Image\n src=\"/images/diagram.png\"\n alt=\"How the request flows\"\n width={800}\n height={400}\n />\n );\n}\n```\n\n`alt` is required (toiljs enforces it for accessibility). For a purely decorative image, pass `alt=\"\"`.\n\nThere is no server-side resizing here: `Toil.Image` is a client component. Point `src` at an already-optimized image. When you import an image (see below), Vite hashes and optimizes the file for you.\n\n## Automatic blur placeholders with `?toil`\n\nFor a nicer loading experience you can show a tiny blurred preview of the image while the full one downloads. toiljs generates that preview automatically when you import the image with a `?toil` flag:\n\n```tsx\nimport hero from './hero.webp?toil';\n\nexport default function Landing() {\n return <Toil.Image src={hero} alt=\"Product hero\" placeholder=\"blur\" />;\n}\n```\n\nThe `?toil` import does not give you a plain URL string. It gives you an object:\n\n```ts\n{\n src: string; // the optimized, hashed image URL\n width: number; // the image's intrinsic width\n height: number; // the image's intrinsic height\n blurDataURL: string; // a tiny base64 blurred preview, inlined\n}\n```\n\n`Toil.Image` unpacks that object for you. So a `?toil` import auto-fills `width`, `height`, and the blur placeholder with **no extra props**: you do not repeat the dimensions, and `placeholder=\"blur\"` just works. Explicit props still win if you pass them.\n\nBehind the scenes, at import time toiljs uses `sharp` to downscale the image to about 24 pixels on its longest edge, blur it, and encode it as a small WebP data URI (a few hundred bytes) inlined right into your bundle. This runs in dev and in the build. Only raster images (`.png`, `.jpg`, `.webp`, `.avif`, `.gif`, `.tiff`) get a blur; SVGs and animated images are left as-is.\n\n### The skeleton fallback\n\n`placeholder=\"blur\"` is never a silent no-op. If there is no `blurDataURL` (you used a string `src` and did not pass one), `Toil.Image` shows a neutral animated \"skeleton shimmer\" instead of a blur. Either way the placeholder needs a reserved box to paint into, so give it `width` + `height` (or use `fill`, below).\n\nYou can also pass a `blurDataURL` by hand for a plain string `src`:\n\n```tsx\n<Toil.Image\n src=\"/images/photo.jpg\"\n alt=\"A photo\"\n width={1200}\n height={800}\n placeholder=\"blur\"\n blurDataURL=\"data:image/webp;base64,UklGR...\"\n/>\n```\n\n## The real layout-shift fix: aspect ratio\n\nThe important detail: the thing that actually prevents layout shift is not the blur, it is the **reserved aspect ratio**. When you pass both `width` and `height`, `Toil.Image` sets a CSS `aspect-ratio` derived from them. That reservation survives responsive CSS like `width: 100%` (a bare `width`/`height` attribute does not survive that). So the box holds its shape while the image loads, and nothing jumps. The blur is cosmetic; the reserved size is what holds.\n\nThe takeaway: always give `width` + `height` (or `fill`). A `?toil` import supplies them for you.\n\n## `fill`: sizing from the container\n\nSometimes you do not know the image's size, you want it to fill a box you control (a card, a banner). Pass `fill`:\n\n```tsx\n<div style={{ maxWidth: 480 }}>\n <Toil.Image src={photo} alt=\"Cover\" fill />\n</div>\n```\n\nWith `fill`, `Toil.Image` wraps the image in a block-level box and the image fills that box's width, scaling to its natural height. If you size the box yourself (with `width`/`height`, or a `style` like `aspectRatio: '16 / 9'`), the image *covers* that box, cropped according to `objectFit` (default `cover`):\n\n```tsx\n<Toil.Image\n src={photo}\n alt=\"Cover\"\n fill\n style={{ aspectRatio: '16 / 9' }}\n objectFit=\"cover\"\n/>\n```\n\nThe `fill` image flows in the normal document layout (it is never absolutely positioned), so it cannot escape to cover the whole page nor collapse to zero height, two common bugs with hand-rolled fill images. With `fill`, your `className` and `style` apply to the wrapper box, not the raw `<img>`.\n\nUse fixed `width`/`height` when you know the image's size; use `fill` when the layout decides the size.\n\n## Priority images (above the fold)\n\nBy default every `Toil.Image` lazy-loads: the browser fetches it only as it nears the viewport. That is right for most images, but wrong for the one big image at the top of the page (your hero, your LCP element). Mark that one `priority`:\n\n```tsx\n<Toil.Image src={hero} alt=\"Hero\" width={1200} height={630} priority />\n```\n\n`priority` loads the image eagerly and sets `fetchPriority=\"high\"`, telling the browser to fetch it right away. Use it only for the important above-the-fold image; everything else should stay lazy.\n\n## Prop reference\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `src` | `string \\| ToilImageSource` | (required) | A URL, or a `?toil` import object. |\n| `alt` | `string` | (required) | Alt text. Use `\"\"` for decorative images. |\n| `width` / `height` | `number` | from `?toil` | Intrinsic size, reserves space. |\n| `fill` | `boolean` | `false` | Fill the container instead of using fixed size. |\n| `objectFit` | CSS `object-fit` | `cover` (with fill) | How the image fits its box. |\n| `priority` | `boolean` | `false` | Eager load + high fetch priority (above-the-fold). |\n| `placeholder` | `'empty' \\| 'blur'` | `'empty'` | Show a blur / skeleton while loading. |\n| `blurDataURL` | `string` | from `?toil` | The tiny preview for `placeholder=\"blur\"`. |\n\nEvery other standard `<img>` attribute (`className`, `style`, `onLoad`, `sizes`, `srcSet`, `id`, `data-*`) passes straight through.\n\n## Gotchas\n\n- **No server-side resizing.** `Toil.Image` does not shrink or convert your image at request time. Ship an appropriately sized `src` (importing it lets Vite optimize and hash it). The `?toil` blur is only a placeholder, not a resized copy.\n- **A placeholder needs a reserved box.** `placeholder=\"blur\"` only shows if there is a size to paint into. Give `width` + `height`, or use `fill` inside a sized parent.\n- **`?toil` is for raster images.** SVGs and animated formats do not get a generated blur (importing them with `?toil` skips the blur step). Import an SVG normally.\n- **`alt` is mandatory.** This is intentional. For decorative images that add no meaning, pass `alt=\"\"` so screen readers skip them.\n- **Do not double up dimensions with a `?toil` import.** The import already carries `width`/`height`; passing them again is redundant (though harmless, explicit props win).\n\n## Related\n\n- [Styling](./styling.md): importing images and CSS in general.\n- [Metadata and SEO](./metadata.md): setting `og:image` for social-share previews.\n- [Rendering and SSR](./rendering.md): why first-paint layout stability matters.\n",
35
35
  "frontend/metadata.md": "# Metadata and SEO\n\nMetadata is the information in a page's `<head>`: its title, its description, and the tags that control how it looks when shared on social media or listed by a search engine. toiljs lets you set all of it per route, and bakes it into real HTML so crawlers see it even without running your JavaScript.\n\n## The quick version\n\nFor most pages, `export const metadata` from the route file. That is it:\n\n```tsx\n// client/routes/features/seo.tsx\nexport const metadata: Toil.Metadata = {\n title: 'useReducer | React Hooks',\n description: 'Manage complex state transitions with a reducer function.',\n keywords: ['react', 'hooks', 'useReducer'],\n canonical: 'https://example.com/features/seo',\n openGraph: {\n title: 'useReducer | React Hooks',\n description: 'Manage complex state transitions with a reducer.',\n type: 'website',\n },\n};\n\nexport default function SeoDemo() {\n return <main><h1>Route metadata</h1></main>;\n}\n```\n\nThe router applies this before the page paints (so the tab title updates with no flicker), and the build bakes it into the page's static HTML so search engines and link-preview bots read it directly.\n\n## The `Metadata` fields\n\n`Toil.Metadata` maps friendly fields onto the right `<meta>` and `<link>` tags for you:\n\n| Field | Becomes |\n| --- | --- |\n| `title` | The document `<title>`. |\n| `description` | `<meta name=\"description\">`. |\n| `keywords` | `<meta name=\"keywords\">` (an array is joined with commas). |\n| `canonical` | `<link rel=\"canonical\">`. |\n| `robots` | `<meta name=\"robots\">`, e.g. `'noindex, nofollow'`. |\n| `themeColor` | `<meta name=\"theme-color\">` (the accent color of some link embeds). |\n| `openGraph` | The `og:*` tags (title, description, type, url, image, siteName). |\n| `meta` | Escape hatch: extra raw `<meta>` tags. |\n| `link` | Escape hatch: extra raw `<link>` tags. |\n\nOpen Graph (the `og:*` tags) is the shared standard that Facebook, Discord, Slack, LinkedIn, and iMessage read to build a link preview card. Set `openGraph.image` (an absolute URL, ideally at least 1200 by 630 pixels) to control the preview picture:\n\n```tsx\nexport const metadata: Toil.Metadata = {\n title: 'Our launch',\n description: 'Read the announcement.',\n openGraph: {\n title: 'Our launch',\n description: 'Read the announcement.',\n type: 'article',\n image: 'https://example.com/og/launch.png',\n },\n};\n```\n\n## Dynamic metadata: `generateMetadata`\n\nWhen the title depends on the URL or on fetched data (a blog post's title, a product name), export `generateMetadata` instead of a static object. It receives the route params, the query, and the route loader's data, and returns a `Metadata`:\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({\n title: `Blog post ${params.id}`,\n description: `Reading blog post ${params.id}.`,\n});\n```\n\nNow `/blog/42` sets the tab to \"Blog post 42\". If your route has a `loader`, its data is passed in as `data`, so you can title a page from the content it loaded:\n\n```tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) =>\n Server.REST.blog.get({ params: { id: params.id } });\n\nexport const generateMetadata: Toil.GenerateMetadata = ({ data }) => ({\n title: data.title,\n description: data.excerpt,\n});\n```\n\n## Imperative and stateful head: `useHead`, `useTitle`, `<Head>`\n\nSometimes the head depends on component state, not on the route. For that, set it from inside a component with `Toil.useHead`, `Toil.useTitle`, or the declarative `<Toil.Head>`. These apply for the component's lifetime and revert when it unmounts:\n\n```tsx\nexport default function HeadDemo() {\n const [count, setCount] = useState(0);\n\n // The tab title updates every render as count changes.\n Toil.useTitle(`Clicked ${count} times`);\n\n Toil.useHead({\n meta: [{ name: 'description', content: `Clicked ${count} times.` }],\n });\n\n return <button onClick={() => setCount((c) => c + 1)}>Clicked {count}</button>;\n}\n```\n\nThe declarative form renders nothing and is equivalent:\n\n```tsx\n<Toil.Head\n title=\"Blog\"\n meta={[{ name: 'description', content: 'Latest posts' }]}\n/>\n```\n\nUse `useHead`/`useTitle` when the value is dynamic or lives in component state; use the `metadata` export when it is a static property of the route.\n\n### Applying a whole `Metadata` object from a component: `useMetadata`\n\n`useHead` takes raw `<meta>` and `<link>` tags. When you would rather pass the same friendly `Metadata` shape you use in a route's `metadata` export (with `title`, `openGraph`, `keywords`, and the rest), use `Toil.useMetadata` from inside any component. It applies for that component's lifetime and reverts on unmount, exactly like `useHead`. This is the tool for content that is not itself a route file: a reusable article component, a widget, a search-result view.\n\n```tsx\nexport default function Article({ post }: { post: Post }) {\n Toil.useMetadata({\n title: post.title,\n description: post.excerpt,\n openGraph: { type: 'article', title: post.title, image: post.cover },\n });\n\n return <article>{/* ... */}</article>;\n}\n```\n\nThere is a declarative twin, `<Toil.Metadata title=\"...\" openGraph={...} />`, which renders nothing (the component-level counterpart of a route's `metadata` export). A route's own `metadata` export is still merged last (highest priority), so it wins for the keys it sets, and `useMetadata` fills in for routes that declare none.\n\n> **Advanced.** Under the hood, `Toil.resolveMetadata(metadata)` is the pure function that expands a `Metadata` object into concrete `<meta>`/`<link>` tags (a `HeadSpec`), and `Toil.mergeHead(specs)` is the pure merge that resolves all active head contributions into the final result (the last `title` wins, `meta` dedupes by `name`/`property`, `link` by `rel` + `href`). You rarely call these directly, but they are exported for tooling and tests.\n\n## How the pieces combine\n\nMultiple things can contribute to the head at once: your site-wide defaults, a layout, and the page. They merge by key, with a clear priority. Later, more specific contributions win per key, and anything left unset falls through to a broader default:\n\n```mermaid\nflowchart TB\n A[\"Site-wide defaults<br/>client.seo config\"] --> M{\"merge by key\"}\n B[\"Root layout &lt;Toil.Head&gt;<br/>(fallback title, description)\"] --> M\n C[\"Component useHead / useTitle\"] --> M\n D[\"Route metadata / generateMetadata<br/>(highest priority)\"] --> M\n M --> R[\"The page's final &lt;head&gt;\"]\n```\n\n- A **root layout** is the natural home for site-wide fallbacks (a default title and description for any page that sets none):\n\n ```tsx\n // client/layout.tsx\n <Toil.Head\n title=\"My Site\"\n meta={[{ name: 'description', content: 'Planet-scale apps.' }]}\n />\n ```\n\n- A **route's `metadata`** overrides those defaults for the keys it sets, while the layout still fills in everything the route leaves unset. So a page can set just a `title` and inherit the site description.\n\nThe rule of thumb: put fallbacks in the layout, put the specifics on the route.\n\n## Build-time SEO for the whole site\n\nBeyond per-route metadata, toiljs generates site-level SEO assets at build time from a `client.seo` block in `toil.config.ts`. These are baked into the HTML `<head>` and written as files, so JavaScript-less crawlers and AI bots get correct information:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n seo: {\n url: 'https://example.com', // required for canonical/OG urls, sitemap\n title: 'My Site',\n description: 'Planet-scale apps from a single repo.',\n openGraph: {\n type: 'website',\n siteName: 'My Site',\n image: 'https://example.com/og.png',\n },\n twitter: { card: 'summary_large_image', site: '@mysite' },\n robots: { ai: 'allow' }, // allow or disallow known AI crawlers\n llms: { instructions: 'Docs live at /docs.' },\n jsonLd: { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },\n },\n },\n});\n```\n\nFrom this one block, the build:\n\n- bakes the default `<title>`, `description`, Open Graph, Twitter card, and JSON-LD structured data into every page's HTML;\n- overlays each route's own `metadata` on top and points that route's canonical and `og:url` at its own URL;\n- generates `robots.txt` (with directives for AI crawlers like GPTBot and ClaudeBot), `sitemap.xml` (from your static routes), and `llms.txt` (a guidance file for AI crawlers).\n\nYou get correct, per-page SEO in the raw HTML with almost no manual tag writing. Confirm it with \"View source\" on a built page: the real title and tags are right there, not injected later by JavaScript.\n\n### The complete `client.seo` reference\n\nEvery field `client.seo` accepts is below. All of them are optional, and everything you set becomes a baked-in `<head>` tag or a generated file (`robots.txt`, `sitemap.xml`, `llms.txt`).\n\n**Top level:**\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `url` | `string` | Absolute site base URL, e.g. `'https://example.com'`. Unlocks `sitemap.xml`, the canonical `<link>`, and absolute Open Graph / Twitter URLs. |\n| `title` | `string` | Default document `<title>`. |\n| `description` | `string` | Default `<meta name=\"description\">`. |\n| `robotsMeta` | `string` | Default `<meta name=\"robots\">`, e.g. `'index, follow'`. (This is the meta tag; the `robots` field below is the separate `robots.txt` file.) |\n| `themeColor` | `string` | `<meta name=\"theme-color\">`, also the accent color of some Discord / Slack link embeds. |\n| `openGraph` | object | Open Graph (`og:*`) defaults, see below. |\n| `twitter` | object | Twitter / X card, see below. |\n| `facebook` | `{ appId?: string }` | `appId` renders `<meta property=\"fb:app_id\">`. Open Graph covers the rest of the Facebook card. |\n| `preconnect` | `string[]` | Origins to `<link rel=\"preconnect\">` (early connection hints). |\n| `dnsPrefetch` | `string[]` | Origins to `<link rel=\"dns-prefetch\">`. |\n| `jsonLd` | object or object[] | JSON-LD structured data injected as `<script type=\"application/ld+json\">`. Pass an array to include several nodes (they are serialized into one `<script>` as a JSON array). |\n| `robots` | object or `false` | `robots.txt` generation, see below. `false` skips the file. |\n| `sitemap` | `boolean` | `sitemap.xml` generation. On by default when `url` is set; `false` skips it. |\n| `llms` | object or `boolean` | `llms.txt` (AI-crawler guidance) generation. `false` skips it, `true` or an object configures it. |\n\n**`openGraph`** (the `og:*` tags; `title` and `description` fall back to the top-level values):\n\n| Field | Type | Renders |\n| --- | --- | --- |\n| `title` | `string` | `og:title` |\n| `description` | `string` | `og:description` |\n| `type` | `string` | `og:type`, e.g. `'website'` or `'article'` (defaults to `'website'`). |\n| `siteName` | `string` | `og:site_name` |\n| `locale` | `string` | `og:locale`, e.g. `'en_US'`. |\n| `image` | `string` | `og:image`, the preview picture (absolute URL, ideally at least 1200 by 630 pixels). |\n| `imageAlt` | `string` | `og:image:alt` |\n| `imageWidth` | `number` | `og:image:width` in pixels (lets Facebook / LinkedIn render without a re-fetch). |\n| `imageHeight` | `number` | `og:image:height` in pixels. |\n| `imageType` | `string` | `og:image:type`, e.g. `'image/png'`. |\n\n**`twitter`** (the Twitter / X card; unset fields fall back to the Open Graph or top-level values):\n\n| Field | Type | Renders |\n| --- | --- | --- |\n| `card` | `string` | `twitter:card`: `'summary'` or `'summary_large_image'` (defaults by whether an image is present). |\n| `site` | `string` | `twitter:site`, the site's `@handle`. |\n| `creator` | `string` | `twitter:creator`, the author's `@handle`. |\n| `title` | `string` | `twitter:title` (falls back to `openGraph.title` / `title`). |\n| `description` | `string` | `twitter:description` (falls back to `openGraph.description` / `description`). |\n| `image` | `string` | `twitter:image` (falls back to `openGraph.image`). |\n| `imageAlt` | `string` | `twitter:image:alt` (falls back to `openGraph.imageAlt`). |\n\n**`robots`** (the `robots.txt` file; set `robots: false` to skip it):\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `rules` | `RobotsRule[]` | Custom `User-agent` groups. Each rule is `{ userAgent?: string \\| string[], allow?: string[], disallow?: string[] }`. Defaults to one group allowing everything (`User-agent: *`, `Allow: /`). |\n| `ai` | `'allow'` or `'disallow'` | How to treat known AI crawlers (GPTBot, ClaudeBot, Google-Extended, and more). Default `'allow'`. |\n| `sitemap` | `string` | Explicit `Sitemap:` line (defaults to `<url>/sitemap.xml` when `url` is set). |\n\n**`llms`** (the `llms.txt` guidance file; set `llms: false` to skip it, `llms: true` for defaults):\n\n| Field | Type | What it does |\n| --- | --- | --- |\n| `title` | `string` | The file's heading (falls back to `seo.title`). |\n| `summary` | `string` | The summary blockquote (falls back to `seo.description`). |\n| `instructions` | `string` | Free-form guidance for AI / LLM crawlers. |\n| `pages` | `LlmsPage[]` | Key pages, each `{ title: string, url: string, description?: string }`. Defaults to the site's static routes. |\n\nA fully worked config touching most of these fields:\n\n```ts\n// toil.config.ts\nimport { defineConfig } from 'toiljs/compiler';\n\nexport default defineConfig({\n client: {\n seo: {\n url: 'https://example.com', // base URL: unlocks sitemap, canonical, absolute OG/Twitter URLs\n title: 'My Site', // default <title>\n description: 'Planet-scale apps.', // default <meta name=\"description\">\n robotsMeta: 'index, follow', // default <meta name=\"robots\">\n themeColor: '#0b0b0f', // <meta name=\"theme-color\">\n\n openGraph: {\n type: 'website', // og:type\n siteName: 'My Site', // og:site_name\n locale: 'en_US', // og:locale\n image: 'https://example.com/og.png', // og:image (absolute, ideally 1200x630)\n imageAlt: 'My Site preview', // og:image:alt\n imageWidth: 1200, // og:image:width\n imageHeight: 630, // og:image:height\n imageType: 'image/png', // og:image:type\n },\n\n twitter: {\n card: 'summary_large_image', // twitter:card\n site: '@mysite', // twitter:site\n creator: '@ada', // twitter:creator\n // title / description / image / imageAlt fall back to openGraph + top level\n },\n\n facebook: { appId: '1234567890' }, // <meta property=\"fb:app_id\">\n\n preconnect: ['https://cdn.example.com'], // <link rel=\"preconnect\">\n dnsPrefetch: ['https://analytics.example.com'], // <link rel=\"dns-prefetch\">\n\n robots: {\n ai: 'allow', // known AI crawlers: 'allow' | 'disallow'\n rules: [ // custom User-agent groups for robots.txt\n { userAgent: '*', allow: ['/'], disallow: ['/admin'] },\n ],\n // sitemap: 'https://example.com/sitemap.xml', // explicit line (auto from url otherwise)\n },\n sitemap: true, // generate sitemap.xml (on by default when url is set)\n\n llms: { // llms.txt (AI-crawler guidance)\n title: 'My Site',\n summary: 'Planet-scale apps from a single repo.',\n instructions: 'Docs live at /docs.',\n // pages: [{ title: 'Docs', url: 'https://example.com/docs', description: 'Guides' }],\n },\n\n jsonLd: [ // array = several nodes in one <script>\n { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },\n { '@context': 'https://schema.org', '@type': 'Organization', name: 'My Site' },\n ],\n },\n },\n});\n```\n\n## Per-request titles on server-rendered pages\n\nFor a page that is server-rendered (`export const ssr = true`), the backend can set a fresh `<title>` for each individual request (for example, a search page titled with the query the visitor typed). The edge splices that per-request title into the document before sending it, so the correct title is in the very first byte of HTML. This is a server-side API used in your `render` function; the frontend `metadata` above covers everything else. See [Rendering and SSR](./rendering.md) for how SSR pages are assembled.\n\n## Gotchas\n\n- **`generateMetadata` needs the data available.** It runs with the route loader's data, so it can only use what the loader returns. Fetch what the title needs in the loader.\n- **Open Graph images must be absolute URLs.** A relative `/og.png` will not resolve for an external crawler. Use the full `https://...` URL (set `seo.url` and it can build them for you).\n- **`seo.url` unlocks the site-level assets.** `sitemap.xml`, canonical links, and absolute OG urls all need the site's base `url`. Set it once in the config.\n- **Static export vs runtime.** The `metadata` export is baked into HTML at build (great for crawlers) *and* applied at runtime on navigation. `useHead` runs only at runtime; a crawler that does not execute JS will not see it. Prefer the `metadata` export for anything that matters for SEO.\n\n## Related\n\n- [Rendering and SSR](./rendering.md): how the baked head and SSR title reach the browser.\n- [Images](./images.md): producing the `og:image` for share previews.\n- [Routing](./routing.md): where `metadata` and `generateMetadata` live in a route file.\n- [Configuration](../concepts/config.md): the full `toil.config.ts` reference.\n",
36
- "frontend/navigation.md": "# Navigation\n\nOnce your files are turned into URLs (see [Routing](./routing.md)), you need a way to move between them. This page is about that half: the links, hooks, and functions that navigate a user from one page to the next without a full reload, keep the current section highlighted, prefetch what is coming, and restore scroll. Everything here lives on the global `Toil` object, so route files need no imports for the common cases.\n\nRouting is \"file to URL\"; navigation is \"get me to that URL\". If you are looking for how a file becomes a page, or for dynamic params and layouts, that is the [Routing](./routing.md) page.\n\n## `Toil.Link`\n\n`Toil.Link` is the client-side replacement for a plain `<a>`. It navigates in place (no full page reload), and it prefetches the target route's chunk on hover or focus, so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every standard anchor attribute (`className`, `style`, `target`, `rel`, `download`, `referrerPolicy`, `ref`, `data-*`, `aria-*`, event handlers, and so on), plus a few toiljs controls:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |\n| `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |\n\n### When `Link` does not intercept\n\n`Link` is deliberate about when to hand a click back to the browser. It only intercepts a plain, same-origin, left-click. All of these fall through to native browser behavior instead:\n\n- **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.\n- **`target` other than `_self`** (for example `target=\"_blank\"`, a new tab).\n- **`download`** links.\n- **In-page `#hash`-only** links.\n- **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so \"open in new tab\" keeps working).\n\nBecause of this, you can point a `Link` at an external site or a download and it behaves exactly like an `<a>` would, no special-casing on your part. For genuinely external links a plain `<a>` is still fine; reach for `Link` when the target is one of your own routes.\n\n## `Toil.NavLink` and active state\n\n`Toil.NavLink` is a `Link` that knows whether it points at the current page. When active it adds a class (`active` by default) and sets `aria-current=\"page\"`, which is exactly what a navigation bar wants for highlighting the current section. It inherits Link's full anchor API and its prefetching.\n\nOn top of `LinkProps` it adds:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |\n| `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |\n| `className` | `string \\| (state) => string \\| undefined` | (none) | A string, or a function of `{ isActive }`. |\n| `style` | `CSSProperties \\| (state) => CSSProperties \\| undefined` | (none) | A style object, or a function of `{ isActive }`. |\n| `children` | `ReactNode \\| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |\n\nA simple nav bar with string classes:\n\n```tsx\n// client/components/Nav.tsx\nexport default function Nav() {\n return (\n <nav>\n <Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n <Toil.NavLink href=\"/blog\">Blog</Toil.NavLink>\n <Toil.NavLink href=\"/about\" activeClassName=\"is-current\">About</Toil.NavLink>\n </nav>\n );\n}\n```\n\nBy default a parent link stays active for its sub-paths, so `/blog` is active on `/blog`, `/blog/42`, and `/blog/42/edit`. Pass `end` to require an exact match. This matters most for the home link: `/` would otherwise be active on every page, so `Home` almost always wants `end`.\n\nThe function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>\n {({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}\n</Toil.NavLink>\n```\n\n## Navigating in code\n\nNot every navigation is a click. After a form submit, a login, or a `fetch`, you often want to move the user yourself. There are three ways, from smallest to fullest.\n\n### `Toil.useNavigate()`\n\nThe hook returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `NavigateOptions`, the same two controls `Link` exposes:\n\n| Option | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |\n| `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |\n\n### `Toil.navigate` (outside React)\n\nThe exact same function is available free of any hook as `Toil.navigate(href, options)`. Use it in code that is not a React component: a plain event handler, a utility module, or right after a `fetch` resolves:\n\n```tsx\nasync function saveDraft(draft: Draft) {\n await Server.REST.posts.create({ body: draft });\n Toil.navigate('/posts');\n}\n```\n\n`Toil.back()`, `Toil.forward()`, and `Toil.refresh()` are the history counterparts, also callable from anywhere: `back` and `forward` step through history, and `refresh` re-renders the current route (re-running its loader for the current URL).\n\n### `Toil.useRouter()`\n\nFor a full imperative handle, `useRouter()` returns a `RouterInstance`:\n\n```tsx\nexport default function PostActions() {\n const router = Toil.useRouter();\n return (\n <>\n <button onClick={() => router.push('/blog/new')}>New post</button>\n <button onClick={() => router.back()}>Back</button>\n <button onClick={() => router.revalidate()}>Refresh data</button>\n </>\n );\n}\n```\n\n| Method | What it does |\n| --- | --- |\n| `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |\n| `replace(href)` | Navigate to `href`, replacing the current history entry. |\n| `back()` | Go back one history entry. |\n| `forward()` | Go forward one history entry. |\n| `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |\n| `revalidate(href?)` | Invalidate cached loader data and re-render so it refetches. No argument targets the active route; pass an `href` for a specific route. Use after a mutation. |\n| `prefetch(href)` | Warm a route's chunk ahead of navigation. |\n\nReach for `revalidate()` after a write (you changed some data and want the current page's loader to refetch), and `refresh()` when you want a clean re-run of everything. `push`/`replace` are the imperative twins of a `Link` click.\n\n## Typed hrefs and the `href()` escape hatch\n\nEvery `href` you pass to `Toil.Link`, `NavLink`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a `toil-routes.d.ts` that narrows the `Href` type to the union of your actual paths, so a typo is a compile error before you run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes appear in that union as template-literal types, so a file at `client/routes/blog/[id].tsx` contributes `` `/blog/${string}` `` and a single interpolation still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine\n```\n\nWhen a URL is assembled from several data pieces (or from values TypeScript cannot prove the shape of), it is typed as a plain `string`, and `string` is not assignable to `Href`. That is when you reach for `Toil.href()`, the escape hatch that asserts a runtime string is a valid href:\n\n```tsx\nconst path = `/${product.category}/${product.slug}`; // string\nToil.navigate(Toil.href(path)); // asserted valid\n<Toil.Link href={Toil.href(path)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so use it only when the type-check is genuinely in your way, not to paper over a real typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n## Reading the current location\n\nA handful of hooks let a component read where it is. Each re-reads on every navigation, so a component using one re-renders when the location changes:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.usePathname()` | The current pathname, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |\n| `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |\n\n`useNavigationPending()` is what you wire a top loading bar to. It flips to `true` when a navigation begins and back to `false` once the new route commits:\n\n```tsx\n// client/components/ProgressBar.tsx\nexport default function ProgressBar() {\n const pending = Toil.useNavigationPending();\n return <div className=\"progress\" data-active={pending} />;\n}\n```\n\n`useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:\n\n```tsx\nexport default function Results() {\n const params = Toil.useSearchParams();\n const q = params.get('q') ?? '';\n return <p>Results for {q}</p>;\n}\n```\n\n## Prefetching\n\ntoiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:\n\n- **Hover / focus intent.** When you hover or focus a link that points at a known internal route, toiljs warms both its route chunk and its loader data, so the actual click can commit right away.\n- **Viewport intent.** As a link scrolls into view (or within about 200px of it), its route chunk is warmed. Links added later by client navigation are picked up automatically.\n\nPrefetching is best-effort and cheap: each route loads at most once, a failed prefetch is forgotten (so the real navigation can retry and surface the error), and new-tab / download / opted-out links are skipped. It is also **skipped entirely when the browser signals data-saver** (or reports a 2g-class connection), so you never spend a metered user's bandwidth on speculation.\n\nOpt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:\n\n```tsx\n<Toil.Link href=\"/huge-report\" prefetch={false}>Annual report</Toil.Link>\n```\n\nYou can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:\n\n```tsx\n<button\n onPointerEnter={() => Toil.prefetch('/dashboard')}\n onClick={() => Toil.navigate('/dashboard')}>\n Open dashboard\n</button>\n```\n\n## Scroll restoration\n\ntoiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):\n\n- **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.\n- **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.\n- **A `#hash` target** scrolls that element into view instead of jumping to the top.\n\nTo keep the current scroll on a push navigation, set `scroll={false}` on the `Link` (or `{ scroll: false }` in `NavigateOptions`). This is handy for tab bars or filters that change the URL but should not yank the viewport:\n\n```tsx\n<Toil.Link href=\"/settings/billing\" scroll={false}>Billing</Toil.Link>\n```\n\n```tsx\nnavigate('/settings/billing', { scroll: false });\n```\n\n## Animated transitions\n\nTwo optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:\n\n```ts\n// toil.config.ts\nexport default {\n client: {\n viewTransitions: true, // browser View Transitions API (a crossfade between pages)\n transitions: true, // React transition: keep the old page visible while the next loads\n },\n};\n```\n\n`viewTransitions` uses the browser's View Transitions API to crossfade the old and new page (and it respects `prefers-reduced-motion`, animating nothing for users who ask for less motion). `transitions` wraps each navigation in a React transition, keeping the current page on screen while the next route's loader runs, instead of showing its `loading.tsx` right away (smoother, but you trade away the immediate loading state).\n\nConfig is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:\n\n```tsx\nToil.setViewTransitions(true);\nToil.setTransitions(false);\n```\n\nYou rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.\n\n## Types\n\nAlmost everything on this page is a value on the global `Toil` object, so you call it with no import (`Toil.Link`, `Toil.navigate`, `Toil.useRouter`, and so on). A few of the **types**, though, are not namespaced under `Toil`, so if you want to annotate a prop or a variable you import them from `toiljs/client`:\n\n```tsx\nimport type {\n LinkProps,\n NavLinkProps,\n NavLinkState,\n NavigateOptions,\n RouterInstance,\n} from 'toiljs/client';\n```\n\nFor example, a component that forwards `Link` props, or a helper typed against the router handle:\n\n```tsx\nimport type { RouterInstance } from 'toiljs/client';\n\nfunction logoutThenGo(router: RouterInstance) {\n router.replace('/login');\n}\n```\n\n## Related\n\n- [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.\n",
36
+ "frontend/navigation.md": "# Navigation\n\nOnce your files are turned into URLs (see [Routing](./routing.md)), you need a way to move between them. This page is about that half: the links, hooks, and functions that navigate a user from one page to the next without a full reload, keep the current section highlighted, prefetch what is coming, and restore scroll. Everything here lives on the global `Toil` object, so route files need no imports for the common cases.\n\nRouting is \"file to URL\"; navigation is \"get me to that URL\". If you are looking for how a file becomes a page, or for dynamic params and layouts, that is the [Routing](./routing.md) page.\n\n## `Toil.Link`\n\n`Toil.Link` is the client-side replacement for a plain `<a>`. It navigates in place (no full page reload), and it prefetches the target route's chunk on hover or focus, so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every standard anchor attribute (`className`, `style`, `target`, `rel`, `download`, `referrerPolicy`, `ref`, `data-*`, `aria-*`, event handlers, and so on), plus a few toiljs controls:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |\n| `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |\n\n### When `Link` does not intercept\n\n`Link` is deliberate about when to hand a click back to the browser. It only intercepts a plain, same-origin, left-click. All of these fall through to native browser behavior instead:\n\n- **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.\n- **`target` other than `_self`** (for example `target=\"_blank\"`, a new tab).\n- **`download`** links.\n- **In-page `#hash`-only** links.\n- **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so \"open in new tab\" keeps working).\n\nBecause of this, you can point a `Link` at an external site or a download and it behaves exactly like an `<a>` would, no special-casing on your part. For genuinely external links a plain `<a>` is still fine; reach for `Link` when the target is one of your own routes.\n\n## `Toil.NavLink` and active state\n\n`Toil.NavLink` is a `Link` that knows whether it points at the current page. When active it adds a class (`active` by default) and sets `aria-current=\"page\"`, which is exactly what a navigation bar wants for highlighting the current section. It inherits Link's full anchor API and its prefetching.\n\nOn top of `LinkProps` it adds:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |\n| `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |\n| `className` | `string \\| (state) => string \\| undefined` | (none) | A string, or a function of `{ isActive }`. |\n| `style` | `CSSProperties \\| (state) => CSSProperties \\| undefined` | (none) | A style object, or a function of `{ isActive }`. |\n| `children` | `ReactNode \\| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |\n\nA simple nav bar with string classes:\n\n```tsx\n// client/components/Nav.tsx\nexport default function Nav() {\n return (\n <nav>\n <Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n <Toil.NavLink href=\"/blog\">Blog</Toil.NavLink>\n <Toil.NavLink href=\"/about\" activeClassName=\"is-current\">About</Toil.NavLink>\n </nav>\n );\n}\n```\n\nBy default a parent link stays active for its sub-paths, so `/blog` is active on `/blog`, `/blog/42`, and `/blog/42/edit`. Pass `end` to require an exact match. This matters most for the home link: `/` would otherwise be active on every page, so `Home` almost always wants `end`.\n\nThe function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>\n {({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}\n</Toil.NavLink>\n```\n\n## Navigating in code\n\nNot every navigation is a click. After a form submit, a login, or a `fetch`, you often want to move the user yourself. There are three ways, from smallest to fullest.\n\n### `Toil.useNavigate()`\n\nThe hook returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `NavigateOptions`, the same two controls `Link` exposes:\n\n| Option | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |\n| `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |\n\n### `Toil.navigate` (outside React)\n\nThe exact same function is available free of any hook as `Toil.navigate(href, options)`. Use it in code that is not a React component: a plain event handler, a utility module, or right after a `fetch` resolves:\n\n```tsx\nasync function saveDraft(draft: Draft) {\n await Server.REST.posts.create({ body: draft });\n Toil.navigate('/posts');\n}\n```\n\n`Toil.back()`, `Toil.forward()`, and `Toil.refresh()` are the history counterparts, also callable from anywhere: `back` and `forward` step through history, and `refresh` re-renders the current route (re-running its loader for the current URL).\n\n### `Toil.useRouter()`\n\nFor a full imperative handle, `useRouter()` returns a `RouterInstance`:\n\n```tsx\nexport default function PostActions() {\n const router = Toil.useRouter();\n return (\n <>\n <button onClick={() => router.push('/blog/new')}>New post</button>\n <button onClick={() => router.back()}>Back</button>\n <button onClick={() => router.revalidate()}>Refresh data</button>\n </>\n );\n}\n```\n\n| Method | What it does |\n| --- | --- |\n| `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |\n| `replace(href)` | Navigate to `href`, replacing the current history entry. |\n| `back()` | Go back one history entry. |\n| `forward()` | Go forward one history entry. |\n| `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |\n| `revalidate(href?)` | Invalidate cached loader data and re-render so it refetches. No argument targets the active route; pass an `href` for a specific route. Use after a mutation. |\n| `prefetch(href)` | Warm a route's chunk ahead of navigation. |\n\nReach for `revalidate()` after a write (you changed some data and want the current page's loader to refetch), and `refresh()` when you want a clean re-run of everything. `push`/`replace` are the imperative twins of a `Link` click.\n\n## Typed hrefs and the `href()` escape hatch\n\nEvery `href` you pass to `Toil.Link`, `NavLink`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a `toil-routes.d.ts` that narrows the `Href` type to the union of your actual paths, so a typo is a compile error before you run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes appear in that union as template-literal types, so a file at `client/routes/blog/[id].tsx` contributes `` `/blog/${string}` `` and a single interpolation still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine\n```\n\nWhen a URL is assembled from several data pieces (or from values TypeScript cannot prove the shape of), it is typed as a plain `string`, and `string` is not assignable to `Href`. That is when you reach for `Toil.href()`, the escape hatch that asserts a runtime string is a valid href:\n\n```tsx\nconst path = `/${product.category}/${product.slug}`; // string\nToil.navigate(Toil.href(path)); // asserted valid\n<Toil.Link href={Toil.href(path)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so use it only when the type-check is genuinely in your way, not to paper over a real typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n## Reading the current location\n\nA handful of hooks let a component read where it is. Each re-reads on every navigation, so a component using one re-renders when the location changes:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.usePathname()` | The current pathname, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |\n| `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |\n\n`useNavigationPending()` is what you wire a top loading bar to. It flips to `true` when a navigation begins and back to `false` once the new route commits:\n\n```tsx\n// client/components/ProgressBar.tsx\nexport default function ProgressBar() {\n const pending = Toil.useNavigationPending();\n return <div className=\"progress\" data-active={pending} />;\n}\n```\n\n`useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:\n\n```tsx\nexport default function Results() {\n const params = Toil.useSearchParams();\n const q = params.get('q') ?? '';\n return <p>Results for {q}</p>;\n}\n```\n\n## Prefetching\n\ntoiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:\n\n- **Hover / focus intent.** When you hover or focus a link that points at a known internal route, toiljs warms both its route chunk and its loader data, so the actual click can commit right away.\n- **Viewport intent.** As a link scrolls into view (or within about 200px of it), its route chunk is warmed. Links added later by client navigation are picked up automatically.\n\nPrefetching is best-effort and cheap: each route loads at most once, a failed prefetch is forgotten (so the real navigation can retry and surface the error), and new-tab / download / opted-out links are skipped. It is also **skipped entirely when the browser signals data-saver** (or reports a 2g-class connection), so you never spend a metered user's bandwidth on speculation.\n\nOpt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:\n\n```tsx\n<Toil.Link href=\"/huge-report\" prefetch={false}>Annual report</Toil.Link>\n```\n\nYou can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:\n\n```tsx\n<button\n onPointerEnter={() => Toil.prefetch('/dashboard')}\n onClick={() => Toil.navigate('/dashboard')}>\n Open dashboard\n</button>\n```\n\n## Scroll restoration\n\ntoiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):\n\n- **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.\n- **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.\n- **A `#hash` target** scrolls that element into view instead of jumping to the top.\n\nTo keep the current scroll on a push navigation, set `scroll={false}` on the `Link` (or `{ scroll: false }` in `NavigateOptions`). This is handy for tab bars or filters that change the URL but should not yank the viewport:\n\n```tsx\n<Toil.Link href=\"/settings/billing\" scroll={false}>Billing</Toil.Link>\n```\n\n```tsx\nnavigate('/settings/billing', { scroll: false });\n```\n\n## Animated transitions\n\nTwo optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:\n\n```ts\n// toil.config.ts\nexport default {\n client: {\n viewTransitions: true, // browser View Transitions API (a crossfade between pages)\n transitions: true, // React transition: keep the old page visible while the next loads\n },\n};\n```\n\n`viewTransitions` uses the browser's View Transitions API to crossfade the old and new page (and it respects `prefers-reduced-motion`, animating nothing for users who ask for less motion). `transitions` wraps each navigation in a React transition, keeping the current page on screen while the next route's loader runs, instead of showing its `loading.tsx` right away (smoother, but you trade away the immediate loading state).\n\nConfig is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:\n\n```tsx\nToil.setViewTransitions(true);\nToil.setTransitions(false);\n```\n\nYou rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.\n\n## Types\n\nAlmost everything on this page is a value on the global `Toil` object, so you call it with no import (`Toil.Link`, `Toil.navigate`, `Toil.useRouter`, and so on). A few of the **types**, though, are not namespaced under `Toil`, so if you want to annotate a prop or a variable you import them from `toiljs/client`:\n\n```tsx\nimport type {\n LinkProps,\n NavLinkProps,\n NavLinkState,\n NavigateOptions,\n RouterInstance,\n} from 'toiljs/client';\n```\n\nFor example, a component that forwards `Link` props, or a helper typed against the router handle:\n\n```tsx\nimport type { RouterInstance } from 'toiljs/client';\n\nfunction logoutThenGo(router: RouterInstance) {\n router.replace('/login');\n}\n```\n\n## Related\n\n- [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.\n\n### Guarding unsaved changes\n\nListen for the cancelable `window` event `toil-before-navigation` and call\n`event.preventDefault()` to keep the current route mounted. Its detail contains\n`href` and `kind` (`navigate` or `history`). Guards run before application navigation;\na cancelled Back/Forward navigation restores the current indexed history entry.\nOnly router-managed history entries can be restored this way. Use a separate\n`beforeunload` listener for document reloads, tab closure, and external navigation.\n",
37
37
  "frontend/README.md": "# Frontend\n\nYour toiljs frontend is a React app with file-based routing that runs in the browser, and can also be rendered ahead of time on the server for a fast first paint and good SEO.\n\nIf you have written React before, everything here is familiar React: components, hooks, JSX. toiljs adds the parts a plain React app makes you wire up yourself: a router, data loading, `<head>` and SEO management, an image component, and a typed client for calling your backend. You get them for free and you do not import most of them, they live on a global called `Toil`.\n\n## What \"frontend\" means here\n\nA toiljs project has three top-level folders. The frontend is the first two:\n\n- **`client/`** is your React app: pages, components, and styles. This is what runs in the user's browser.\n- **`shared/`** is a typed bridge that toiljs generates for you. It lets the browser call your backend with full type safety (see [Fetching data](./data-fetching.md)).\n- **`server/`** is your backend. It compiles to WebAssembly and runs on the edge. That is a separate section (see [Backend](../backend/README.md)).\n\nInside `client/`, the important pieces are:\n\n| Path | What it is |\n| --- | --- |\n| `client/toil.tsx` | The entry file. It imports your routes and global styles and mounts the app. |\n| `client/routes/` | Your pages. One file per URL (file-based routing). See [Routing](./routing.md). |\n| `client/layout.tsx` | The root layout that wraps every page (a header, a footer, and so on). |\n| `client/components/` | Your own reusable React components. |\n| `client/styles/` | Your CSS. See [Styling](./styling.md). |\n| `client/public/` | Static files served as-is (`favicon.ico`, images, `robots.txt`). |\n\nThe entry file is tiny, and you rarely touch it:\n\n```tsx\n// client/toil.tsx\nimport { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\nimport './styles/main.css';\n\nToil.mount(routes, layout, notFound, globalError, slots);\n```\n\n`toiljs/routes` is a virtual module: the compiler scans `client/routes/` and generates the route table for you, so you never hand-maintain a list of pages. Adding a file under `client/routes/` adds a page.\n\n## SPA, but with a server head start\n\nBy default a toiljs app is a single-page application (SPA). The word \"SPA\" means the browser loads one HTML shell, then JavaScript builds every page and swaps between them without a full reload. That makes navigation instant, but it has a classic weakness: the very first load shows a blank page until the JavaScript runs, and simple crawlers that do not run JavaScript see nothing useful.\n\ntoiljs closes that gap in two ways, both optional and both explained in [Rendering and SSR](./rendering.md):\n\n1. **Build-time prerendering.** At build time toiljs bakes each route's `<head>` (title, description, Open Graph, and so on) into real HTML, so crawlers and link-preview bots see correct metadata even without running your code.\n2. **Edge server-side rendering (SSR).** For routes you opt in with `export const ssr = true`, the edge fills in real first-paint HTML for the page body, then the browser \"hydrates\" it (attaches React to the already-drawn markup instead of redrawing it).\n\nHere is the full life of one request, from a cold link click to a warm client-side app:\n\n```mermaid\nsequenceDiagram\n participant U as User's browser\n participant E as Dacely edge\n U->>E: GET /some-page\n E-->>U: HTML (baked head + first-paint body if ssr=true)\n Note over U: First paint appears immediately\n U->>E: fetch the JS bundle + route chunk\n Note over U: React \"hydrates\": attaches to the existing HTML\n Note over U: Page is now interactive\n U->>U: Click a link -> client-side navigation (no reload)\n Note over U: Only the new route's data + chunk are fetched\n```\n\nThe key idea: the server gets you a correct first paint fast, and from then on the app runs entirely in the browser, fetching only small route chunks and data as you navigate.\n\n## The `Toil` global\n\nMost of the client API lives on a global object called `Toil`, so route files need no imports for the common things. A few examples you will meet across these pages:\n\n```tsx\nToil.Link // a client-side navigation link\nToil.NavLink // a Link that knows when it is \"active\"\nToil.useParams() // read dynamic URL params, e.g. { id } for /blog/[id]\nToil.useLoaderData // read data your route's loader fetched\nToil.Image // an <img> that avoids layout shift and lazy-loads\nToil.useHead // set the <title> and <meta> tags\nServer.REST.* // the typed fetch client for your backend\n```\n\n`Server` is also global (it is the typed backend client, see [Fetching data](./data-fetching.md)). Everything on `Toil` is fully typed: your editor autocompletes it, because toiljs generates a `toil-env.d.ts` that maps `Toil` onto the `toiljs/client` package.\n\nThe same fast data utilities your backend uses are available in client code too, as bare globals with no import: `FastMap` and `FastSet` (high-performance map and set collections), and `DataWriter` / `DataReader` (a compact binary codec for encoding and decoding buffers). They are handed to you the same way `Toil` and `Server` are, so you can write `new DataWriter()` straight in a component. See [Data types](../backend/data.md) for the codec and when to reach for it.\n\n## The frontend pages\n\nRead them in roughly this order:\n\n- **[Routing](./routing.md)**: turn files into URLs. Index, nested, and dynamic pages; layouts and templates.\n- **[Navigation](./navigation.md)**: move between pages. Links and active state, navigating in code, typed hrefs, prefetching, scroll restoration, and animated transitions.\n- **[Components](./components.md)**: use your own React components, plus the toiljs primitives (`Image`, `Script`, `Form`, `Slot`, `Head`, and the SSR markers).\n- **[Rendering and SSR](./rendering.md)**: what renders on the server versus in the browser, how hydration works, and the current SSR limitations.\n- **[Styling](./styling.md)**: plain CSS, preprocessors (Sass / Less / Stylus), and Tailwind.\n- **[Images](./images.md)**: the `Toil.Image` component, automatic blur placeholders, and how it stops layout shift.\n- **[Metadata and SEO](./metadata.md)**: set the page title, description, and social-share tags per route.\n- **[Fetching data](./data-fetching.md)**: call your backend with the generated typed clients, submit forms, and read who is logged in.\n- **[Scripts](./scripts.md)**: load external or inline `<script>` tags with a loading strategy, using `Toil.Script`.\n- **[Search](./search.md)**: the built-in, statically-baked page search and command palette (`usePageSearch`).\n- **[The Toil global (reference)](./toil-global.md)**: a complete, grouped list of everything on the `Toil` object.\n\n## Related\n\n- [Getting started](../getting-started/README.md): install toiljs and create a project.\n- [Project structure](../getting-started/project-structure.md): the full folder layout.\n- [Backend overview](../backend/README.md): the `server/` side your frontend talks to.\n- [The CLI](../cli/README.md): `toiljs dev`, `toiljs build`, and every flag.\n",
38
38
  "frontend/rendering.md": "# Rendering and SSR\n\nThis page explains where your pages are built: in the browser, ahead of time at build, or on the server for each request. Getting this right is what makes a page paint fast and rank well.\n\n## The three ways a page can render\n\nA toiljs page can reach the user in three ways. You mostly get all three for free; the only one you opt into per page is edge SSR.\n\n| Mode | Who builds the HTML | When | Good for |\n| --- | --- | --- | --- |\n| **Client rendering** | The browser, from JavaScript | On every visit | Interactive, per-user pages (a dashboard). The default. |\n| **Build-time prerender** | The build, once | When you run `toiljs build` | Baking each route's `<head>` (SEO) into real HTML. Automatic. |\n| **Edge SSR** | The edge server, per request | When you set `ssr = true` | A real first-paint page body plus SEO, for landing and content pages. |\n\nLet us define the two words that trip people up:\n\n- **Rendering** means turning your React components into HTML.\n- **Hydration** means React attaching to HTML that already exists on the page (from the server) instead of throwing it away and redrawing it. Hydration is what makes a server-rendered page interactive without a flash.\n\n## Client rendering (the default)\n\nBy default, toiljs ships a small HTML shell with an empty `<div id=\"root\">`, plus your JavaScript. The browser downloads the JS, React runs, and it builds the page into `#root`. From then on, navigating between pages is pure JavaScript: only the next route's small code chunk and its data are fetched, and the page swaps in place with no reload.\n\nThis is fast to navigate and simple to reason about. Its one weakness is the *first* paint: until the JavaScript runs, `#root` is empty. For an app behind a login (a dashboard) that is fine, nobody is trying to index it. For a public landing page, you usually want one of the two server-assisted modes below.\n\n## Build-time prerender (automatic SEO)\n\nEvery time you build, toiljs renders each static route once and bakes its resolved `<head>` (title, description, canonical link, Open Graph tags, and so on) into that route's HTML file. This happens for all routes with no extra work from you, and it is driven by the `metadata` you export from a route plus the site-wide `seo` config (see [Metadata and SEO](./metadata.md)).\n\nThe payoff: a crawler or a link-preview bot (Slack, Discord, iMessage) that fetches your page sees correct tags immediately, even though it does not run your JavaScript. \"View source\" on a built page shows the real title and meta tags, not an empty shell.\n\nIn production, `toiljs build` writes one prerendered HTML file per route (for example `about/index.html`), and the production static server (`npm start`) serves each route its own prerendered file rather than a single shared shell. That is how each page gets its own metadata in the raw HTML.\n\nBuild-time prerender covers the `<head>`. It does not, by itself, fill in the page *body*: for a client-rendered route the body is still built by React in the browser. To get real first-paint body HTML, opt the route into edge SSR.\n\n### Prerendering dynamic routes with `generateStaticParams`\n\nBuild-time prerender bakes a `<head>` for every **static** route on its own. A **dynamic** route (`client/routes/blog/[id].tsx`, one file that serves `/blog/1`, `/blog/2`, and so on) has no single URL to prerender, so by default it gets none of that per-URL HTML. If you know the concrete URLs ahead of time (a fixed set of blog posts, product pages, or docs), you can opt the route into **static site generation (SSG)**, which means the build renders one HTML file per known URL: it enumerates each URL and writes a real `<url>/index.html` with that page's resolved metadata, plus a `sitemap.xml` entry. This is the toiljs analog of Next.js `generateStaticParams`.\n\nYou opt in by exporting `generateStaticParams` from the dynamic route. It returns one object per URL, keyed by the route's param names:\n\n```tsx\n// client/routes/blog/[id].tsx\n\n// One entry per URL to prerender. `id` matches the [id] segment.\nexport const generateStaticParams: Toil.GenerateStaticParams = () => {\n return [{ id: '1' }, { id: '2' }, { id: '3' }];\n};\n\n// The build runs this once per URL, so each baked page gets its own <head>.\nexport const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({\n title: `Blog post ${params.id}`,\n description: `Reading blog post ${params.id}.`,\n});\n\nexport default function BlogPost() {\n const { id } = Toil.useParams();\n return <h1>Blog post {id}</h1>;\n}\n```\n\nAt build, that writes `blog/1/index.html`, `blog/2/index.html`, and `blog/3/index.html`, each with its own title and description in the raw HTML (so a crawler that does not run JavaScript sees them), and all three land in `sitemap.xml`. The param values also feed the route's `loader`, so per-URL metadata can depend on real data.\n\n`generateStaticParams` is async-friendly (return a promise if you fetch the id list first) and completely opt-in: a dynamic route without it is untouched, and the whole pass is skipped when your project has no `seo` config. A catch-all segment (`[...slug]`) takes an array value: `{ slug: ['2024', 'hello'] }` fills the URL as `2024/hello`.\n\n> Two things this is **not**: it is build-time prerender of the `<head>`, not edge SSR of the body. To also serve real body HTML for these URLs on first paint, add `export const ssr = true` as well (see below). And to make a dynamic route show up in the on-site [search index](./search.md) even though its title is dynamic, export `searchHints` (covered there), which is separate from `generateStaticParams`.\n\n## Edge SSR (`ssr = true`)\n\nFor a route where you want the body content visible on first paint (a marketing page, an article), add one line:\n\n```tsx\n// client/routes/index.tsx\nexport const ssr = true;\n\nexport default function Home() {\n return (\n <section className=\"hero\">\n <h1>Welcome</h1>\n </section>\n );\n}\n```\n\nNow the Dacely edge sends a real, filled-in first paint for that page, and the browser hydrates it. The user sees content immediately, and React takes over without redrawing anything.\n\n### How it works, in brief\n\ntoiljs does something clever to keep SSR cheap. It does not re-run React on the server for every request. Instead, at build time it renders the page once into a **template**: the static HTML with the dynamic bits punched out into named holes. Then, per request, your compiled backend fills only the hole values (a small list of \"slot 3 = this text\"), and the edge splices those values into the pre-baked template. The result is real first-paint HTML produced about as fast as serving a static file.\n\n```mermaid\nsequenceDiagram\n participant B as Build\n participant Edge as Dacely edge\n participant U as Browser\n B->>Edge: prebuilt template (HTML with holes) plus a coherence hash\n U->>Edge: GET / (ssr route)\n Edge->>Edge: run the wasm render to get a small hole-values list\n Edge->>Edge: splice values into the template\n Edge-->>U: real first-paint HTML\n Note over U: Content is visible immediately\n U->>Edge: fetch JS plus route chunk\n Note over U: React hydrates and attaches to the existing HTML\n Note over U: Page is interactive with no redraw or flash\n```\n\nFor SSR to hydrate cleanly, the HTML the server produced and the HTML the browser would produce must match byte-for-byte. toiljs guarantees this by escaping hole values exactly as React does and by carrying a hash that ties the running backend to the exact template it was built against. Authoring the server side of an SSR route (the hole markers in the page and the matching `render` function in `server/`) is a deeper topic that lives with the [backend](../backend/README.md). For most pages you only need `export const ssr = true` and to keep the page \"SSR-safe\" (below).\n\n### Authoring an SSR route\n\n`export const ssr = true` is all you need for a page whose body is fully static. The moment the body has a **dynamic bit** (a value that changes per request: a name from the URL, a list from a loader, a chunk of user HTML), you have to tell the build *where* that dynamic bit lives, so it can be punched out into a hole. You do that by wrapping the dynamic value in a **hole marker**.\n\nWhy is this necessary? The build renders your page once into a template. It cannot guess which `{expression}` in your JSX is a per-request value and which is a constant, so it does not try: any dynamic content you leave unwrapped gets frozen into the template as whatever value it happened to have during that one build render, and it will never update per request. The markers are the explicit \"fill this in later\" signal.\n\nThe markers live on the `Toil` global (so you do not import them), and they are **transparent in the browser**: `<Toil.Hole>` just renders its children, `<Toil.Repeat>` just maps its rows. They only behave differently during the build render, so your client-side app runs exactly as written.\n\n| Marker | Use it for | Shape |\n| --- | --- | --- |\n| `Toil.Hole` | A single dynamic **text** value. | JSX element with `id` + children |\n| `Toil.Repeat` | A **list**: a row template repeated over an `each` array. | JSX element with `id` + `each` + a render function |\n| `Toil.RawHtml` | A block of **pre-rendered HTML** you trust (Markdown you rendered, say). | JSX element with `id` + `html` (+ optional `as`) |\n| `Toil.attr` | A dynamic value in **attribute position** (an `href`, a `class`). | a **function** you call inside the attribute |\n| `Toil.Island` | Content that must render **only in the browser** (the escape hatch). | JSX element with children |\n\n`Toil.attr` is a function rather than an element because an attribute is not a child node, so it cannot be a JSX element. You call it right where the value goes.\n\nHere is a full SSR route: a blog post whose title, body HTML, tag list, and author link all come from the route's `loader`.\n\n```tsx\n// client/routes/blog/[id].tsx\nexport const ssr = true;\n\n// Runs on the server for the first paint, then again on the client to reproduce\n// the same data so hydration matches (see \"Keeping a route SSR-safe\" below).\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n // Illustrative shape: { title, bodyHtml, tags, authorUrl }.\n return Server.REST.blog.get({ params: { id: params.id } });\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return (\n <article>\n <h1>\n <Toil.Hole id=\"title\">{post.title}</Toil.Hole>\n </h1>\n\n {/* A dynamic attribute: call attr() in attribute position. */}\n <a href={Toil.attr('authorUrl', post.authorUrl)}>By the author</a>\n\n {/* A block of trusted, pre-rendered HTML. */}\n <Toil.RawHtml id=\"body\" html={post.bodyHtml} />\n\n {/* A list: one row template, stamped once per item on the server. */}\n <ul>\n <Toil.Repeat id=\"tags\" each={post.tags}>\n {(tag) => <li key={tag}>{tag}</li>}\n </Toil.Repeat>\n </ul>\n </article>\n );\n}\n```\n\nA few rules that keep the template valid:\n\n- **Every marker needs a stable `id`**: a short name unique within the page. The build maps each id to a numbered slot, so keep the ids constant across builds.\n- **`Toil.Repeat` needs at least one row at build time.** It captures that first row as the sub-template for every row, so the build render must see sample data with one or more items (an empty `each` gives it nothing to capture).\n- **`Toil.RawHtml` renders inside a wrapper element** (a `<div>` by default; pass `as=\"section\"` to change the tag), and you own sanitising that HTML, exactly like React's `dangerouslySetInnerHTML`.\n\nAnything that genuinely cannot run on the server (it reads `window`, calls `Date.now()`, or depends on the live URL) goes inside a `Toil.Island`, which renders nothing on the server and reveals its children only after hydration:\n\n```tsx\n<Toil.Island>\n <LiveClock />\n</Toil.Island>\n```\n\nThis is the flow from your marked-up JSX to the first-paint HTML:\n\n```mermaid\nflowchart TD\n A[\"Your SSR route JSX<br/>dynamic bits wrapped in Toil.Hole / Repeat / RawHtml / attr\"] --> B[\"Build render (once, in sentinel mode)\"]\n B --> C[\".tmpl: static HTML with numbered holes\"]\n B --> D[\".slots: which hole is text / raw / attr / repeat\"]\n B --> E[\"Slot enum + coherence HASH\"]\n C --> F[\"Deployed to the Dacely edge\"]\n E --> G[\"Compiled backend render(req)\"]\n G -->|\"per request: fill only the hole values\"| F\n D --> G\n F -->|\"splice values into the template\"| H[\"First-paint HTML to the browser\"]\n```\n\n#### Advanced: hand-writing the server `render`\n\nYou almost never do this. The compiler generates the server `render(req)` for an SSR route from the JSX above, so the hole ids line up automatically. But the server side is backed by a plain, hand-writable API for the rare case you need full control (an unusual template, or a value the compiler cannot derive). Your `render` returns a `SlotValues` object, filled with:\n\n- `setText(slot, value)`: a text hole (React-escaped for you).\n- `setRaw(slot, html)`: a raw-HTML hole (you own sanitising).\n- `setAttr(slot, value)`: an attribute hole.\n- `setRepeat(slot, rows)`: a repeat region, with the rows assembled through an `HtmlBuilder` (chain `.raw(...)`, `.text(...)`, `.attr(...)`).\n- `setHeader(name, value)`, `setTitle(title)`, and `setStatus(code)`: response headers, a per-request `<title>`, and the status code.\n\n`SlotValues` and `HtmlBuilder` live in `server/runtime/ssr/slots.ts`. The `setTitle` helper is the supported way to give an SSR page a data-driven `<title>` (a blog post's real title from its loader), overriding the one baked into the template. This is server (backend) code, so it belongs with the [backend](../backend/README.md).\n\n### Keeping a route SSR-safe\n\nServer rendering happens where there is no browser: no `window`, no `document`, no mouse. So an SSR route (and every layout above it) must render without touching browser-only APIs during that first render. Anything that must run only in the browser (reading `window`, using `Date.now()`, or router hooks that need the live URL) goes inside an **island**, a marker that renders nothing on the server and appears only after hydration.\n\nIf a route or one of its layouts throws while rendering on the server, toiljs does not ship a broken page. It **skips SSR for that route at build with a warning** and falls back to plain client rendering. So adding `ssr = true` is always safe: worst case you get client rendering plus a build warning telling you what to move into an island.\n\n### Suspense markers and self-healing hydration\n\nUnder the hood the client wraps each route and layout in React `Suspense` boundaries that line up with what the server emitted, so hydration matches. If hydration ever does mismatch (the server HTML and the client's idea of the page disagree), React does the safe thing: it discards the server markup for that part and re-renders it on the client. You get a correct page either way. The cost of a mismatch is a small flash and some wasted work, not a broken page, which is why the guidance above (keep it SSR-safe, put browser-only bits in islands) is about smoothness, not correctness.\n\n## Known SSR limitations\n\nBe aware of these honest gaps as of today:\n\n- **`template.tsx` is not server-rendered.** A `template.tsx` wrapper (the re-mounting cousin of a layout) is not part of the SSR output. A route under one still works: hydration self-heals to client rendering for that part.\n- **Parallel slots (`@slot`) are not server-rendered.** Slot content (including intercepted modals) renders on the client after hydration, not in the first paint. Since slots are typically modals and overlays that appear on interaction, this is rarely a problem.\n- **Islands have no first paint or SEO.** That is by design: an island is your \"client only\" escape hatch, so anything inside it is intentionally absent from the server HTML and from what crawlers see.\n- **The client loader must reproduce the server's data.** For a hole whose value comes from the request (a query param), the route's client `loader` has to derive the same value the server used, or hydration will re-render that part. If the client cannot reproduce a value, put that content in an island.\n\n## Which mode should I use?\n\n- **Interactive, per-user page** (dashboard, settings): client rendering. Do nothing.\n- **Public page that needs correct link previews and titles**: you already have build-time prerender. Do nothing extra.\n- **Public page that should also show its content instantly on first load** (landing page, blog post, docs): add `export const ssr = true` and keep it SSR-safe.\n\n## Related\n\n- [Backend overview](../backend/README.md): where the server-side `render` for an SSR route lives.\n- [Metadata and SEO](./metadata.md): what gets baked into the `<head>`.\n- [Routing](./routing.md): layouts, templates, and slots.\n- [Fetching data](./data-fetching.md): loaders and how their data seeds hydration.\n",
39
39
  "frontend/routing.md": "# Routing\n\ntoiljs uses file-based routing: every file under `client/routes/` becomes a page, and its path on disk becomes its URL. There is no route config to maintain, you create a file and the route exists.\n\nThis page is about **frontend** (browser) routing: the pages a user visits and how they navigate between them. Your **backend** HTTP endpoints use a different, decorator-based system (see [HTTP routes](../backend/rest.md)).\n\n## The basic idea\n\nDrop a React component at `client/routes/<something>.tsx`, `export default` it, and it is a page. The default export is the component that renders at that URL.\n\n```tsx\n// client/routes/about.tsx -> /about\nexport default function About() {\n return (\n <main>\n <h1>About us</h1>\n </main>\n );\n}\n```\n\nRun `toiljs dev`, open `/about`, and it is there. Nothing else to register.\n\n## How a file path becomes a URL\n\nThe compiler scans `client/routes/` and turns each file into a URL pattern with a small set of rules. Here is the whole mapping in one table:\n\n| File under `client/routes/` | URL | What it is |\n| --- | --- | --- |\n| `index.tsx` | `/` | The home page. |\n| `about.tsx` | `/about` | A static page. |\n| `blog/index.tsx` | `/blog` | An index inside a folder. |\n| `blog/[id].tsx` | `/blog/:id` | A **dynamic** page (one param). |\n| `docs/[...slug].tsx` | `/docs/*` | A **catch-all** (one or more segments). |\n| `files/[[...slug]].tsx` | `/files` and `/files/*` | An **optional** catch-all (zero or more). |\n| `(marketing)/pricing.tsx` | `/pricing` | A **route group**: parens add no URL segment. |\n| `@modal/photo/[id].tsx` | (a parallel slot, see below) | A named **slot**. |\n\nThe rules, spelled out:\n\n- **`index`** means \"the folder itself\", so `blog/index.tsx` is `/blog`, and the top `index.tsx` is `/`.\n- **Square brackets** mark a dynamic segment. `[id]` captures one path segment into a param named `id`. You read it with `Toil.useParams()`.\n- **`[...name]`** is a catch-all: it captures the rest of the path (one segment or more) as a single `/`-joined string.\n- **`[[...name]]`** is an *optional* catch-all: like `[...name]`, but it also matches the bare parent URL with nothing after it (the param is then absent).\n- **Parentheses** like `(marketing)` create a **route group**: a folder that organizes files without adding anything to the URL. Handy for grouping pages that share a layout.\n\n```mermaid\nflowchart LR\n A[\"client/routes/blog/[id].tsx\"] -->|scan| B[\"/blog/:id\"]\n B -->|visit /blog/42| C[\"params = { id: '42' }\"]\n C -->|Toil.useParams| D[\"render Blog post 42\"]\n```\n\n### Dynamic pages: reading the param\n\nA file named with brackets gets its captured values from `Toil.useParams()`:\n\n```tsx\n// client/routes/blog/[id].tsx -> /blog/:id\nexport default function BlogPost() {\n const { id } = Toil.useParams();\n return (\n <main>\n <h1>Blog post {id}</h1>\n </main>\n );\n}\n```\n\nVisiting `/blog/42` renders \"Blog post 42\". Param values are URL-decoded for you.\n\nFor a catch-all, the param is the whole tail joined with slashes:\n\n```tsx\n// client/routes/docs/[...slug].tsx -> /docs/*slug\nexport default function Docs() {\n const { slug } = Toil.useParams();\n // /docs/getting-started/install -> slug === \"getting-started/install\"\n return <main>{slug}</main>;\n}\n```\n\nAn optional catch-all (`[[...slug]]`) matches the bare parent too, so `slug` can be empty:\n\n```tsx\n// client/routes/files/[[...slug]].tsx -> matches /files AND /files/a/b\nexport default function Files() {\n const { slug } = Toil.useParams();\n // \"/files\" -> slug is undefined; \"/files/a/b\" -> slug === \"a/b\"\n return <main>{slug ?? '(the base /files page)'}</main>;\n}\n```\n\n### When two routes could match\n\nIf a URL could match more than one pattern, toiljs picks the most specific one. Static segments win over dynamic (`:id`) segments, which win over catch-alls (`*slug`), and deeper routes win over shallower ones. So `/blog/new` prefers a literal `blog/new.tsx` over `blog/[id].tsx` if both exist. You do not configure this, it just does the intuitive thing.\n\n## Special files\n\nSome filenames are not pages, they are helpers that wrap or replace pages. They live alongside your routes and are never matched as a URL:\n\n| Filename | Role |\n| --- | --- |\n| `layout.tsx` | Wraps the pages in its folder (and below). **Persists** across navigation. |\n| `template.tsx` | Like a layout, but **re-mounts** on every navigation within it. |\n| `loading.tsx` | Shown while the page (and its data) is loading. |\n| `error.tsx` | Shown when the page throws (an error boundary). |\n| `global-error.tsx` | The last-resort error boundary, wraps even the root layout. |\n| `404.tsx` (or `not-found.tsx`) | Shown when no route matches. |\n\n### Layouts\n\nA `layout.tsx` receives the page (or nested layout) as `children` and renders around it. The root `client/layout.tsx` wraps every page. It is the natural home for your header, footer, and site-wide `<head>` defaults:\n\n```tsx\n// client/layout.tsx\nimport type { ReactNode } from 'react';\nimport Header from './components/Header';\nimport Footer from './components/Footer';\n\nexport default function Layout({ children }: { children?: ReactNode }) {\n return (\n <div className=\"app\">\n <Header />\n <main className=\"content\">{children}</main>\n <Footer />\n </div>\n );\n}\n```\n\nLayouts nest by folder. A `client/routes/dashboard/layout.tsx` wraps every page under `/dashboard`, inside the root layout. Because a layout **persists** across navigations, state inside it (a sidebar's open/closed flag, a scroll position) survives when you move between its child pages.\n\n### Templates vs layouts\n\nA `template.tsx` looks like a layout but does the opposite on navigation: it **re-mounts** every time you move to a new page within it. Use a layout when state should persist (a nav sidebar), and a template when it should reset (an enter animation that should replay, or a counter that should start fresh per page).\n\n### Loading and error states\n\nPut a `loading.tsx` next to a page (or in a folder) and it shows automatically while that page's chunk and its `loader` data are still resolving:\n\n```tsx\n// client/routes/dashboard/loading.tsx\nexport default function Loading() {\n return <p>Loading dashboard...</p>;\n}\n```\n\nPut an `error.tsx` there and it catches any error the page throws, showing a fallback instead of a blank screen. `global-error.tsx` sits outside the root layout, so it catches errors thrown by the layout itself.\n\n## Route groups: shared layout, no URL change\n\nWrap a folder name in parentheses to group files without changing their URLs. This is the trick for \"these three pages share one layout, but I do not want a `/legal` prefix\":\n\n```\nclient/routes/\n (legal)/\n layout.tsx -> wraps both pages below\n privacy.tsx -> /privacy (NOT /legal/privacy)\n terms.tsx -> /terms\n```\n\n## Parallel slots (`@slot`) and intercepting routes\n\nThis is an advanced feature; skip it until you need a modal that keeps the page behind it alive.\n\nA folder starting with `@`, like `@modal`, is a **named slot**. It is a whole second route tree that matches the current URL *independently* of the main page, and renders wherever you place a `<Toil.Slot>`:\n\n```tsx\n// client/routes/gallery/layout.tsx\nimport type { ReactNode } from 'react';\n\nexport default function GalleryLayout({ children }: { children?: ReactNode }) {\n return (\n <div>\n {children}\n <Toil.Slot name=\"modal\" /> {/* renders the @modal slot for this URL */}\n </div>\n );\n}\n```\n\nThe `@` folder adds nothing to the URL; it just says \"these routes fill the slot named `modal`\". A slot with no match renders nothing (or a `fallback` you pass).\n\nAn **intercepting route** is a slot route whose folder name starts with `(.)`, `(..)`, or `(...)`. It hijacks a *soft* (in-app) navigation to another URL and renders that URL's content inside the slot instead, while the main page stays mounted behind it. That is exactly how a \"click a photo, see it in a modal over the gallery\" pattern works, where reloading the page (a hard load) shows the full photo page instead:\n\n```\nclient/routes/gallery/\n index.tsx -> /gallery\n photo/[id].tsx -> /gallery/photo/:id (full page, on hard load)\n @modal/(.)photo/[id].tsx -> fills @modal on a soft click to that URL\n```\n\nThe `(.)` markers mean: `(.)` same level, `(..)` up one level, `(...)` from the routes root. This tells the interceptor which real URL it is standing in for.\n\n## Links and navigation\n\nThis is a quick tour. For the full treatment, programmatic navigation, typed hrefs, prefetching, scroll restoration, and animated transitions, see [Navigation](./navigation.md).\n\n### `Toil.Link`\n\nUse `Toil.Link` instead of a plain `<a>` for in-app links. It navigates client-side (no full page reload), and prefetches the target route's code on hover or focus so the click feels instant:\n\n```tsx\n<Toil.Link href=\"/about\">About</Toil.Link>\n```\n\n`Link` accepts every normal anchor attribute (`className`, `target`, `rel`, `download`, and so on), plus a few toiljs controls:\n\n| Prop | Default | What it does |\n| --- | --- | --- |\n| `replace` | `false` | Replace the current history entry instead of pushing a new one. |\n| `scroll` | `true` | Scroll to the top after navigating. |\n| `prefetch` | `true` | Prefetch the route on hover/focus. Set `false` to opt out. |\n\n`Link` is smart about when *not* to intercept: external URLs, `target=\"_blank\"`, `download`, `#hash` links, and modified clicks (Ctrl/Cmd/middle-click) all fall through to normal browser behavior. The `href` is typed to your project's real routes, so a typo is a compile error.\n\n### `Toil.NavLink` and active state\n\n`NavLink` is a `Link` that knows whether it points at the current page. When active it adds the class `active` (configurable) and `aria-current=\"page\"`. This is what you want for a navigation bar that highlights the current section:\n\n```tsx\n<Toil.NavLink href=\"/blog\" activeClassName=\"is-current\">\n Blog\n</Toil.NavLink>\n```\n\nYou can also drive `className`, `style`, or `children` from the active state with a function:\n\n```tsx\n<Toil.NavLink href=\"/blog\" className={({ isActive }) => (isActive ? 'on' : 'off')}>\n Blog\n</Toil.NavLink>\n```\n\nBy default a parent link is active for its sub-paths too (`/blog` is active on `/blog/42`). Pass `end` to require an exact match:\n\n```tsx\n<Toil.NavLink href=\"/\" end>Home</Toil.NavLink>\n```\n\n### Navigating in code\n\nFor navigation that is not a link (after a form submit, a redirect), use the router hook or the free `navigate` function:\n\n```tsx\nexport default function Login() {\n const router = Toil.useRouter();\n const onDone = () => router.push('/dashboard');\n // router.replace(href), router.back(), router.forward(), router.refresh()\n return <button onClick={onDone}>Continue</button>;\n}\n```\n\n`useRouter()` returns a handle with `push`, `replace`, `back`, `forward`, `refresh` (re-run the current page's data loader), `revalidate` (refetch data), and `prefetch`.\n\nIf you just need to jump to a URL and nothing else, `Toil.useNavigate()` returns the bare `navigate(href, options)` function:\n\n```tsx\nexport default function Login() {\n const navigate = Toil.useNavigate();\n return (\n <button onClick={() => navigate('/dashboard', { replace: true })}>\n Continue\n </button>\n );\n}\n```\n\nThe options are `{ replace?: boolean; scroll?: boolean }`, the same two controls `Toil.Link` exposes above. The same function is also available free of any hook as `Toil.navigate(href, options)`, which is handy in code that is not a React component (a plain event handler, a utility module).\n\n### Typed hrefs and the `href()` escape hatch\n\nEvery href you pass to `Toil.Link`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a file called `toil-routes.d.ts` that registers the union of your route paths (it fills in the `Href` type). So a typo is a compile error before you ever run the app:\n\n```tsx\n<Toil.Link href=\"/abuot\">About</Toil.Link>\n// ^ Type error: \"/abuot\" is not assignable to type 'Href'. (No such route.)\n```\n\nDynamic routes show up in that union as template-literal types. A file at `client/routes/blog/[id].tsx` contributes the type `` `/blog/${string}` ``, so a link built with one interpolation usually still checks:\n\n```tsx\n<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine: matches `/blog/${string}`\n```\n\nThe catch is a URL assembled from several data pieces (or one TypeScript cannot prove the shape of): it is typed as a plain `string`, and `string` is not assignable to `Href`, so you get a type error:\n\n```tsx\nconst cat = product.category; // string\nconst slug = product.slug; // string\nnavigate(`/${cat}/${slug}`);\n// ^ Argument of type 'string' is not assignable to parameter of type 'Href'.\n```\n\nThe escape hatch is `Toil.href()`. It takes a `string` and asserts it is a valid href, returning the `Href` type the navigation APIs expect. Use it right at the call site once you are sure the path is a real route:\n\n```tsx\nnavigate(Toil.href(`/${cat}/${slug}`)); // asserted valid\n<Toil.Link href={Toil.href(`/${cat}/${slug}`)}>Open</Toil.Link>;\n```\n\n`href()` is a pure type assertion (it returns the string unchanged), so reach for it only when the type-check is in your way, not to paper over a genuine typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.\n\n### Reading the current location\n\nA handful of hooks let a component read where it is:\n\n| Hook | Returns |\n| --- | --- |\n| `Toil.useParams()` | The dynamic route params, e.g. `{ id }`. |\n| `Toil.usePathname()` | The current path, e.g. `\"/blog/42\"`. |\n| `Toil.useLocation()` | The current path, e.g. `\"/blog/42\"` (an alias of `usePathname()`). |\n| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |\n| `Toil.useNavigationPending()` | `true` while a navigation is in flight (for a loading bar). |\n\n## Loading data for a route\n\nA route file can `export const loader` alongside its component. The loader runs on navigation, in parallel with loading the page's code, and the page reads the result with `Toil.useLoaderData`. This keeps data fetching out of `useEffect` and lets `loading.tsx` show while it runs. Loaders are covered in depth in [Fetching data](./data-fetching.md):\n\n```tsx\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n const post = await Server.REST.blog.get({ params: { id: params.id } });\n return post;\n};\n\nexport default function BlogPost() {\n const post = Toil.useLoaderData<typeof loader>();\n return <article><h1>{post.title}</h1></article>;\n}\n```\n\n## Gotchas\n\n- **`export default` is required.** A route file without a default-exported component is not a page. The special files (`layout`, `loading`, and so on) also use the default export.\n- **Reserved filenames are not pages.** `layout.tsx`, `template.tsx`, `loading.tsx`, `error.tsx`, `global-error.tsx`, `404.tsx`, and `not-found.tsx` are helpers, not routes. Do not name a real page one of these.\n- **Use `Toil.Link`, not `<a>`, for in-app links.** A plain `<a href=\"/about\">` triggers a full page reload, throwing away the SPA speed. Reserve `<a>` for external links.\n- **Params are always strings.** `useParams()` gives you `{ id: \"42\" }`, not a number. Convert if you need a number.\n- **Route groups and slots are invisible in the URL.** `(group)` and `@slot` folders never appear in the address bar. If a URL looks wrong, check for a stray bracket or paren in the file path.\n\n## Related\n\n- [Rendering and SSR](./rendering.md): what renders on the server versus the browser.\n- [Fetching data](./data-fetching.md): loaders, the typed backend clients, and forms.\n- [Metadata and SEO](./metadata.md): set the title and tags per route.\n- [Backend HTTP routes](../backend/rest.md): the separate, decorator-based server routing.\n",
@@ -100,6 +100,7 @@ export async function createViteConfig(cfg) {
100
100
  const tailwind = await tailwindPlugin(cfg.root);
101
101
  const base = {
102
102
  root: cfg.toilDir,
103
+ envDir: cfg.root,
103
104
  base: cfg.base,
104
105
  configFile: false,
105
106
  customLogger: brandedLogger(),
@@ -1,3 +1,4 @@
1
+ import { SERVER_KEM_PUBLIC_KEY } from 'toiljs/client';
1
2
  // Post-quantum auth demo, the full toil PQ-Auth chain (the password never leaves the
2
3
  // tab). REGISTER: the browser blinds the password through the server-keyed OPRF,
3
4
  // stretches the OPRF output with Argon2id into an ML-DSA-44 keypair, and submits
@@ -89,7 +90,7 @@ export default function Pq(): React.JSX.Element {
89
90
  setVerified(null);
90
91
  try {
91
92
  // Resolves only if the server's mutual-auth confirmation tag verified.
92
- await Auth.login(username, password);
93
+ await Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });
93
94
  setNote({
94
95
  kind: 'ok',
95
96
  text: 'logged in: ML-KEM-768 mutual auth verified (the server proved it holds the KEM secret key).'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.117",
4
+ "version": "0.0.118",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {