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,148 @@
1
+ # Images
2
+
3
+ `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>`.
4
+
5
+ ## Why not just use `<img>`?
6
+
7
+ Two problems with a plain `<img>`:
8
+
9
+ 1. **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).
10
+ 2. **Loading cost.** Every image loads eagerly by default, competing for bandwidth with the things the user actually needs first.
11
+
12
+ `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.
13
+
14
+ ## The simplest usage
15
+
16
+ Give 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:
17
+
18
+ ```tsx
19
+ export default function Post() {
20
+ return (
21
+ <Toil.Image
22
+ src="/images/diagram.png"
23
+ alt="How the request flows"
24
+ width={800}
25
+ height={400}
26
+ />
27
+ );
28
+ }
29
+ ```
30
+
31
+ `alt` is required (toiljs enforces it for accessibility). For a purely decorative image, pass `alt=""`.
32
+
33
+ There 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.
34
+
35
+ ## Automatic blur placeholders with `?toil`
36
+
37
+ For 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:
38
+
39
+ ```tsx
40
+ import hero from './hero.webp?toil';
41
+
42
+ export default function Landing() {
43
+ return <Toil.Image src={hero} alt="Product hero" placeholder="blur" />;
44
+ }
45
+ ```
46
+
47
+ The `?toil` import does not give you a plain URL string. It gives you an object:
48
+
49
+ ```ts
50
+ {
51
+ src: string; // the optimized, hashed image URL
52
+ width: number; // the image's intrinsic width
53
+ height: number; // the image's intrinsic height
54
+ blurDataURL: string; // a tiny base64 blurred preview, inlined
55
+ }
56
+ ```
57
+
58
+ `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.
59
+
60
+ Behind 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.
61
+
62
+ ### The skeleton fallback
63
+
64
+ `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).
65
+
66
+ You can also pass a `blurDataURL` by hand for a plain string `src`:
67
+
68
+ ```tsx
69
+ <Toil.Image
70
+ src="/images/photo.jpg"
71
+ alt="A photo"
72
+ width={1200}
73
+ height={800}
74
+ placeholder="blur"
75
+ blurDataURL="data:image/webp;base64,UklGR..."
76
+ />
77
+ ```
78
+
79
+ ## The real layout-shift fix: aspect ratio
80
+
81
+ The 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.
82
+
83
+ The takeaway: always give `width` + `height` (or `fill`). A `?toil` import supplies them for you.
84
+
85
+ ## `fill`: sizing from the container
86
+
87
+ Sometimes you do not know the image's size, you want it to fill a box you control (a card, a banner). Pass `fill`:
88
+
89
+ ```tsx
90
+ <div style={{ maxWidth: 480 }}>
91
+ <Toil.Image src={photo} alt="Cover" fill />
92
+ </div>
93
+ ```
94
+
95
+ With `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`):
96
+
97
+ ```tsx
98
+ <Toil.Image
99
+ src={photo}
100
+ alt="Cover"
101
+ fill
102
+ style={{ aspectRatio: '16 / 9' }}
103
+ objectFit="cover"
104
+ />
105
+ ```
106
+
107
+ The `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>`.
108
+
109
+ Use fixed `width`/`height` when you know the image's size; use `fill` when the layout decides the size.
110
+
111
+ ## Priority images (above the fold)
112
+
113
+ By 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`:
114
+
115
+ ```tsx
116
+ <Toil.Image src={hero} alt="Hero" width={1200} height={630} priority />
117
+ ```
118
+
119
+ `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.
120
+
121
+ ## Prop reference
122
+
123
+ | Prop | Type | Default | What it does |
124
+ | --- | --- | --- | --- |
125
+ | `src` | `string \| ToilImageSource` | (required) | A URL, or a `?toil` import object. |
126
+ | `alt` | `string` | (required) | Alt text. Use `""` for decorative images. |
127
+ | `width` / `height` | `number` | from `?toil` | Intrinsic size, reserves space. |
128
+ | `fill` | `boolean` | `false` | Fill the container instead of using fixed size. |
129
+ | `objectFit` | CSS `object-fit` | `cover` (with fill) | How the image fits its box. |
130
+ | `priority` | `boolean` | `false` | Eager load + high fetch priority (above-the-fold). |
131
+ | `placeholder` | `'empty' \| 'blur'` | `'empty'` | Show a blur / skeleton while loading. |
132
+ | `blurDataURL` | `string` | from `?toil` | The tiny preview for `placeholder="blur"`. |
133
+
134
+ Every other standard `<img>` attribute (`className`, `style`, `onLoad`, `sizes`, `srcSet`, `id`, `data-*`) passes straight through.
135
+
136
+ ## Gotchas
137
+
138
+ - **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.
139
+ - **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.
140
+ - **`?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.
141
+ - **`alt` is mandatory.** This is intentional. For decorative images that add no meaning, pass `alt=""` so screen readers skip them.
142
+ - **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).
143
+
144
+ ## Related
145
+
146
+ - [Styling](./styling.md): importing images and CSS in general.
147
+ - [Metadata and SEO](./metadata.md): setting `og:image` for social-share previews.
148
+ - [Rendering and SSR](./rendering.md): why first-paint layout stability matters.
@@ -0,0 +1,344 @@
1
+ # Metadata and SEO
2
+
3
+ Metadata 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.
4
+
5
+ ## The quick version
6
+
7
+ For most pages, `export const metadata` from the route file. That is it:
8
+
9
+ ```tsx
10
+ // client/routes/features/seo.tsx
11
+ export const metadata: Toil.Metadata = {
12
+ title: 'useReducer | React Hooks',
13
+ description: 'Manage complex state transitions with a reducer function.',
14
+ keywords: ['react', 'hooks', 'useReducer'],
15
+ canonical: 'https://example.com/features/seo',
16
+ openGraph: {
17
+ title: 'useReducer | React Hooks',
18
+ description: 'Manage complex state transitions with a reducer.',
19
+ type: 'website',
20
+ },
21
+ };
22
+
23
+ export default function SeoDemo() {
24
+ return <main><h1>Route metadata</h1></main>;
25
+ }
26
+ ```
27
+
28
+ The 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.
29
+
30
+ ## The `Metadata` fields
31
+
32
+ `Toil.Metadata` maps friendly fields onto the right `<meta>` and `<link>` tags for you:
33
+
34
+ | Field | Becomes |
35
+ | --- | --- |
36
+ | `title` | The document `<title>`. |
37
+ | `description` | `<meta name="description">`. |
38
+ | `keywords` | `<meta name="keywords">` (an array is joined with commas). |
39
+ | `canonical` | `<link rel="canonical">`. |
40
+ | `robots` | `<meta name="robots">`, e.g. `'noindex, nofollow'`. |
41
+ | `themeColor` | `<meta name="theme-color">` (the accent color of some link embeds). |
42
+ | `openGraph` | The `og:*` tags (title, description, type, url, image, siteName). |
43
+ | `meta` | Escape hatch: extra raw `<meta>` tags. |
44
+ | `link` | Escape hatch: extra raw `<link>` tags. |
45
+
46
+ Open 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:
47
+
48
+ ```tsx
49
+ export const metadata: Toil.Metadata = {
50
+ title: 'Our launch',
51
+ description: 'Read the announcement.',
52
+ openGraph: {
53
+ title: 'Our launch',
54
+ description: 'Read the announcement.',
55
+ type: 'article',
56
+ image: 'https://example.com/og/launch.png',
57
+ },
58
+ };
59
+ ```
60
+
61
+ ## Dynamic metadata: `generateMetadata`
62
+
63
+ When 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`:
64
+
65
+ ```tsx
66
+ // client/routes/blog/[id].tsx
67
+ export const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({
68
+ title: `Blog post ${params.id}`,
69
+ description: `Reading blog post ${params.id}.`,
70
+ });
71
+ ```
72
+
73
+ Now `/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:
74
+
75
+ ```tsx
76
+ export const loader = async ({ params }: Toil.LoaderArgs) =>
77
+ Server.REST.blog.get({ params: { id: params.id } });
78
+
79
+ export const generateMetadata: Toil.GenerateMetadata = ({ data }) => ({
80
+ title: data.title,
81
+ description: data.excerpt,
82
+ });
83
+ ```
84
+
85
+ ## Imperative and stateful head: `useHead`, `useTitle`, `<Head>`
86
+
87
+ Sometimes 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:
88
+
89
+ ```tsx
90
+ export default function HeadDemo() {
91
+ const [count, setCount] = useState(0);
92
+
93
+ // The tab title updates every render as count changes.
94
+ Toil.useTitle(`Clicked ${count} times`);
95
+
96
+ Toil.useHead({
97
+ meta: [{ name: 'description', content: `Clicked ${count} times.` }],
98
+ });
99
+
100
+ return <button onClick={() => setCount((c) => c + 1)}>Clicked {count}</button>;
101
+ }
102
+ ```
103
+
104
+ The declarative form renders nothing and is equivalent:
105
+
106
+ ```tsx
107
+ <Toil.Head
108
+ title="Blog"
109
+ meta={[{ name: 'description', content: 'Latest posts' }]}
110
+ />
111
+ ```
112
+
113
+ Use `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.
114
+
115
+ ### Applying a whole `Metadata` object from a component: `useMetadata`
116
+
117
+ `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.
118
+
119
+ ```tsx
120
+ export default function Article({ post }: { post: Post }) {
121
+ Toil.useMetadata({
122
+ title: post.title,
123
+ description: post.excerpt,
124
+ openGraph: { type: 'article', title: post.title, image: post.cover },
125
+ });
126
+
127
+ return <article>{/* ... */}</article>;
128
+ }
129
+ ```
130
+
131
+ There 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.
132
+
133
+ > **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.
134
+
135
+ ## How the pieces combine
136
+
137
+ Multiple 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:
138
+
139
+ ```mermaid
140
+ flowchart TB
141
+ A["Site-wide defaults<br/>client.seo config"] --> M{"merge by key"}
142
+ B["Root layout &lt;Toil.Head&gt;<br/>(fallback title, description)"] --> M
143
+ C["Component useHead / useTitle"] --> M
144
+ D["Route metadata / generateMetadata<br/>(highest priority)"] --> M
145
+ M --> R["The page's final &lt;head&gt;"]
146
+ ```
147
+
148
+ - A **root layout** is the natural home for site-wide fallbacks (a default title and description for any page that sets none):
149
+
150
+ ```tsx
151
+ // client/layout.tsx
152
+ <Toil.Head
153
+ title="My Site"
154
+ meta={[{ name: 'description', content: 'Planet-scale apps.' }]}
155
+ />
156
+ ```
157
+
158
+ - 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.
159
+
160
+ The rule of thumb: put fallbacks in the layout, put the specifics on the route.
161
+
162
+ ## Build-time SEO for the whole site
163
+
164
+ Beyond 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:
165
+
166
+ ```ts
167
+ // toil.config.ts
168
+ import { defineConfig } from 'toiljs/compiler';
169
+
170
+ export default defineConfig({
171
+ client: {
172
+ seo: {
173
+ url: 'https://example.com', // required for canonical/OG urls, sitemap
174
+ title: 'My Site',
175
+ description: 'Planet-scale apps from a single repo.',
176
+ openGraph: {
177
+ type: 'website',
178
+ siteName: 'My Site',
179
+ image: 'https://example.com/og.png',
180
+ },
181
+ twitter: { card: 'summary_large_image', site: '@mysite' },
182
+ robots: { ai: 'allow' }, // allow or disallow known AI crawlers
183
+ llms: { instructions: 'Docs live at /docs.' },
184
+ jsonLd: { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },
185
+ },
186
+ },
187
+ });
188
+ ```
189
+
190
+ From this one block, the build:
191
+
192
+ - bakes the default `<title>`, `description`, Open Graph, Twitter card, and JSON-LD structured data into every page's HTML;
193
+ - overlays each route's own `metadata` on top and points that route's canonical and `og:url` at its own URL;
194
+ - 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).
195
+
196
+ You 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.
197
+
198
+ ### The complete `client.seo` reference
199
+
200
+ Every 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`).
201
+
202
+ **Top level:**
203
+
204
+ | Field | Type | What it does |
205
+ | --- | --- | --- |
206
+ | `url` | `string` | Absolute site base URL, e.g. `'https://example.com'`. Unlocks `sitemap.xml`, the canonical `<link>`, and absolute Open Graph / Twitter URLs. |
207
+ | `title` | `string` | Default document `<title>`. |
208
+ | `description` | `string` | Default `<meta name="description">`. |
209
+ | `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.) |
210
+ | `themeColor` | `string` | `<meta name="theme-color">`, also the accent color of some Discord / Slack link embeds. |
211
+ | `openGraph` | object | Open Graph (`og:*`) defaults, see below. |
212
+ | `twitter` | object | Twitter / X card, see below. |
213
+ | `facebook` | `{ appId?: string }` | `appId` renders `<meta property="fb:app_id">`. Open Graph covers the rest of the Facebook card. |
214
+ | `preconnect` | `string[]` | Origins to `<link rel="preconnect">` (early connection hints). |
215
+ | `dnsPrefetch` | `string[]` | Origins to `<link rel="dns-prefetch">`. |
216
+ | `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). |
217
+ | `robots` | object or `false` | `robots.txt` generation, see below. `false` skips the file. |
218
+ | `sitemap` | `boolean` | `sitemap.xml` generation. On by default when `url` is set; `false` skips it. |
219
+ | `llms` | object or `boolean` | `llms.txt` (AI-crawler guidance) generation. `false` skips it, `true` or an object configures it. |
220
+
221
+ **`openGraph`** (the `og:*` tags; `title` and `description` fall back to the top-level values):
222
+
223
+ | Field | Type | Renders |
224
+ | --- | --- | --- |
225
+ | `title` | `string` | `og:title` |
226
+ | `description` | `string` | `og:description` |
227
+ | `type` | `string` | `og:type`, e.g. `'website'` or `'article'` (defaults to `'website'`). |
228
+ | `siteName` | `string` | `og:site_name` |
229
+ | `locale` | `string` | `og:locale`, e.g. `'en_US'`. |
230
+ | `image` | `string` | `og:image`, the preview picture (absolute URL, ideally at least 1200 by 630 pixels). |
231
+ | `imageAlt` | `string` | `og:image:alt` |
232
+ | `imageWidth` | `number` | `og:image:width` in pixels (lets Facebook / LinkedIn render without a re-fetch). |
233
+ | `imageHeight` | `number` | `og:image:height` in pixels. |
234
+ | `imageType` | `string` | `og:image:type`, e.g. `'image/png'`. |
235
+
236
+ **`twitter`** (the Twitter / X card; unset fields fall back to the Open Graph or top-level values):
237
+
238
+ | Field | Type | Renders |
239
+ | --- | --- | --- |
240
+ | `card` | `string` | `twitter:card`: `'summary'` or `'summary_large_image'` (defaults by whether an image is present). |
241
+ | `site` | `string` | `twitter:site`, the site's `@handle`. |
242
+ | `creator` | `string` | `twitter:creator`, the author's `@handle`. |
243
+ | `title` | `string` | `twitter:title` (falls back to `openGraph.title` / `title`). |
244
+ | `description` | `string` | `twitter:description` (falls back to `openGraph.description` / `description`). |
245
+ | `image` | `string` | `twitter:image` (falls back to `openGraph.image`). |
246
+ | `imageAlt` | `string` | `twitter:image:alt` (falls back to `openGraph.imageAlt`). |
247
+
248
+ **`robots`** (the `robots.txt` file; set `robots: false` to skip it):
249
+
250
+ | Field | Type | What it does |
251
+ | --- | --- | --- |
252
+ | `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: /`). |
253
+ | `ai` | `'allow'` or `'disallow'` | How to treat known AI crawlers (GPTBot, ClaudeBot, Google-Extended, and more). Default `'allow'`. |
254
+ | `sitemap` | `string` | Explicit `Sitemap:` line (defaults to `<url>/sitemap.xml` when `url` is set). |
255
+
256
+ **`llms`** (the `llms.txt` guidance file; set `llms: false` to skip it, `llms: true` for defaults):
257
+
258
+ | Field | Type | What it does |
259
+ | --- | --- | --- |
260
+ | `title` | `string` | The file's heading (falls back to `seo.title`). |
261
+ | `summary` | `string` | The summary blockquote (falls back to `seo.description`). |
262
+ | `instructions` | `string` | Free-form guidance for AI / LLM crawlers. |
263
+ | `pages` | `LlmsPage[]` | Key pages, each `{ title: string, url: string, description?: string }`. Defaults to the site's static routes. |
264
+
265
+ A fully worked config touching most of these fields:
266
+
267
+ ```ts
268
+ // toil.config.ts
269
+ import { defineConfig } from 'toiljs/compiler';
270
+
271
+ export default defineConfig({
272
+ client: {
273
+ seo: {
274
+ url: 'https://example.com', // base URL: unlocks sitemap, canonical, absolute OG/Twitter URLs
275
+ title: 'My Site', // default <title>
276
+ description: 'Planet-scale apps.', // default <meta name="description">
277
+ robotsMeta: 'index, follow', // default <meta name="robots">
278
+ themeColor: '#0b0b0f', // <meta name="theme-color">
279
+
280
+ openGraph: {
281
+ type: 'website', // og:type
282
+ siteName: 'My Site', // og:site_name
283
+ locale: 'en_US', // og:locale
284
+ image: 'https://example.com/og.png', // og:image (absolute, ideally 1200x630)
285
+ imageAlt: 'My Site preview', // og:image:alt
286
+ imageWidth: 1200, // og:image:width
287
+ imageHeight: 630, // og:image:height
288
+ imageType: 'image/png', // og:image:type
289
+ },
290
+
291
+ twitter: {
292
+ card: 'summary_large_image', // twitter:card
293
+ site: '@mysite', // twitter:site
294
+ creator: '@ada', // twitter:creator
295
+ // title / description / image / imageAlt fall back to openGraph + top level
296
+ },
297
+
298
+ facebook: { appId: '1234567890' }, // <meta property="fb:app_id">
299
+
300
+ preconnect: ['https://cdn.example.com'], // <link rel="preconnect">
301
+ dnsPrefetch: ['https://analytics.example.com'], // <link rel="dns-prefetch">
302
+
303
+ robots: {
304
+ ai: 'allow', // known AI crawlers: 'allow' | 'disallow'
305
+ rules: [ // custom User-agent groups for robots.txt
306
+ { userAgent: '*', allow: ['/'], disallow: ['/admin'] },
307
+ ],
308
+ // sitemap: 'https://example.com/sitemap.xml', // explicit line (auto from url otherwise)
309
+ },
310
+ sitemap: true, // generate sitemap.xml (on by default when url is set)
311
+
312
+ llms: { // llms.txt (AI-crawler guidance)
313
+ title: 'My Site',
314
+ summary: 'Planet-scale apps from a single repo.',
315
+ instructions: 'Docs live at /docs.',
316
+ // pages: [{ title: 'Docs', url: 'https://example.com/docs', description: 'Guides' }],
317
+ },
318
+
319
+ jsonLd: [ // array = several nodes in one <script>
320
+ { '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site' },
321
+ { '@context': 'https://schema.org', '@type': 'Organization', name: 'My Site' },
322
+ ],
323
+ },
324
+ },
325
+ });
326
+ ```
327
+
328
+ ## Per-request titles on server-rendered pages
329
+
330
+ For 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.
331
+
332
+ ## Gotchas
333
+
334
+ - **`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.
335
+ - **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).
336
+ - **`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.
337
+ - **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.
338
+
339
+ ## Related
340
+
341
+ - [Rendering and SSR](./rendering.md): how the baked head and SSR title reach the browser.
342
+ - [Images](./images.md): producing the `og:image` for share previews.
343
+ - [Routing](./routing.md): where `metadata` and `generateMetadata` live in a route file.
344
+ - [Configuration](../concepts/config.md): the full `toil.config.ts` reference.