toiljs 0.0.98 → 0.0.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +15 -0
- package/build/compiler/toil-docs.generated.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/email/console.d.ts +4 -0
- package/build/devserver/email/console.js +199 -2
- package/docs/frontend/data-fetching.md +17 -0
- package/package.json +1 -1
- package/src/compiler/docs.ts +15 -0
- package/src/compiler/toil-docs.generated.ts +1 -1
- package/src/devserver/email/console.ts +231 -17
- package/test/email-console.test.ts +31 -1
|
@@ -28,7 +28,7 @@ export const TOIL_DOCS = {
|
|
|
28
28
|
"database/unique.md": "# Unique\n\nThe **Unique** family enforces that a value is claimed by only one owner across the entire world. It is how you make usernames, email addresses, and URL slugs one-of-a-kind, safely, even when two people try to grab the same one at the same instant.\n\n## What and why\n\nA **Unique collection** maps a `@data` **claim key** (the thing that must be unique, like a username) to a `@data` **owner value** (who or what claimed it, like a user id). At any moment a claim key is either **unclaimed** or **owned by exactly one owner**. The family gives you three operations: look up who owns a key, claim a key, and release it.\n\nReach for Unique whenever a value must be globally singular:\n\n- usernames or handles\n- email addresses\n- URL slugs or workspace names\n- any \"reserve this name for me\" scenario\n\nWhy not just check a Documents record first? Because a check-then-write has a race: two requests can both check \"is `alice` free?\", both see yes, and both create it. Unique closes that gap. The claim is decided at the key's single **home** (see [consistency](./README.md#eventual-consistency-in-plain-words)), where claims are processed one at a time, so exactly one of the two racers wins.\n\nDeclare one by typing a `@collection` as `Unique<ClaimKey, OwnerValue>`:\n\n```ts\n@data\nclass Username {\n name: string = '';\n constructor(name: string = '') { this.name = name; }\n}\n\n@data\nclass OwnerId {\n userId: string = '';\n constructor(userId: string = '') { this.userId = userId; }\n}\n\n@database\nclass AppDb {\n @collection static usernames: Unique<Username, OwnerId>;\n}\n```\n\n## The operations\n\n`K` is the claim-key type, `V` the owner value type.\n\n| Operation | Signature | Returns | Use it to |\n| --- | --- | --- | --- |\n| `lookup` | `lookup(key: K): V \\| null` | the current owner, or `null` if unclaimed | find out who owns a name |\n| `claim` | `claim(key: K, value: V): ClaimResult<V>` | a `ClaimResult` (see below) | try to take a name for an owner |\n| `release` | `release(key: K, value: V): void` | nothing; **traps** if you are not the owner | give a name back |\n\n`lookup` is a read, so it works in any function. `claim` and `release` are writes, so they need an **Action** (a `@post` route or an `@action`); see [Setup](./setup.md#how-access-is-gated-query-action-and-friends).\n\n### `ClaimResult`\n\n`claim` returns a small object that tells you what happened:\n\n```ts\nclass ClaimResult<V> {\n claimed: bool; // true if YOU own the key now\n owner: V | null; // when claimed is false, who owns it instead\n}\n```\n\n- **`claimed == true`**: you own the key. This covers both a fresh claim and an **idempotent re-claim**: if you claim a key you already own (same owner value), you still get `true`, so retrying a claim is safe. `owner` is `null` in this case.\n- **`claimed == false`**: someone else got there first. `owner` is their value, so you can tell the user \"that name is taken.\"\n\n### `lookup`\n\n`lookup` just reads the current owner without changing anything:\n\n```ts\nconst owner = AppDb.usernames.lookup(new Username('alice'));\nif (owner == null) {\n // 'alice' is free\n} else {\n // owner.userId currently holds 'alice'\n}\n```\n\nNote that `lookup` is subject to [eventual consistency](./README.md#eventual-consistency-in-plain-words): a claim made moments ago in another region may not show up in a far-away `lookup` yet. Do not use `lookup` as your uniqueness guard. `lookup` is for display (\"this name is taken by ...\"); the real guarantee comes from `claim`, which is decided at the home and cannot race.\n\n### `claim`\n\n`claim` is the operation that actually enforces uniqueness. You pass the key and the owner value:\n\n```ts\nconst result = AppDb.usernames.claim(new Username('alice'), new OwnerId('u_123'));\nif (result.claimed) {\n // 'alice' is now yours\n} else {\n // taken; result.owner is the current owner\n}\n```\n\nBecause claims are serialized at the key's home, this is race-safe. If two requests anywhere in the world call `claim('alice', ...)` at the same moment, the home applies them in order: the first gets `claimed: true`, the second gets `claimed: false` with the first as `owner`. There is no window where both win.\n\n### `release`\n\n`release` gives a claim back so the key becomes available again. Only the **current owner** may release: you pass both the key and the owner value, and if that value is not the current owner, `release` **traps** (aborts the request). This prevents one user from releasing another user's name.\n\n```ts\nAppDb.usernames.release(new Username('alice'), new OwnerId('u_123'));\n```\n\nRelease when a name is being changed or an account is deleted, so the name returns to the pool.\n\n## The claim / release lifecycle\n\nA claim is a small state machine: unclaimed, owned, and back to unclaimed.\n\n```mermaid\nstateDiagram-v2\n [*] --> Unclaimed\n Unclaimed --> Owned: claim(key, me) -> claimed: true\n Owned --> Owned: claim(key, me) again -> claimed: true (idempotent)\n Owned --> Owned: claim(key, someoneElse) -> claimed: false, owner: me\n Owned --> Unclaimed: release(key, me)\n```\n\nThe key insight: **claiming is the guard, releasing is cleanup.** You claim to reserve, you release to free. A claim that is never released stays owned forever (there is no automatic expiry; if you want time-limited holds, that is what the [Capacity](./capacity.md) family's TTL reservations are for).\n\n## Worked example: reserving a username on signup\n\nThe safe signup pattern is: claim the username first, then create the account, and if creating the account fails, release the claim so the name is not stranded.\n\n```ts\nimport { Response } from 'toiljs/server/runtime';\n\n@data\nclass Username {\n name: string = '';\n constructor(name: string = '') { this.name = name; }\n}\n\n@data\nclass OwnerId {\n userId: string = '';\n constructor(userId: string = '') { this.userId = userId; }\n}\n\n@data\nclass UserId {\n id: string = '';\n constructor(id: string = '') { this.id = id; }\n}\n\n@data\nclass User {\n id: string = '';\n username: string = '';\n}\n\n@data\nclass SignupInput {\n username: string = '';\n userId: string = '';\n}\n\n@database\nclass AppDb {\n @collection static usernames: Unique<Username, OwnerId>;\n @collection static users: Documents<UserId, User>;\n}\n\n@rest('signup')\nclass Signup {\n // POST /signup (Action: may claim and write)\n @post('/')\n public signup(input: SignupInput): Response {\n const owner = new OwnerId(input.userId);\n\n // 1. Claim the username. This is the uniqueness guard.\n const claim = AppDb.usernames.claim(new Username(input.username), owner);\n if (!claim.claimed) {\n return Response.text('username taken', 409);\n }\n\n // 2. Create the account. If this fails, undo the claim so the name is free.\n const user = new User();\n user.id = input.userId;\n user.username = input.username;\n if (!AppDb.users.create(new UserId(input.userId), user)) {\n AppDb.usernames.release(new Username(input.username), owner);\n return Response.text('could not create account', 409);\n }\n\n return Response.json(user.toJSON().toString());\n }\n}\n```\n\nTwo things make this correct:\n\n- **The claim happens before the account write**, so uniqueness is decided up front.\n- **The claim is released if the follow-up write fails**, so a failed signup does not permanently burn a username.\n\nBecause `claim` is idempotent for the same owner, a client that retries the whole request after a network hiccup does not get a false \"taken\" error: the second `claim('alice', u_123)` returns `claimed: true` again.\n\n## Consistency notes\n\n- **`claim` and `release` are strongly consistent at the key's home.** Two callers can never both own the same key; the home serializes claims. This is the whole point of the family.\n- **`lookup` is eventually consistent.** It reads a possibly-nearby copy, so a very recent claim from elsewhere may not appear yet. Never gate uniqueness on `lookup`; gate it on the result of `claim`.\n- **Claims do not expire.** A claim stays until someone releases it. Use [Capacity](./capacity.md) if you need holds that auto-release after a timeout.\n\n## Gotchas\n\n- **Do not use `lookup` as the uniqueness check.** Its answer can be stale. Call `claim` and trust its `claimed` flag.\n- **Release on failure and on account deletion.** A claim you forget to release stays owned forever, quietly blocking that name.\n- **`release` traps for a non-owner.** Pass the correct owner value, or you abort the request. If you are not sure you own it, `lookup` first (for display) but expect `release` to enforce ownership regardless.\n- **The owner value is data you choose.** Store enough in it (a user id, say) to know who holds the claim, so you can display and release it later.\n\n## Related\n\n- [ToilDB overview](./README.md): the seven families and how to choose.\n- [Setup](./setup.md): declaring the collection and which function kinds may claim.\n- [Documents](./documents.md): the account record you create alongside a claim.\n- [Capacity](./capacity.md): time-limited holds that auto-release (a different kind of \"reserve\").\n- [Data types (`@data`)](../backend/data.md): the claim key and owner value.\n",
|
|
29
29
|
"database/views.md": "# Views (materialized views)\n\nA `View` collection is a **precomputed, read-optimized result** that you publish\nonce and read many times. Reads are a single cheap keyed lookup, no scanning, no\nrecomputing.\n\n## What and why\n\nA \"materialized view\" is a fancy name for a simple idea: instead of computing an\nanswer every time someone asks, you compute it **once**, store the finished\nanswer, and just hand it out on each request. \"Materialized\" means the result is\nmade real and saved (as opposed to computed on the fly). \"View\" means it is a\nread-friendly projection of your underlying data.\n\nThink of a leaderboard. Computing \"the top 10 players by score\" means scanning\nevery player and sorting. You do not want to do that on every page load. So you\ncompute it in the background, save the finished top-10 list as a view, and every\npage load just reads that saved list. Fast, cheap, and it does not get slower as\nyou add players.\n\nUse a `View` when:\n\n- A page needs a result that is **expensive to compute** (a scan, a sort, a fold\n over many rows).\n- That result is **read far more often than it changes** (a home page, a feed, a\n leaderboard, a \"latest N\" list, a rendered summary).\n\nThe rule of thumb: if a route wants \"the newest N of something\" or \"the top N of\nsomething\", that is a scan, and scans are barred on the request path. A `View`\nis how you serve that result without scanning per request.\n\n```mermaid\nflowchart LR\n subgraph off[\"Off the request path (@derive)\"]\n S[(\"Source data<br/>events / counters / records\")] --> D[\"@derive: scan + compute\"]\n D --> P[\"publish(key, result)\"]\n end\n P --> V[(\"View\")]\n subgraph on[\"On the request path (@get)\"]\n G[\"view.get(key)\"] --> V\n end\n```\n\n## The type\n\nA `View` has two type parameters: the **key** (which view) and the **value**\n(the precomputed result).\n\n```ts\nView<K, V>\n```\n\n- `K` is the key type: it picks *which* view to read or publish. For a single\n global leaderboard, the key can be a fixed constant like `'main'`.\n- `V` is the value type: the finished result you serve. Both are\n [`@data`](../concepts/types.md) classes.\n\nDeclare it as a `@collection` field inside a `@database` class, alongside the\nsources it is built from:\n\n```ts\n@database\nclass BoardDb {\n @collection static scores: Events<GameKey, ScoreEvent>; // a source\n @collection static board: View<GameKey, Leaderboard>; // the view\n}\n```\n\n## Operations\n\nA `View` has exactly two operations you use directly (plus `require`, a\nconvenience wrapper). Exact signatures:\n\n| Operation | Signature | What it does |\n| --- | --- | --- |\n| `get` | `get(key: K): V \\| null` | Read the published view, or `null` if nothing has been published yet. |\n| `require` | `require(key: K): V` | Like `get`, but traps (aborts the request) if nothing is published. |\n| `publish` | `publish(key: K, value: V): void` | Overwrite the view for `key` with a fresh result. |\n\n### `get`\n\nA plain keyed read. It is cheap and is allowed from **any** handler, including a\nread-only `@get` route.\n\n```ts\nconst board = BoardDb.board.get(new GameKey('main'));\nif (board == null) {\n // Nothing published yet (e.g. brand-new game). Serve an empty default.\n return new Leaderboard();\n}\nreturn board;\n```\n\nAlways handle the `null` case: until a `@derive` (or `@job`) has published at\nleast once, `get` returns `null`.\n\n### `publish`\n\nOverwrites the stored view with a new value. This is how the view gets its\ncontent.\n\n```ts\nBoardDb.board.publish(new GameKey('main'), freshBoard);\n```\n\n`publish` is **restricted**: you may only call it from a\n[`@derive`](../background/derive.md) or a `@job` (a background task), never from a\nrequest handler (`@get`/`@post`/...). The compiler enforces this, and so does the\nedge at runtime. The reason: a view is meant to be maintained off the request\npath from the source of truth, not written ad hoc by whichever request happens to\nrun.\n\n### How `publish` differs from a Documents write\n\nA `View`'s `publish` looks like writing a value, but it is not the same as a\n[Documents](./documents.md) `patch`/`create`. The differences matter:\n\n| | `Documents` write (`create` / `patch`) | `View` `publish` |\n| --- | --- | --- |\n| Who may call it | Any action handler (`@post`, ...) | Only a `@derive` or `@job` |\n| Meaning | The record IS the source of truth | The view is a **copy** derived from a source |\n| Version control | You may version-check (optimistic concurrency) | The host assigns the version; a later publish always wins (last writer wins) |\n| Read path | Standard keyed read | A read-optimized fast path (views are heavily read) |\n| Who owns correctness | You (each writer edits the true value) | The derive (it recomputes from the source, so the view always converges) |\n\nIn short: a Document is the truth; a View is a saved snapshot of a computation\nover the truth. If the view is ever wrong or stale, the derive just recomputes and\nrepublishes it. You never hand-edit a view from a route.\n\n## Automatic maintenance with `@derive`\n\nYou rarely call `publish` by hand. The normal pattern is a\n[`@derive`](../background/derive.md): a method on your `@database` class that\nreads the sources, builds the value, and publishes it. The runtime runs it for\nyou:\n\n- **Right after a write to a source.** When a request writes one of the\n database's source collections (an `append`, a `counter.add`, a record write),\n the database's derives run right after the response is produced. So the view\n reflects the new data on the next read.\n- **On box load.** When the server starts or reloads, the views are rebuilt from\n their sources before the first read is served.\n\nYou never call a derive yourself, and a derive's own `publish` never re-triggers\nit. See [`@derive`](../background/derive.md) for the full rules.\n\n## Worked example: a leaderboard\n\nA game appends score events; a derive folds them into a top-10 list; a route\nserves that list with a single `get`.\n\n```ts\nimport { ScoreEvent } from '../models/ScoreEvent';\nimport { Leaderboard } from '../models/Leaderboard';\nimport { GameKey } from '../models/GameKey';\nimport { NewScore } from '../models/NewScore';\n\n@database\nclass BoardDb {\n // The source of truth: every score, appended as it happens.\n @collection static scores: Events<GameKey, ScoreEvent>;\n // The materialized view: the current top entries, ready to serve.\n @collection static board: View<GameKey, Leaderboard>;\n\n // Off the request path: scan the recent scores, sort, publish the top 10.\n @derive\n rebuild(): void {\n const key = new GameKey('main');\n const recent = BoardDb.scores.latest(key, 500); // a scan, allowed in a derive\n const board = new Leaderboard();\n board.entries = topTen(recent); // your own sort/aggregate\n BoardDb.board.publish(key, board);\n }\n}\n\n@rest('leaderboard')\nclass LeaderboardRoutes {\n // GET reads the precomputed view: one keyed read, no scan.\n @get('/')\n public top(): Leaderboard {\n const board = BoardDb.board.get(new GameKey('main'));\n return board == null ? new Leaderboard() : board;\n }\n\n // POST records a score. The @derive rebuilds `board` right after.\n @post('/')\n public submit(input: NewScore): Leaderboard {\n BoardDb.scores.append(new GameKey('main'), new ScoreEvent(input.player, input.score));\n return new Leaderboard(); // ack; the GET serves the updated board from the view\n }\n}\n```\n\nThe models:\n\n```ts\n@data\nexport class ScoreEvent {\n player: string = '';\n score: u64 = 0;\n}\n\n@data\nexport class Leaderboard {\n entries: ScoreEvent[] = [];\n}\n```\n\nA \"latest N\" view is the same shape: the derive calls `latest(key, N)` and\npublishes the list. See the [Events](./events.md) page for that variant end to\nend.\n\n## Consistency\n\n- **Last writer wins.** The host assigns each `publish` a version, and a later\n publish always supersedes an earlier one. Because a derive recomputes the whole\n view from the source of truth, the view converges to a correct snapshot; you do\n not need to coordinate concurrent publishes.\n- **A view can be briefly stale.** ToilDB is worldwide, and a view is published\n at its home then copied to other regions in the background (asynchronous\n replication). A read from a far region can lag the newest publish by a moment.\n This is usually fine for the things views hold (feeds, leaderboards, summaries):\n a leaderboard that is a second behind is still a good leaderboard.\n- **`get` returns `null` until the first publish.** Always default the empty case.\n\n## Gotchas\n\n- **`publish` is derive/job only.** You cannot publish from a route. If you need\n to update a view in response to a request, write the *source* (append an event,\n add to a counter) from the action and let the `@derive` republish.\n- **Handle `null` from `get`.** A never-published view reads as `null`, not as an\n empty value. Return a sensible default.\n- **A view is a copy, not the truth.** Never store data *only* in a view. Keep\n the real data in a source family (Documents, Events, Counters) and treat the\n view as a disposable, recomputable projection.\n- **Keep views bounded.** A view value is read whole on every request, so build\n it from a bounded read (top N, latest N, a total), not the entire history.\n\n## Related\n\n- [`@derive`](../background/derive.md): the normal way a view is maintained.\n- [Events](./events.md): the append-only log a view is often folded from.\n- [Counters](./counters.md): another common source for a view (totals).\n- [Documents](./documents.md): mutable source-of-truth records (and how a write\n differs from a publish).\n- [Data types (`@data`)](../concepts/types.md): how view keys and values are stored.\n",
|
|
30
30
|
"frontend/components.md": "# Components\n\nA toiljs frontend is a normal React app, so the components you write are just React components: functions that return JSX, hold state with hooks, and compose however you like. There is no toiljs-specific base class, no decorator, and no registration step. On top of your own components, toiljs ships a small set of ready-made ones on the `Toil` global (an image, a script loader, a form, and a few more) for the jobs a plain React app makes you wire up by hand. This page covers both: how your components fit in, and a reference for every `Toil.*` component.\n\n## Writing your own components\n\nAnything you already know about writing React components applies unchanged. You write ordinary functions, use `useState` / `useEffect` / `useMemo` and any hooks you like, and return JSX:\n\n```tsx\n// client/components/Counter.tsx\nimport { useState } from 'react';\n\nexport default function Counter({ start = 0 }: { start?: number }) {\n const [n, setN] = useState(start);\n return <button onClick={() => setN(n + 1)}>Clicked {n} times</button>;\n}\n```\n\nYour reusable components live in `client/components/`, and you import them the normal way from anywhere in `client/`:\n\n```tsx\n// client/routes/index.tsx\nimport Counter from '../components/Counter';\n\nexport default function Home() {\n return (\n <main>\n <h1>Welcome</h1>\n <Counter start={10} />\n </main>\n );\n}\n```\n\nA few things are worth spelling out, because they are the parts that are handled for you rather than by you:\n\n- **Route files must `export default` a component.** A file under `client/routes/` becomes a page only if it default-exports a component (see [Routing](./routing.md)). Files under `client/components/` have no such rule; export them however you please (default or named).\n- **The `Toil.*` globals need no import.** `Toil.Link`, `Toil.useParams()`, `Toil.Image`, and everything else on `Toil` are ambient. The compiler generates a `toil-env.d.ts` that types the global, so your editor autocompletes `Toil.` and type-checks it with no `import` line. The same goes for `Server` (the typed backend client) and the `FastMap` / `DataWriter` data utilities. See the [Frontend overview](./README.md) for the whole ambient surface.\n- **The JSX runtime is configured for you.** You do not write `import React from 'react'` at the top of every file. The build sets up the automatic JSX runtime, so JSX just works. Import named hooks and types from `react` when you need them (`import { useState, type ReactNode } from 'react'`), but the bare React import for JSX is unnecessary.\n\nIn other words, a component in a toiljs app is indistinguishable from a component in any React app. Two things are genuinely special, and both are opt-in.\n\n### Special case 1: components inside an SSR route\n\nIf a route opts into server rendering with `export const ssr = true`, its component tree is rendered once at build time into a template, then filled per request on the edge (see [Rendering and SSR](./rendering.md)). For that to work, any part of the JSX that changes per request (a value from the URL, a list from a loader, a block of user HTML) has to be wrapped in one of the [SSR marker primitives](#ssr-marker-primitives) below, or isolated in a `Toil.Island`. Anything you leave unwrapped gets frozen into the template at its build-time value.\n\nYou do not have to get this perfect: if an SSR route (or a layout above it) cannot render on the server, toiljs skips SSR for that route at build, prints a warning telling you what to move, and falls back to plain client rendering. So the route still works, it just loses its server first paint until you address the warning.\n\n### Special case 2: importing images and other assets\n\nImporting an image with the `?toil` suffix gives you an object carrying the resolved URL, the intrinsic width and height, and an auto-generated blur placeholder (a `blurDataURL`), ready to hand straight to `Toil.Image`:\n\n```tsx\nimport hero from './hero.webp?toil';\n// hero is { src, width, height, blurDataURL }\n\n<Toil.Image src={hero} alt=\"Our office\" />;\n```\n\nPassing the whole object lets `Toil.Image` reserve the correct aspect ratio and paint the blur while the real image loads, with no extra props. See [Images](./images.md) for the full treatment.\n\nPlain asset imports are typed for you as well. A bare `import logo from './logo.svg'` (or `.png`, `.webp`, and so on) resolves to the hashed URL string, and the vite-imagetools query forms (`?url`, `?as=srcset`, `?as=metadata`) are typed too, so you get autocomplete and type-checking on all of them without declaring any modules yourself.\n\n## The toiljs component primitives\n\nThese are the components toiljs provides on the `Toil` global. They cover the framework-level jobs a plain React app leaves to you. All are ambient (no import), and all are fully typed.\n\n| Component | What it is | Renders |\n| --- | --- | --- |\n| `Toil.Image` | Layout-shift-free `<img>` replacement with lazy-load and blur placeholder. | An `<img>` (optionally wrapped in a sizing box). |\n| `Toil.Script` | One-time external or inline `<script>` loader with a load strategy. | Nothing (`null`). |\n| `Toil.Form` | A `<form>` that runs an action on submit and revalidates loader data. | A `<form>`. |\n| `Toil.Slot` | Renders a named parallel-route slot for the current URL. | The slot's route tree, or a fallback. |\n| `Toil.Head` | Declarative `<head>` contribution (title, meta, link). | Nothing (`null`). |\n| `Toil.Metadata` | Declarative route-style metadata applied from any component. | Nothing (`null`). |\n\nThe [SSR marker primitives](#ssr-marker-primitives) (`Toil.Hole`, `Toil.Repeat`, `Toil.RawHtml`, `Toil.attr`, `Toil.Island`) are a separate group, covered in their own section below.\n\n### `Toil.Image`\n\nA drop-in `<img>` replacement that prevents layout shift and lazy-loads by default. You give it `width` and `height` (or `fill`) so it reserves space before the image arrives, it decodes asynchronously, it lazy-loads unless you mark it `priority`, and it can fade in from a blur placeholder. It accepts either a string URL or a `?toil` import object (which auto-fills the size and blur):\n\n```tsx\nimport hero from './hero.webp?toil';\n\nexport default function Home() {\n return (\n <>\n <Toil.Image src={hero} alt=\"Our office\" priority placeholder=\"blur\" />\n <Toil.Image src=\"/team.jpg\" alt=\"The team\" width={800} height={600} />\n </>\n );\n}\n```\n\nKey props are `src`, `alt` (required, pass `alt=\"\"` for a decorative image), `width` / `height` or `fill`, `priority` (for an above-the-fold hero), and `placeholder` (`'empty'` or `'blur'`). It also accepts any standard `<img>` attribute. This is kept brief on purpose: [Images](./images.md) covers sizing, `fill`, blur placeholders, and how it stops layout shift in full.\n\n### `Toil.Script`\n\nLoads an external or inline `<script>` exactly once for the whole life of the app (even across client-side navigations), and lets you choose when it runs with a `strategy` prop. Use it instead of a hand-written `<script>` tag for third-party snippets like analytics or chat widgets, which a plain `<script>` in a single-page app runs unreliably or twice:\n\n```tsx\n// client/layout.tsx\nexport default function Layout({ children }: { children?: React.ReactNode }) {\n return (\n <div className=\"app\">\n <Toil.Script src=\"https://cdn.example-analytics.com/analytics.js\" />\n {children}\n </div>\n );\n}\n```\n\nThe `strategy` is `afterInteractive` (default), `lazyOnload`, or `beforeInteractive`, and there are `onLoad` / `onReady` / `onError` callbacks. `Toil.Script` renders nothing. See [Scripts](./scripts.md) for the strategies, inline scripts, dedup rules, and the full prop table.\n\n### `Toil.Form`\n\nA `<form>` that submits without reloading the page. On submit it runs your `action` (which receives the form's `FormData`), tracks pending and error state, and on success revalidates the current route's loader data so the page reflects the write. It is the convenient front end of the loader/action data loop:\n\n```tsx\n// A guestbook that refreshes its list after a successful sign.\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, error }) => (\n <>\n <input name=\"author\" placeholder=\"Your name\" />\n <textarea name=\"message\" />\n <button disabled={pending}>{pending ? 'Signing...' : 'Sign'}</button>\n {error ? <p className=\"err\">Could not sign, try again.</p> : null}\n </>\n )}\n </Toil.Form>\n );\n}\n```\n\nPass a render function as the child to read the live submit state: it receives `{ pending, error, data }`, which is how you disable the button while pending or show an error. The props:\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `action` | `(data: FormData) => void \\| Promise<void>` | (required) | Runs on submit, receiving the form's `FormData`. May be async. |\n| `revalidate` | `RevalidateTarget` | `true` | Which loader data to refetch after a successful submit. `true` is the current route. |\n| `onSuccess` | `() => void` | (none) | Called after a successful submit. |\n| `onError` | `(error: unknown) => void` | (none) | Called when the action throws. |\n| `resetOnSuccess` | `boolean` | `false` | Reset the form fields after a successful submit. |\n| `className` | `string` | (none) | Class on the `<form>` element. |\n| `children` | `ReactNode` or `(state) => ReactNode` | (none) | Form contents. A function child receives `{ pending, error, data }`. |\n\nFor writes that are not form submits (a delete button, a like toggle), reach for the underlying `Toil.useAction` hook instead. Both are covered in [Fetching data](./data-fetching.md).\n\n### `Toil.Slot`\n\nRenders the named parallel-route slot for the current URL. A folder starting with `@` (like `@modal`) under `client/routes/` is a whole second route tree that matches the URL independently of the main page; placing `<Toil.Slot name=\"modal\" />` is where that tree renders. If no slot route matches the current URL, it renders the `fallback` (nothing by default):\n\n```tsx\n// client/routes/gallery/layout.tsx\nexport default function GalleryLayout({ children }: { children?: React.ReactNode }) {\n return (\n <div>\n {children}\n <Toil.Slot name=\"modal\" fallback={null} />\n </div>\n );\n}\n```\n\n| Prop | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `name` | `string` | (required) | The slot name: the `@name` directory under `routes/`, without the `@`. |\n| `fallback` | `ReactNode` | `null` | Rendered when no slot route matches the current URL. |\n\nParallel slots and the intercepting routes that fill them (the \"click a photo, open it in a modal\" pattern) are explained in [Routing](./routing.md).\n\n### `Toil.Head`\n\nA declarative way for any component (a page, a layout, a deep child) to contribute to the document `<head>`: a title, `<meta>` tags, and `<link>` tags. It renders nothing; it just applies its head entries for the lifetime of the component and reverts them on unmount. Entries compose across the tree, with later or deeper ones winning per key:\n\n```tsx\nexport default function ArticlePage() {\n const post = Toil.useLoaderData<typeof loader>();\n return (\n <>\n <Toil.Head\n title={post.title}\n meta={[\n { name: 'description', content: post.summary },\n { property: 'og:title', content: post.title },\n ]}\n link={[{ rel: 'canonical', href: `https://example.com/blog/${post.id}` }]}\n />\n <article>{/* ... */}</article>\n </>\n );\n}\n```\n\n`<Toil.Head>` takes `title`, `meta` (an array of `{ name | property, content }` tags), and `link` (an array of `{ rel, href }` tags). The hook form `Toil.useHead(spec)` and the shorthand `Toil.useTitle(title)` do the same job imperatively. See [Metadata and SEO](./metadata.md).\n\n### `Toil.Metadata`\n\nThe declarative, convenience-shaped cousin of `Toil.Head`. Instead of raw meta and link arrays, you pass a route-style metadata object (the same shape a route file's `export const metadata` uses), and toiljs expands the convenience fields (`description`, `keywords`, `canonical`, `openGraph`, and so on) into the right tags. It renders nothing, and applies for the component's lifetime. Use it to set metadata from a component that is not itself a route file (a rendered article, a widget):\n\n```tsx\n<Toil.Metadata\n title=\"Our pricing\"\n description=\"Simple, flat pricing for teams of any size.\"\n openGraph={{ title: 'Pricing', image: 'https://example.com/og/pricing.png' }}\n/>\n```\n\nBecause a route's own `metadata` export is applied last (highest priority), `Toil.Metadata` fills in for routes that declare none and yields to a route that sets the same key. The full field list lives in [Metadata and SEO](./metadata.md).\n\n## SSR marker primitives\n\nThese five primitives matter only for routes with `export const ssr = true`. They are how you tell the build which parts of a server-rendered page are dynamic (filled per request) versus static (baked into the template once). On a client-only route they do nothing special, so you never need them unless you opt a route into SSR.\n\n**The mental model.** In the browser these markers are transparent: `<Toil.Hole>` renders its children, `<Toil.Repeat>` maps its rows, `<Toil.RawHtml>` renders a raw-HTML wrapper, and `Toil.attr(id, value)` returns the value unchanged. Your client-side app runs exactly as written. But under the build-time SSR extractor, each marker instead emits a sentinel token that marks an insertion point. The extractor strips those tokens and records their positions, producing a static template with numbered holes. Per request, your compiled backend fills only the hole values, and the edge splices them into the template. Because the static scaffold around each hole is React's own rendering output and the hole values are escaped exactly as React escapes them, the browser hydrates the result byte-for-byte, with no mismatch.\n\n| Marker | Use it for | Shape |\n| --- | --- | --- |\n| `Toil.Hole` | A single dynamic **text** value. | `<Toil.Hole id=\"...\">{value}</Toil.Hole>` |\n| `Toil.Repeat` | A **list**: one row template repeated over an `each` array. | `<Toil.Repeat id=\"...\" each={rows}>{(item, i) => ...}</Toil.Repeat>` |\n| `Toil.RawHtml` | A block of **trusted, pre-rendered HTML**. | `<Toil.RawHtml id=\"...\" html={s} as=\"section\" />` |\n| `Toil.attr` | A dynamic value in **attribute position** (an `href`, a `class`). | `href={Toil.attr('id', value)}` (a function call) |\n| `Toil.Island` | Content that must render **only in the browser** (escape hatch). | `<Toil.Island>{children}</Toil.Island>` |\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 a sample 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, `as=\"section\"` to change the tag), and you own sanitising that HTML, exactly like React's `dangerouslySetInnerHTML`.\n- **`Toil.attr` is a function, not an element.** An attribute is not a child node, so it cannot be a JSX element. You call `Toil.attr(id, value)` right where the attribute value goes, and it composes with literal text around it (`` className={`btn ${Toil.attr('kind', d.kind)}`} ``).\n- **`Toil.Island` renders nothing on the server and on the first (hydration) render**, then reveals its children after mount. So an island gets no server first paint and no SEO, by design. It is the place for anything that genuinely cannot run on the server (reads `window`, calls `Date.now()`, depends on the live URL).\n\nHere is a compact SSR route wiring several of them together. The title, 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\nexport const loader = async ({ params }: Toil.LoaderArgs) => {\n // Illustrative shape: { title, 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 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\nFor the whole SSR story (the template extraction flow, keeping a route SSR-safe, and the current SSR limitations), see [Rendering and SSR](./rendering.md).\n\n## Related\n\n- [Rendering and SSR](./rendering.md): how SSR routes render, hydrate, and use the marker primitives.\n- [Images](./images.md): the full `Toil.Image` reference, blur placeholders, and layout shift.\n- [Scripts](./scripts.md): `Toil.Script` strategies, inline scripts, and dedup.\n- [Fetching data](./data-fetching.md): `Toil.Form`, `useAction`, loaders, and the typed backend clients.\n- [Metadata and SEO](./metadata.md): `Toil.Head`, `Toil.Metadata`, and per-route metadata.\n- [Routing](./routing.md): pages, layouts, and the parallel slots `Toil.Slot` renders.\n",
|
|
31
|
-
"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## 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",
|
|
31
|
+
"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",
|
|
32
32
|
"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",
|
|
33
33
|
"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 <Toil.Head><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 <head>\"]\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",
|
|
34
34
|
"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",
|