toiljs 0.0.115 → 0.0.117
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/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
|
@@ -1,344 +0,0 @@
|
|
|
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 <Toil.Head><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 <head>"]
|
|
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.
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
# Navigation
|
|
2
|
-
|
|
3
|
-
Once 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.
|
|
4
|
-
|
|
5
|
-
Routing 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.
|
|
6
|
-
|
|
7
|
-
## `Toil.Link`
|
|
8
|
-
|
|
9
|
-
`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:
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
<Toil.Link href="/about">About</Toil.Link>
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
`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:
|
|
16
|
-
|
|
17
|
-
| Prop | Type | Default | What it does |
|
|
18
|
-
| --- | --- | --- | --- |
|
|
19
|
-
| `href` | `Href` | (required) | Destination, typed to your project's real routes (a typo is a compile error). |
|
|
20
|
-
| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing a new one. |
|
|
21
|
-
| `scroll` | `boolean` | `true` | Scroll to the top after navigating. `false` keeps the current position. |
|
|
22
|
-
| `prefetch` | `boolean` | `true` | Prefetch the route chunk on hover/focus. `false` opts this link out. |
|
|
23
|
-
|
|
24
|
-
### When `Link` does not intercept
|
|
25
|
-
|
|
26
|
-
`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:
|
|
27
|
-
|
|
28
|
-
- **External URLs** (a different origin), and opaque targets like `mailto:` or `tel:`.
|
|
29
|
-
- **`target` other than `_self`** (for example `target="_blank"`, a new tab).
|
|
30
|
-
- **`download`** links.
|
|
31
|
-
- **In-page `#hash`-only** links.
|
|
32
|
-
- **Modified clicks**: middle-click or any click with Ctrl, Cmd (Meta), Shift, or Alt held (so "open in new tab" keeps working).
|
|
33
|
-
|
|
34
|
-
Because 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.
|
|
35
|
-
|
|
36
|
-
## `Toil.NavLink` and active state
|
|
37
|
-
|
|
38
|
-
`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.
|
|
39
|
-
|
|
40
|
-
On top of `LinkProps` it adds:
|
|
41
|
-
|
|
42
|
-
| Prop | Type | Default | What it does |
|
|
43
|
-
| --- | --- | --- | --- |
|
|
44
|
-
| `end` | `boolean` | `false` | Require an exact match. Without it, a parent link is active for its sub-paths. |
|
|
45
|
-
| `activeClassName` | `string` | `'active'` | The class added when active (used with a string `className`). |
|
|
46
|
-
| `className` | `string \| (state) => string \| undefined` | (none) | A string, or a function of `{ isActive }`. |
|
|
47
|
-
| `style` | `CSSProperties \| (state) => CSSProperties \| undefined` | (none) | A style object, or a function of `{ isActive }`. |
|
|
48
|
-
| `children` | `ReactNode \| (state) => ReactNode` | (none) | Content, or a function of `{ isActive }`. |
|
|
49
|
-
|
|
50
|
-
A simple nav bar with string classes:
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
// client/components/Nav.tsx
|
|
54
|
-
export default function Nav() {
|
|
55
|
-
return (
|
|
56
|
-
<nav>
|
|
57
|
-
<Toil.NavLink href="/" end>Home</Toil.NavLink>
|
|
58
|
-
<Toil.NavLink href="/blog">Blog</Toil.NavLink>
|
|
59
|
-
<Toil.NavLink href="/about" activeClassName="is-current">About</Toil.NavLink>
|
|
60
|
-
</nav>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
By 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`.
|
|
66
|
-
|
|
67
|
-
The function forms let the active state drive `className`, `style`, or `children` directly. Each receives a `{ isActive }` object:
|
|
68
|
-
|
|
69
|
-
```tsx
|
|
70
|
-
<Toil.NavLink href="/blog" className={({ isActive }) => (isActive ? 'tab on' : 'tab')}>
|
|
71
|
-
{({ isActive }) => <span>{isActive ? '• ' : ''}Blog</span>}
|
|
72
|
-
</Toil.NavLink>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Navigating in code
|
|
76
|
-
|
|
77
|
-
Not 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.
|
|
78
|
-
|
|
79
|
-
### `Toil.useNavigate()`
|
|
80
|
-
|
|
81
|
-
The hook returns the bare `navigate(href, options)` function:
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
export default function Login() {
|
|
85
|
-
const navigate = Toil.useNavigate();
|
|
86
|
-
return (
|
|
87
|
-
<button onClick={() => navigate('/dashboard', { replace: true })}>
|
|
88
|
-
Continue
|
|
89
|
-
</button>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
The options are `NavigateOptions`, the same two controls `Link` exposes:
|
|
95
|
-
|
|
96
|
-
| Option | Type | Default | What it does |
|
|
97
|
-
| --- | --- | --- | --- |
|
|
98
|
-
| `replace` | `boolean` | `false` | Replace the current history entry instead of pushing. |
|
|
99
|
-
| `scroll` | `boolean` | `true` | Scroll to top after navigating. `false` keeps the position. |
|
|
100
|
-
|
|
101
|
-
### `Toil.navigate` (outside React)
|
|
102
|
-
|
|
103
|
-
The 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:
|
|
104
|
-
|
|
105
|
-
```tsx
|
|
106
|
-
async function saveDraft(draft: Draft) {
|
|
107
|
-
await Server.REST.posts.create({ body: draft });
|
|
108
|
-
Toil.navigate('/posts');
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
`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).
|
|
113
|
-
|
|
114
|
-
### `Toil.useRouter()`
|
|
115
|
-
|
|
116
|
-
For a full imperative handle, `useRouter()` returns a `RouterInstance`:
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
export default function PostActions() {
|
|
120
|
-
const router = Toil.useRouter();
|
|
121
|
-
return (
|
|
122
|
-
<>
|
|
123
|
-
<button onClick={() => router.push('/blog/new')}>New post</button>
|
|
124
|
-
<button onClick={() => router.back()}>Back</button>
|
|
125
|
-
<button onClick={() => router.revalidate()}>Refresh data</button>
|
|
126
|
-
</>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
| Method | What it does |
|
|
132
|
-
| --- | --- |
|
|
133
|
-
| `push(href, options?)` | Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). |
|
|
134
|
-
| `replace(href)` | Navigate to `href`, replacing the current history entry. |
|
|
135
|
-
| `back()` | Go back one history entry. |
|
|
136
|
-
| `forward()` | Go forward one history entry. |
|
|
137
|
-
| `refresh()` | Re-render the current route and re-run its loader (clears all cached loader data). |
|
|
138
|
-
| `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. |
|
|
139
|
-
| `prefetch(href)` | Warm a route's chunk ahead of navigation. |
|
|
140
|
-
|
|
141
|
-
Reach 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.
|
|
142
|
-
|
|
143
|
-
## Typed hrefs and the `href()` escape hatch
|
|
144
|
-
|
|
145
|
-
Every `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:
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
<Toil.Link href="/abuot">About</Toil.Link>
|
|
149
|
-
// ^ Type error: "/abuot" is not assignable to type 'Href'. (No such route.)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Dynamic 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:
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
<Toil.Link href={`/blog/${post.id}`}>Read</Toil.Link> // fine
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
When 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:
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
const path = `/${product.category}/${product.slug}`; // string
|
|
162
|
-
Toil.navigate(Toil.href(path)); // asserted valid
|
|
163
|
-
<Toil.Link href={Toil.href(path)}>Open</Toil.Link>;
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
`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.
|
|
167
|
-
|
|
168
|
-
## Reading the current location
|
|
169
|
-
|
|
170
|
-
A 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:
|
|
171
|
-
|
|
172
|
-
| Hook | Returns |
|
|
173
|
-
| --- | --- |
|
|
174
|
-
| `Toil.usePathname()` | The current pathname, e.g. `"/blog/42"`. |
|
|
175
|
-
| `Toil.useLocation()` | The current pathname (an alias of `usePathname()`). |
|
|
176
|
-
| `Toil.useParams<T>()` | The dynamic route params, e.g. `{ id }` for `/blog/[id]`. |
|
|
177
|
-
| `Toil.useSearchParams()` | The query string as a `URLSearchParams`. |
|
|
178
|
-
| `Toil.useNavigationPending()` | `true` while a navigation is in flight (started but not committed). |
|
|
179
|
-
|
|
180
|
-
`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:
|
|
181
|
-
|
|
182
|
-
```tsx
|
|
183
|
-
// client/components/ProgressBar.tsx
|
|
184
|
-
export default function ProgressBar() {
|
|
185
|
-
const pending = Toil.useNavigationPending();
|
|
186
|
-
return <div className="progress" data-active={pending} />;
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
`useSearchParams()` gives you a live `URLSearchParams`, so a filtered list reads its state from the URL:
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
export default function Results() {
|
|
194
|
-
const params = Toil.useSearchParams();
|
|
195
|
-
const q = params.get('q') ?? '';
|
|
196
|
-
return <p>Results for {q}</p>;
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Prefetching
|
|
201
|
-
|
|
202
|
-
toiljs prefetches routes before you navigate to them, so a click resolves with nothing left to download. There are two intents, both automatic:
|
|
203
|
-
|
|
204
|
-
- **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.
|
|
205
|
-
- **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.
|
|
206
|
-
|
|
207
|
-
Prefetching 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.
|
|
208
|
-
|
|
209
|
-
Opt a single link out with `prefetch={false}`, which emits a `data-no-prefetch` attribute the prefetcher respects:
|
|
210
|
-
|
|
211
|
-
```tsx
|
|
212
|
-
<Toil.Link href="/huge-report" prefetch={false}>Annual report</Toil.Link>
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
You can also warm a route yourself, for example right before an imperative `navigate`, with the standalone `Toil.prefetch(href)`:
|
|
216
|
-
|
|
217
|
-
```tsx
|
|
218
|
-
<button
|
|
219
|
-
onPointerEnter={() => Toil.prefetch('/dashboard')}
|
|
220
|
-
onClick={() => Toil.navigate('/dashboard')}>
|
|
221
|
-
Open dashboard
|
|
222
|
-
</button>
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Scroll restoration
|
|
226
|
-
|
|
227
|
-
toiljs manages scroll for you (it switches off the browser's automatic restoration and does the intuitive thing per navigation type):
|
|
228
|
-
|
|
229
|
-
- **A push navigation** (a `Link` click, `navigate`, `router.push`) **scrolls to the top** of the new page.
|
|
230
|
-
- **Back and forward restore** the scroll position you had on that entry, so returning to a long list lands you where you were.
|
|
231
|
-
- **A `#hash` target** scrolls that element into view instead of jumping to the top.
|
|
232
|
-
|
|
233
|
-
To 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:
|
|
234
|
-
|
|
235
|
-
```tsx
|
|
236
|
-
<Toil.Link href="/settings/billing" scroll={false}>Billing</Toil.Link>
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
navigate('/settings/billing', { scroll: false });
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
## Animated transitions
|
|
244
|
-
|
|
245
|
-
Two optional effects can animate navigations. Both are **off by default** and are normally enabled from `toil.config.ts`, not in code:
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
// toil.config.ts
|
|
249
|
-
export default {
|
|
250
|
-
client: {
|
|
251
|
-
viewTransitions: true, // browser View Transitions API (a crossfade between pages)
|
|
252
|
-
transitions: true, // React transition: keep the old page visible while the next loads
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`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).
|
|
258
|
-
|
|
259
|
-
Config is the normal way to turn these on. For a manual override at runtime, the setters are `Toil.setViewTransitions(enabled)` and `Toil.setTransitions(enabled)`:
|
|
260
|
-
|
|
261
|
-
```tsx
|
|
262
|
-
Toil.setViewTransitions(true);
|
|
263
|
-
Toil.setTransitions(false);
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
You rarely need the setters; prefer the config keys unless you are toggling an effect dynamically.
|
|
267
|
-
|
|
268
|
-
## Types
|
|
269
|
-
|
|
270
|
-
Almost 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`:
|
|
271
|
-
|
|
272
|
-
```tsx
|
|
273
|
-
import type {
|
|
274
|
-
LinkProps,
|
|
275
|
-
NavLinkProps,
|
|
276
|
-
NavLinkState,
|
|
277
|
-
NavigateOptions,
|
|
278
|
-
RouterInstance,
|
|
279
|
-
} from 'toiljs/client';
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
For example, a component that forwards `Link` props, or a helper typed against the router handle:
|
|
283
|
-
|
|
284
|
-
```tsx
|
|
285
|
-
import type { RouterInstance } from 'toiljs/client';
|
|
286
|
-
|
|
287
|
-
function logoutThenGo(router: RouterInstance) {
|
|
288
|
-
router.replace('/login');
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## Related
|
|
293
|
-
|
|
294
|
-
- [Routing](./routing.md): how files become URLs, dynamic params, layouts, and templates.
|
|
295
|
-
- [Fetching data](./data-fetching.md): loaders, the typed backend clients, forms, and revalidation.
|
|
296
|
-
- [Rendering and SSR](./rendering.md): what renders on the server versus the browser, and how hydration fits with client navigation.
|