toiljs 0.0.85 → 0.0.87

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -0,0 +1,236 @@
1
+ # Rendering and SSR
2
+
3
+ This page explains where your pages are built: in the browser, ahead of time at build, or on the server for each request. Getting this right is what makes a page paint fast and rank well.
4
+
5
+ ## The three ways a page can render
6
+
7
+ A toiljs page can reach the user in three ways. You mostly get all three for free; the only one you opt into per page is edge SSR.
8
+
9
+ | Mode | Who builds the HTML | When | Good for |
10
+ | --- | --- | --- | --- |
11
+ | **Client rendering** | The browser, from JavaScript | On every visit | Interactive, per-user pages (a dashboard). The default. |
12
+ | **Build-time prerender** | The build, once | When you run `toiljs build` | Baking each route's `<head>` (SEO) into real HTML. Automatic. |
13
+ | **Edge SSR** | The edge server, per request | When you set `ssr = true` | A real first-paint page body plus SEO, for landing and content pages. |
14
+
15
+ Let us define the two words that trip people up:
16
+
17
+ - **Rendering** means turning your React components into HTML.
18
+ - **Hydration** means React attaching to HTML that already exists on the page (from the server) instead of throwing it away and redrawing it. Hydration is what makes a server-rendered page interactive without a flash.
19
+
20
+ ## Client rendering (the default)
21
+
22
+ By default, toiljs ships a small HTML shell with an empty `<div id="root">`, plus your JavaScript. The browser downloads the JS, React runs, and it builds the page into `#root`. From then on, navigating between pages is pure JavaScript: only the next route's small code chunk and its data are fetched, and the page swaps in place with no reload.
23
+
24
+ This is fast to navigate and simple to reason about. Its one weakness is the *first* paint: until the JavaScript runs, `#root` is empty. For an app behind a login (a dashboard) that is fine, nobody is trying to index it. For a public landing page, you usually want one of the two server-assisted modes below.
25
+
26
+ ## Build-time prerender (automatic SEO)
27
+
28
+ Every time you build, toiljs renders each static route once and bakes its resolved `<head>` (title, description, canonical link, Open Graph tags, and so on) into that route's HTML file. This happens for all routes with no extra work from you, and it is driven by the `metadata` you export from a route plus the site-wide `seo` config (see [Metadata and SEO](./metadata.md)).
29
+
30
+ The payoff: a crawler or a link-preview bot (Slack, Discord, iMessage) that fetches your page sees correct tags immediately, even though it does not run your JavaScript. "View source" on a built page shows the real title and meta tags, not an empty shell.
31
+
32
+ In production, `toiljs build` writes one prerendered HTML file per route (for example `about/index.html`), and the production static server (`npm start`) serves each route its own prerendered file rather than a single shared shell. That is how each page gets its own metadata in the raw HTML.
33
+
34
+ Build-time prerender covers the `<head>`. It does not, by itself, fill in the page *body*: for a client-rendered route the body is still built by React in the browser. To get real first-paint body HTML, opt the route into edge SSR.
35
+
36
+ ### Prerendering dynamic routes with `generateStaticParams`
37
+
38
+ Build-time prerender bakes a `<head>` for every **static** route on its own. A **dynamic** route (`client/routes/blog/[id].tsx`, one file that serves `/blog/1`, `/blog/2`, and so on) has no single URL to prerender, so by default it gets none of that per-URL HTML. If you know the concrete URLs ahead of time (a fixed set of blog posts, product pages, or docs), you can opt the route into **static site generation (SSG)**, which means the build renders one HTML file per known URL: it enumerates each URL and writes a real `<url>/index.html` with that page's resolved metadata, plus a `sitemap.xml` entry. This is the toiljs analog of Next.js `generateStaticParams`.
39
+
40
+ You opt in by exporting `generateStaticParams` from the dynamic route. It returns one object per URL, keyed by the route's param names:
41
+
42
+ ```tsx
43
+ // client/routes/blog/[id].tsx
44
+
45
+ // One entry per URL to prerender. `id` matches the [id] segment.
46
+ export const generateStaticParams: Toil.GenerateStaticParams = () => {
47
+ return [{ id: '1' }, { id: '2' }, { id: '3' }];
48
+ };
49
+
50
+ // The build runs this once per URL, so each baked page gets its own <head>.
51
+ export const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({
52
+ title: `Blog post ${params.id}`,
53
+ description: `Reading blog post ${params.id}.`,
54
+ });
55
+
56
+ export default function BlogPost() {
57
+ const { id } = Toil.useParams();
58
+ return <h1>Blog post {id}</h1>;
59
+ }
60
+ ```
61
+
62
+ At build, that writes `blog/1/index.html`, `blog/2/index.html`, and `blog/3/index.html`, each with its own title and description in the raw HTML (so a crawler that does not run JavaScript sees them), and all three land in `sitemap.xml`. The param values also feed the route's `loader`, so per-URL metadata can depend on real data.
63
+
64
+ `generateStaticParams` is async-friendly (return a promise if you fetch the id list first) and completely opt-in: a dynamic route without it is untouched, and the whole pass is skipped when your project has no `seo` config. A catch-all segment (`[...slug]`) takes an array value: `{ slug: ['2024', 'hello'] }` fills the URL as `2024/hello`.
65
+
66
+ > Two things this is **not**: it is build-time prerender of the `<head>`, not edge SSR of the body. To also serve real body HTML for these URLs on first paint, add `export const ssr = true` as well (see below). And to make a dynamic route show up in the on-site [search index](./search.md) even though its title is dynamic, export `searchHints` (covered there), which is separate from `generateStaticParams`.
67
+
68
+ ## Edge SSR (`ssr = true`)
69
+
70
+ For a route where you want the body content visible on first paint (a marketing page, an article), add one line:
71
+
72
+ ```tsx
73
+ // client/routes/index.tsx
74
+ export const ssr = true;
75
+
76
+ export default function Home() {
77
+ return (
78
+ <section className="hero">
79
+ <h1>Welcome</h1>
80
+ </section>
81
+ );
82
+ }
83
+ ```
84
+
85
+ Now the Dacely edge sends a real, filled-in first paint for that page, and the browser hydrates it. The user sees content immediately, and React takes over without redrawing anything.
86
+
87
+ ### How it works, in brief
88
+
89
+ toiljs does something clever to keep SSR cheap. It does not re-run React on the server for every request. Instead, at build time it renders the page once into a **template**: the static HTML with the dynamic bits punched out into named holes. Then, per request, your compiled backend fills only the hole values (a small list of "slot 3 = this text"), and the edge splices those values into the pre-baked template. The result is real first-paint HTML produced about as fast as serving a static file.
90
+
91
+ ```mermaid
92
+ sequenceDiagram
93
+ participant B as Build
94
+ participant Edge as Dacely edge
95
+ participant U as Browser
96
+ B->>Edge: prebuilt template (HTML with holes) + a coherence hash
97
+ U->>Edge: GET / (ssr route)
98
+ Edge->>Edge: run the wasm render -> small "hole values" list
99
+ Edge->>Edge: splice values into the template
100
+ Edge-->>U: real first-paint HTML
101
+ Note over U: Content is visible immediately
102
+ U->>Edge: fetch JS + route chunk
103
+ Note over U: React hydrates: attaches to the existing HTML
104
+ Note over U: Page is interactive; no redraw, no flash
105
+ ```
106
+
107
+ For SSR to hydrate cleanly, the HTML the server produced and the HTML the browser would produce must match byte-for-byte. toiljs guarantees this by escaping hole values exactly as React does and by carrying a hash that ties the running backend to the exact template it was built against. Authoring the server side of an SSR route (the hole markers in the page and the matching `render` function in `server/`) is a deeper topic that lives with the [backend](../backend/README.md). For most pages you only need `export const ssr = true` and to keep the page "SSR-safe" (below).
108
+
109
+ ### Authoring an SSR route
110
+
111
+ `export const ssr = true` is all you need for a page whose body is fully static. The moment the body has a **dynamic bit** (a value that changes per request: a name from the URL, a list from a loader, a chunk of user HTML), you have to tell the build *where* that dynamic bit lives, so it can be punched out into a hole. You do that by wrapping the dynamic value in a **hole marker**.
112
+
113
+ Why is this necessary? The build renders your page once into a template. It cannot guess which `{expression}` in your JSX is a per-request value and which is a constant, so it does not try: any dynamic content you leave unwrapped gets frozen into the template as whatever value it happened to have during that one build render, and it will never update per request. The markers are the explicit "fill this in later" signal.
114
+
115
+ The markers live on the `Toil` global (so you do not import them), and they are **transparent in the browser**: `<Toil.Hole>` just renders its children, `<Toil.Repeat>` just maps its rows. They only behave differently during the build render, so your client-side app runs exactly as written.
116
+
117
+ | Marker | Use it for | Shape |
118
+ | --- | --- | --- |
119
+ | `Toil.Hole` | A single dynamic **text** value. | JSX element with `id` + children |
120
+ | `Toil.Repeat` | A **list**: a row template repeated over an `each` array. | JSX element with `id` + `each` + a render function |
121
+ | `Toil.RawHtml` | A block of **pre-rendered HTML** you trust (Markdown you rendered, say). | JSX element with `id` + `html` (+ optional `as`) |
122
+ | `Toil.attr` | A dynamic value in **attribute position** (an `href`, a `class`). | a **function** you call inside the attribute |
123
+ | `Toil.Island` | Content that must render **only in the browser** (the escape hatch). | JSX element with children |
124
+
125
+ `Toil.attr` is a function rather than an element because an attribute is not a child node, so it cannot be a JSX element. You call it right where the value goes.
126
+
127
+ Here is a full SSR route: a blog post whose title, body HTML, tag list, and author link all come from the route's `loader`.
128
+
129
+ ```tsx
130
+ // client/routes/blog/[id].tsx
131
+ export const ssr = true;
132
+
133
+ // Runs on the server for the first paint, then again on the client to reproduce
134
+ // the same data so hydration matches (see "Keeping a route SSR-safe" below).
135
+ export const loader = async ({ params }: Toil.LoaderArgs) => {
136
+ // Illustrative shape: { title, bodyHtml, tags, authorUrl }.
137
+ return Server.REST.blog.get({ params: { id: params.id } });
138
+ };
139
+
140
+ export default function BlogPost() {
141
+ const post = Toil.useLoaderData<typeof loader>();
142
+ return (
143
+ <article>
144
+ <h1>
145
+ <Toil.Hole id="title">{post.title}</Toil.Hole>
146
+ </h1>
147
+
148
+ {/* A dynamic attribute: call attr() in attribute position. */}
149
+ <a href={Toil.attr('authorUrl', post.authorUrl)}>By the author</a>
150
+
151
+ {/* A block of trusted, pre-rendered HTML. */}
152
+ <Toil.RawHtml id="body" html={post.bodyHtml} />
153
+
154
+ {/* A list: one row template, stamped once per item on the server. */}
155
+ <ul>
156
+ <Toil.Repeat id="tags" each={post.tags}>
157
+ {(tag) => <li key={tag}>{tag}</li>}
158
+ </Toil.Repeat>
159
+ </ul>
160
+ </article>
161
+ );
162
+ }
163
+ ```
164
+
165
+ A few rules that keep the template valid:
166
+
167
+ - **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.
168
+ - **`Toil.Repeat` needs at least one row at build time.** It captures that first row as the sub-template for every row, so the build render must see sample data with one or more items (an empty `each` gives it nothing to capture).
169
+ - **`Toil.RawHtml` renders inside a wrapper element** (a `<div>` by default; pass `as="section"` to change the tag), and you own sanitising that HTML, exactly like React's `dangerouslySetInnerHTML`.
170
+
171
+ Anything that genuinely cannot run on the server (it reads `window`, calls `Date.now()`, or depends on the live URL) goes inside a `Toil.Island`, which renders nothing on the server and reveals its children only after hydration:
172
+
173
+ ```tsx
174
+ <Toil.Island>
175
+ <LiveClock />
176
+ </Toil.Island>
177
+ ```
178
+
179
+ This is the flow from your marked-up JSX to the first-paint HTML:
180
+
181
+ ```mermaid
182
+ flowchart TD
183
+ A["Your SSR route JSX<br/>dynamic bits wrapped in Toil.Hole / Repeat / RawHtml / attr"] --> B["Build render (once, in sentinel mode)"]
184
+ B --> C[".tmpl: static HTML with numbered holes"]
185
+ B --> D[".slots: which hole is text / raw / attr / repeat"]
186
+ B --> E["Slot enum + coherence HASH"]
187
+ C --> F["Deployed to the Dacely edge"]
188
+ E --> G["Compiled backend render(req)"]
189
+ G -->|"per request: fill only the hole values"| F
190
+ D --> G
191
+ F -->|"splice values into the template"| H["First-paint HTML to the browser"]
192
+ ```
193
+
194
+ #### Advanced: hand-writing the server `render`
195
+
196
+ You almost never do this. The compiler generates the server `render(req)` for an SSR route from the JSX above, so the hole ids line up automatically. But the server side is backed by a plain, hand-writable API for the rare case you need full control (an unusual template, or a value the compiler cannot derive). Your `render` returns a `SlotValues` object, filled with:
197
+
198
+ - `setText(slot, value)`: a text hole (React-escaped for you).
199
+ - `setRaw(slot, html)`: a raw-HTML hole (you own sanitising).
200
+ - `setAttr(slot, value)`: an attribute hole.
201
+ - `setRepeat(slot, rows)`: a repeat region, with the rows assembled through an `HtmlBuilder` (chain `.raw(...)`, `.text(...)`, `.attr(...)`).
202
+ - `setHeader(name, value)`, `setTitle(title)`, and `setStatus(code)`: response headers, a per-request `<title>`, and the status code.
203
+
204
+ `SlotValues` and `HtmlBuilder` live in `server/runtime/ssr/slots.ts`. The `setTitle` helper is the supported way to give an SSR page a data-driven `<title>` (a blog post's real title from its loader), overriding the one baked into the template. This is server (backend) code, so it belongs with the [backend](../backend/README.md).
205
+
206
+ ### Keeping a route SSR-safe
207
+
208
+ Server rendering happens where there is no browser: no `window`, no `document`, no mouse. So an SSR route (and every layout above it) must render without touching browser-only APIs during that first render. Anything that must run only in the browser (reading `window`, using `Date.now()`, or router hooks that need the live URL) goes inside an **island**, a marker that renders nothing on the server and appears only after hydration.
209
+
210
+ If a route or one of its layouts throws while rendering on the server, toiljs does not ship a broken page. It **skips SSR for that route at build with a warning** and falls back to plain client rendering. So adding `ssr = true` is always safe: worst case you get client rendering plus a build warning telling you what to move into an island.
211
+
212
+ ### Suspense markers and self-healing hydration
213
+
214
+ Under the hood the client wraps each route and layout in React `Suspense` boundaries that line up with what the server emitted, so hydration matches. If hydration ever does mismatch (the server HTML and the client's idea of the page disagree), React does the safe thing: it discards the server markup for that part and re-renders it on the client. You get a correct page either way. The cost of a mismatch is a small flash and some wasted work, not a broken page, which is why the guidance above (keep it SSR-safe, put browser-only bits in islands) is about smoothness, not correctness.
215
+
216
+ ## Known SSR limitations
217
+
218
+ Be aware of these honest gaps as of today:
219
+
220
+ - **`template.tsx` is not server-rendered.** A `template.tsx` wrapper (the re-mounting cousin of a layout) is not part of the SSR output. A route under one still works: hydration self-heals to client rendering for that part.
221
+ - **Parallel slots (`@slot`) are not server-rendered.** Slot content (including intercepted modals) renders on the client after hydration, not in the first paint. Since slots are typically modals and overlays that appear on interaction, this is rarely a problem.
222
+ - **Islands have no first paint or SEO.** That is by design: an island is your "client only" escape hatch, so anything inside it is intentionally absent from the server HTML and from what crawlers see.
223
+ - **The client loader must reproduce the server's data.** For a hole whose value comes from the request (a query param), the route's client `loader` has to derive the same value the server used, or hydration will re-render that part. If the client cannot reproduce a value, put that content in an island.
224
+
225
+ ## Which mode should I use?
226
+
227
+ - **Interactive, per-user page** (dashboard, settings): client rendering. Do nothing.
228
+ - **Public page that needs correct link previews and titles**: you already have build-time prerender. Do nothing extra.
229
+ - **Public page that should also show its content instantly on first load** (landing page, blog post, docs): add `export const ssr = true` and keep it SSR-safe.
230
+
231
+ ## Related
232
+
233
+ - [Backend overview](../backend/README.md): where the server-side `render` for an SSR route lives.
234
+ - [Metadata and SEO](./metadata.md): what gets baked into the `<head>`.
235
+ - [Routing](./routing.md): layouts, templates, and slots.
236
+ - [Fetching data](./data-fetching.md): loaders and how their data seeds hydration.
@@ -0,0 +1,344 @@
1
+ # Routing
2
+
3
+ toiljs uses file-based routing: every file under `client/routes/` becomes a page, and its path on disk becomes its URL. There is no route config to maintain, you create a file and the route exists.
4
+
5
+ This page is about **frontend** (browser) routing: the pages a user visits and how they navigate between them. Your **backend** HTTP endpoints use a different, decorator-based system (see [HTTP routes](../backend/rest.md)).
6
+
7
+ ## The basic idea
8
+
9
+ Drop a React component at `client/routes/<something>.tsx`, `export default` it, and it is a page. The default export is the component that renders at that URL.
10
+
11
+ ```tsx
12
+ // client/routes/about.tsx -> /about
13
+ export default function About() {
14
+ return (
15
+ <main>
16
+ <h1>About us</h1>
17
+ </main>
18
+ );
19
+ }
20
+ ```
21
+
22
+ Run `toiljs dev`, open `/about`, and it is there. Nothing else to register.
23
+
24
+ ## How a file path becomes a URL
25
+
26
+ The compiler scans `client/routes/` and turns each file into a URL pattern with a small set of rules. Here is the whole mapping in one table:
27
+
28
+ | File under `client/routes/` | URL | What it is |
29
+ | --- | --- | --- |
30
+ | `index.tsx` | `/` | The home page. |
31
+ | `about.tsx` | `/about` | A static page. |
32
+ | `blog/index.tsx` | `/blog` | An index inside a folder. |
33
+ | `blog/[id].tsx` | `/blog/:id` | A **dynamic** page (one param). |
34
+ | `docs/[...slug].tsx` | `/docs/*` | A **catch-all** (one or more segments). |
35
+ | `files/[[...slug]].tsx` | `/files` and `/files/*` | An **optional** catch-all (zero or more). |
36
+ | `(marketing)/pricing.tsx` | `/pricing` | A **route group**: parens add no URL segment. |
37
+ | `@modal/photo/[id].tsx` | (a parallel slot, see below) | A named **slot**. |
38
+
39
+ The rules, spelled out:
40
+
41
+ - **`index`** means "the folder itself", so `blog/index.tsx` is `/blog`, and the top `index.tsx` is `/`.
42
+ - **Square brackets** mark a dynamic segment. `[id]` captures one path segment into a param named `id`. You read it with `Toil.useParams()`.
43
+ - **`[...name]`** is a catch-all: it captures the rest of the path (one segment or more) as a single `/`-joined string.
44
+ - **`[[...name]]`** is an *optional* catch-all: like `[...name]`, but it also matches the bare parent URL with nothing after it (the param is then absent).
45
+ - **Parentheses** like `(marketing)` create a **route group**: a folder that organizes files without adding anything to the URL. Handy for grouping pages that share a layout.
46
+
47
+ ```mermaid
48
+ flowchart LR
49
+ A["client/routes/blog/[id].tsx"] -->|scan| B["/blog/:id"]
50
+ B -->|visit /blog/42| C["params = { id: '42' }"]
51
+ C -->|Toil.useParams| D["render Blog post 42"]
52
+ ```
53
+
54
+ ### Dynamic pages: reading the param
55
+
56
+ A file named with brackets gets its captured values from `Toil.useParams()`:
57
+
58
+ ```tsx
59
+ // client/routes/blog/[id].tsx -> /blog/:id
60
+ export default function BlogPost() {
61
+ const { id } = Toil.useParams();
62
+ return (
63
+ <main>
64
+ <h1>Blog post {id}</h1>
65
+ </main>
66
+ );
67
+ }
68
+ ```
69
+
70
+ Visiting `/blog/42` renders "Blog post 42". Param values are URL-decoded for you.
71
+
72
+ For a catch-all, the param is the whole tail joined with slashes:
73
+
74
+ ```tsx
75
+ // client/routes/docs/[...slug].tsx -> /docs/*slug
76
+ export default function Docs() {
77
+ const { slug } = Toil.useParams();
78
+ // /docs/getting-started/install -> slug === "getting-started/install"
79
+ return <main>{slug}</main>;
80
+ }
81
+ ```
82
+
83
+ An optional catch-all (`[[...slug]]`) matches the bare parent too, so `slug` can be empty:
84
+
85
+ ```tsx
86
+ // client/routes/files/[[...slug]].tsx -> matches /files AND /files/a/b
87
+ export default function Files() {
88
+ const { slug } = Toil.useParams();
89
+ // "/files" -> slug is undefined; "/files/a/b" -> slug === "a/b"
90
+ return <main>{slug ?? '(the base /files page)'}</main>;
91
+ }
92
+ ```
93
+
94
+ ### When two routes could match
95
+
96
+ If a URL could match more than one pattern, toiljs picks the most specific one. Static segments win over dynamic (`:id`) segments, which win over catch-alls (`*slug`), and deeper routes win over shallower ones. So `/blog/new` prefers a literal `blog/new.tsx` over `blog/[id].tsx` if both exist. You do not configure this, it just does the intuitive thing.
97
+
98
+ ## Special files
99
+
100
+ Some filenames are not pages, they are helpers that wrap or replace pages. They live alongside your routes and are never matched as a URL:
101
+
102
+ | Filename | Role |
103
+ | --- | --- |
104
+ | `layout.tsx` | Wraps the pages in its folder (and below). **Persists** across navigation. |
105
+ | `template.tsx` | Like a layout, but **re-mounts** on every navigation within it. |
106
+ | `loading.tsx` | Shown while the page (and its data) is loading. |
107
+ | `error.tsx` | Shown when the page throws (an error boundary). |
108
+ | `global-error.tsx` | The last-resort error boundary, wraps even the root layout. |
109
+ | `404.tsx` (or `not-found.tsx`) | Shown when no route matches. |
110
+
111
+ ### Layouts
112
+
113
+ A `layout.tsx` receives the page (or nested layout) as `children` and renders around it. The root `client/layout.tsx` wraps every page. It is the natural home for your header, footer, and site-wide `<head>` defaults:
114
+
115
+ ```tsx
116
+ // client/layout.tsx
117
+ import type { ReactNode } from 'react';
118
+ import Header from './components/Header';
119
+ import Footer from './components/Footer';
120
+
121
+ export default function Layout({ children }: { children?: ReactNode }) {
122
+ return (
123
+ <div className="app">
124
+ <Header />
125
+ <main className="content">{children}</main>
126
+ <Footer />
127
+ </div>
128
+ );
129
+ }
130
+ ```
131
+
132
+ Layouts nest by folder. A `client/routes/dashboard/layout.tsx` wraps every page under `/dashboard`, inside the root layout. Because a layout **persists** across navigations, state inside it (a sidebar's open/closed flag, a scroll position) survives when you move between its child pages.
133
+
134
+ ### Templates vs layouts
135
+
136
+ A `template.tsx` looks like a layout but does the opposite on navigation: it **re-mounts** every time you move to a new page within it. Use a layout when state should persist (a nav sidebar), and a template when it should reset (an enter animation that should replay, or a counter that should start fresh per page).
137
+
138
+ ### Loading and error states
139
+
140
+ Put a `loading.tsx` next to a page (or in a folder) and it shows automatically while that page's chunk and its `loader` data are still resolving:
141
+
142
+ ```tsx
143
+ // client/routes/dashboard/loading.tsx
144
+ export default function Loading() {
145
+ return <p>Loading dashboard...</p>;
146
+ }
147
+ ```
148
+
149
+ Put an `error.tsx` there and it catches any error the page throws, showing a fallback instead of a blank screen. `global-error.tsx` sits outside the root layout, so it catches errors thrown by the layout itself.
150
+
151
+ ## Route groups: shared layout, no URL change
152
+
153
+ Wrap a folder name in parentheses to group files without changing their URLs. This is the trick for "these three pages share one layout, but I do not want a `/legal` prefix":
154
+
155
+ ```
156
+ client/routes/
157
+ (legal)/
158
+ layout.tsx -> wraps both pages below
159
+ privacy.tsx -> /privacy (NOT /legal/privacy)
160
+ terms.tsx -> /terms
161
+ ```
162
+
163
+ ## Parallel slots (`@slot`) and intercepting routes
164
+
165
+ This is an advanced feature; skip it until you need a modal that keeps the page behind it alive.
166
+
167
+ A folder starting with `@`, like `@modal`, is a **named slot**. It is a whole second route tree that matches the current URL *independently* of the main page, and renders wherever you place a `<Toil.Slot>`:
168
+
169
+ ```tsx
170
+ // client/routes/gallery/layout.tsx
171
+ import type { ReactNode } from 'react';
172
+
173
+ export default function GalleryLayout({ children }: { children?: ReactNode }) {
174
+ return (
175
+ <div>
176
+ {children}
177
+ <Toil.Slot name="modal" /> {/* renders the @modal slot for this URL */}
178
+ </div>
179
+ );
180
+ }
181
+ ```
182
+
183
+ The `@` folder adds nothing to the URL; it just says "these routes fill the slot named `modal`". A slot with no match renders nothing (or a `fallback` you pass).
184
+
185
+ An **intercepting route** is a slot route whose folder name starts with `(.)`, `(..)`, or `(...)`. It hijacks a *soft* (in-app) navigation to another URL and renders that URL's content inside the slot instead, while the main page stays mounted behind it. That is exactly how a "click a photo, see it in a modal over the gallery" pattern works, where reloading the page (a hard load) shows the full photo page instead:
186
+
187
+ ```
188
+ client/routes/gallery/
189
+ index.tsx -> /gallery
190
+ photo/[id].tsx -> /gallery/photo/:id (full page, on hard load)
191
+ @modal/(.)photo/[id].tsx -> fills @modal on a soft click to that URL
192
+ ```
193
+
194
+ The `(.)` markers mean: `(.)` same level, `(..)` up one level, `(...)` from the routes root. This tells the interceptor which real URL it is standing in for.
195
+
196
+ ## Links and navigation
197
+
198
+ ### `Toil.Link`
199
+
200
+ Use `Toil.Link` instead of a plain `<a>` for in-app links. It navigates client-side (no full page reload), and prefetches the target route's code on hover or focus so the click feels instant:
201
+
202
+ ```tsx
203
+ <Toil.Link href="/about">About</Toil.Link>
204
+ ```
205
+
206
+ `Link` accepts every normal anchor attribute (`className`, `target`, `rel`, `download`, and so on), plus a few toiljs controls:
207
+
208
+ | Prop | Default | What it does |
209
+ | --- | --- | --- |
210
+ | `replace` | `false` | Replace the current history entry instead of pushing a new one. |
211
+ | `scroll` | `true` | Scroll to the top after navigating. |
212
+ | `prefetch` | `true` | Prefetch the route on hover/focus. Set `false` to opt out. |
213
+
214
+ `Link` is smart about when *not* to intercept: external URLs, `target="_blank"`, `download`, `#hash` links, and modified clicks (Ctrl/Cmd/middle-click) all fall through to normal browser behavior. The `href` is typed to your project's real routes, so a typo is a compile error.
215
+
216
+ ### `Toil.NavLink` and active state
217
+
218
+ `NavLink` is a `Link` that knows whether it points at the current page. When active it adds the class `active` (configurable) and `aria-current="page"`. This is what you want for a navigation bar that highlights the current section:
219
+
220
+ ```tsx
221
+ <Toil.NavLink href="/blog" activeClassName="is-current">
222
+ Blog
223
+ </Toil.NavLink>
224
+ ```
225
+
226
+ You can also drive `className`, `style`, or `children` from the active state with a function:
227
+
228
+ ```tsx
229
+ <Toil.NavLink href="/blog" className={({ isActive }) => (isActive ? 'on' : 'off')}>
230
+ Blog
231
+ </Toil.NavLink>
232
+ ```
233
+
234
+ By default a parent link is active for its sub-paths too (`/blog` is active on `/blog/42`). Pass `end` to require an exact match:
235
+
236
+ ```tsx
237
+ <Toil.NavLink href="/" end>Home</Toil.NavLink>
238
+ ```
239
+
240
+ ### Navigating in code
241
+
242
+ For navigation that is not a link (after a form submit, a redirect), use the router hook or the free `navigate` function:
243
+
244
+ ```tsx
245
+ export default function Login() {
246
+ const router = Toil.useRouter();
247
+ const onDone = () => router.push('/dashboard');
248
+ // router.replace(href), router.back(), router.forward(), router.refresh()
249
+ return <button onClick={onDone}>Continue</button>;
250
+ }
251
+ ```
252
+
253
+ `useRouter()` returns a handle with `push`, `replace`, `back`, `forward`, `refresh` (re-run the current page's data loader), `revalidate` (refetch data), and `prefetch`.
254
+
255
+ If you just need to jump to a URL and nothing else, `Toil.useNavigate()` returns the bare `navigate(href, options)` function:
256
+
257
+ ```tsx
258
+ export default function Login() {
259
+ const navigate = Toil.useNavigate();
260
+ return (
261
+ <button onClick={() => navigate('/dashboard', { replace: true })}>
262
+ Continue
263
+ </button>
264
+ );
265
+ }
266
+ ```
267
+
268
+ The options are `{ replace?: boolean; scroll?: boolean }`, the same two controls `Toil.Link` exposes above. The same function is also available free of any hook as `Toil.navigate(href, options)`, which is handy in code that is not a React component (a plain event handler, a utility module).
269
+
270
+ ### Typed hrefs and the `href()` escape hatch
271
+
272
+ Every href you pass to `Toil.Link`, `navigate`, or `router.push` is type-checked against your project's real routes. The compiler scans `client/routes/` and generates a file called `toil-routes.d.ts` that registers the union of your route paths (it fills in the `Href` type). So a typo is a compile error before you ever run the app:
273
+
274
+ ```tsx
275
+ <Toil.Link href="/abuot">About</Toil.Link>
276
+ // ^ Type error: "/abuot" is not assignable to type 'Href'. (No such route.)
277
+ ```
278
+
279
+ Dynamic routes show up in that union as template-literal types. A file at `client/routes/blog/[id].tsx` contributes the type `` `/blog/${string}` ``, so a link built with one interpolation usually still checks:
280
+
281
+ ```tsx
282
+ <Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine: matches `/blog/${string}`
283
+ ```
284
+
285
+ The catch is a URL assembled from several data pieces (or one TypeScript cannot prove the shape of): it is typed as a plain `string`, and `string` is not assignable to `Href`, so you get a type error:
286
+
287
+ ```tsx
288
+ const cat = product.category; // string
289
+ const slug = product.slug; // string
290
+ navigate(`/${cat}/${slug}`);
291
+ // ^ Argument of type 'string' is not assignable to parameter of type 'Href'.
292
+ ```
293
+
294
+ The escape hatch is `Toil.href()`. It takes a `string` and asserts it is a valid href, returning the `Href` type the navigation APIs expect. Use it right at the call site once you are sure the path is a real route:
295
+
296
+ ```tsx
297
+ navigate(Toil.href(`/${cat}/${slug}`)); // asserted valid
298
+ <Toil.Link href={Toil.href(`/${cat}/${slug}`)}>Open</Toil.Link>;
299
+ ```
300
+
301
+ `href()` is a pure type assertion (it returns the string unchanged), so reach for it only when the type-check is in your way, not to paper over a genuine typo. Before the routes are generated (a fresh project, the first build), `Href` is just `string`, so nothing complains until `toil-routes.d.ts` exists.
302
+
303
+ ### Reading the current location
304
+
305
+ A handful of hooks let a component read where it is:
306
+
307
+ | Hook | Returns |
308
+ | --- | --- |
309
+ | `Toil.useParams()` | The dynamic route params, e.g. `{ id }`. |
310
+ | `Toil.usePathname()` | The current path, e.g. `"/blog/42"`. |
311
+ | `Toil.useLocation()` | The current path, e.g. `"/blog/42"` (an alias of `usePathname()`). |
312
+ | `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |
313
+ | `Toil.useNavigationPending()` | `true` while a navigation is in flight (for a loading bar). |
314
+
315
+ ## Loading data for a route
316
+
317
+ A route file can `export const loader` alongside its component. The loader runs on navigation, in parallel with loading the page's code, and the page reads the result with `Toil.useLoaderData`. This keeps data fetching out of `useEffect` and lets `loading.tsx` show while it runs. Loaders are covered in depth in [Fetching data](./data-fetching.md):
318
+
319
+ ```tsx
320
+ export const loader = async ({ params }: Toil.LoaderArgs) => {
321
+ const post = await Server.REST.blog.get({ params: { id: params.id } });
322
+ return post;
323
+ };
324
+
325
+ export default function BlogPost() {
326
+ const post = Toil.useLoaderData<typeof loader>();
327
+ return <article><h1>{post.title}</h1></article>;
328
+ }
329
+ ```
330
+
331
+ ## Gotchas
332
+
333
+ - **`export default` is required.** A route file without a default-exported component is not a page. The special files (`layout`, `loading`, and so on) also use the default export.
334
+ - **Reserved filenames are not pages.** `layout.tsx`, `template.tsx`, `loading.tsx`, `error.tsx`, `global-error.tsx`, `404.tsx`, and `not-found.tsx` are helpers, not routes. Do not name a real page one of these.
335
+ - **Use `Toil.Link`, not `<a>`, for in-app links.** A plain `<a href="/about">` triggers a full page reload, throwing away the SPA speed. Reserve `<a>` for external links.
336
+ - **Params are always strings.** `useParams()` gives you `{ id: "42" }`, not a number. Convert if you need a number.
337
+ - **Route groups and slots are invisible in the URL.** `(group)` and `@slot` folders never appear in the address bar. If a URL looks wrong, check for a stray bracket or paren in the file path.
338
+
339
+ ## Related
340
+
341
+ - [Rendering and SSR](./rendering.md): what renders on the server versus the browser.
342
+ - [Fetching data](./data-fetching.md): loaders, the typed backend clients, and forms.
343
+ - [Metadata and SEO](./metadata.md): set the title and tags per route.
344
+ - [Backend HTTP routes](../backend/rest.md): the separate, decorator-based server routing.