toiljs 0.0.11 → 0.0.14
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/README.md +3 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +58 -30
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +33 -24
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -41
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
- package/examples/basic/client/routes/about.tsx +21 -22
- package/examples/basic/client/routes/blog/[id].tsx +26 -18
- package/examples/basic/client/routes/features/actions.tsx +67 -67
- package/examples/basic/client/routes/features/error/index.tsx +27 -27
- package/examples/basic/client/routes/features/head.tsx +38 -38
- package/examples/basic/client/routes/features/index.tsx +83 -75
- package/examples/basic/client/routes/features/realtime.tsx +34 -32
- package/examples/basic/client/routes/features/script.tsx +31 -31
- package/examples/basic/client/routes/features/seo.tsx +39 -39
- package/examples/basic/client/routes/features/template/index.tsx +20 -20
- package/examples/basic/client/routes/features/template/template.tsx +16 -18
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
- package/examples/basic/client/routes/gallery/index.tsx +42 -42
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -96
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/package.json +2 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +45 -27
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -145
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +1 -1
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +1 -2
- package/test/dom/router-loading.test.tsx +1 -1
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +30 -8
- package/test/update.test.ts +44 -0
|
@@ -1,197 +1,193 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Development-only error overlay. In dev, surfaces errors that would otherwise leave a blank page or
|
|
3
|
-
* live only in the console: uncaught render errors (incl. those thrown by a loader during render),
|
|
4
|
-
* plus `window` `error` / `unhandledrejection` events. Shows the message, stack, and (for render
|
|
5
|
-
* errors) the React component stack, with Dismiss / Reload. Inert in production builds.
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.unsubscribe
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Development-only error overlay. In dev, surfaces errors that would otherwise leave a blank page or
|
|
3
|
+
* live only in the console: uncaught render errors (incl. those thrown by a loader during render),
|
|
4
|
+
* plus `window` `error` / `unhandledrejection` events. Shows the message, stack, and (for render
|
|
5
|
+
* errors) the React component stack, with Dismiss / Reload. Inert in production builds.
|
|
6
|
+
*/
|
|
7
|
+
import { Component, type CSSProperties, type ErrorInfo, type ReactNode, useSyncExternalStore, } from 'react';
|
|
8
|
+
|
|
9
|
+
/** A captured dev error. */
|
|
10
|
+
interface DevError {
|
|
11
|
+
readonly error: Error;
|
|
12
|
+
readonly componentStack?: string;
|
|
13
|
+
/** Where it came from, a render boundary, a window `error`, or an unhandled rejection. */
|
|
14
|
+
readonly source: 'render' | 'window' | 'unhandledrejection';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let current: DevError | null = null;
|
|
18
|
+
const listeners = new Set<() => void>();
|
|
19
|
+
|
|
20
|
+
function emit(): void {
|
|
21
|
+
for (const listener of listeners) listener();
|
|
22
|
+
}
|
|
23
|
+
function setDevError(next: DevError | null): void {
|
|
24
|
+
current = next;
|
|
25
|
+
emit();
|
|
26
|
+
}
|
|
27
|
+
function subscribe(listener: () => void): () => void {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when running under Vite's dev server (replaced at build time; falsy in production). */
|
|
35
|
+
export function isDevMode(): boolean {
|
|
36
|
+
try {
|
|
37
|
+
return Boolean((import.meta as { env?: { DEV?: boolean } }).env?.DEV);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let windowBound = false;
|
|
44
|
+
/** Wires `window` error / unhandledrejection into the overlay (idempotent; dev only). */
|
|
45
|
+
export function initDevErrorOverlay(): void {
|
|
46
|
+
if (windowBound || typeof window === 'undefined') return;
|
|
47
|
+
windowBound = true;
|
|
48
|
+
window.addEventListener('error', (event) => {
|
|
49
|
+
if (event.error instanceof Error) setDevError({ error: event.error, source: 'window' });
|
|
50
|
+
});
|
|
51
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
52
|
+
const reason: unknown = event.reason;
|
|
53
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
54
|
+
setDevError({ error, source: 'unhandledrejection' });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface BoundaryProps {
|
|
59
|
+
readonly children: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
interface BoundaryState {
|
|
62
|
+
readonly crashed: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Catches render errors in its subtree and reports them to the overlay. While crashed it renders
|
|
67
|
+
* nothing (the subtree threw); it recovers when the overlay is dismissed. Class component because
|
|
68
|
+
* React error boundaries have no hook equivalent.
|
|
69
|
+
*/
|
|
70
|
+
export class DevErrorBoundary extends Component<BoundaryProps, BoundaryState> {
|
|
71
|
+
public state: BoundaryState = { crashed: false };
|
|
72
|
+
private unsubscribe: (() => void) | undefined;
|
|
73
|
+
|
|
74
|
+
public static getDerivedStateFromError(): BoundaryState {
|
|
75
|
+
return { crashed: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public override componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
79
|
+
setDevError({ error, componentStack: info.componentStack ?? undefined, source: 'render' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public override componentDidMount(): void {
|
|
83
|
+
// Recover (re-render children) once the error is dismissed from the overlay.
|
|
84
|
+
this.unsubscribe = subscribe(() => {
|
|
85
|
+
if (current === null && this.state.crashed) this.setState({ crashed: false });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public override componentWillUnmount(): void {
|
|
90
|
+
this.unsubscribe?.();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public override render(): ReactNode {
|
|
94
|
+
return this.state.crashed ? null : this.props.children;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const overlayStyle: CSSProperties = {
|
|
99
|
+
position: 'fixed',
|
|
100
|
+
inset: 0,
|
|
101
|
+
zIndex: 2147483647,
|
|
102
|
+
background: 'rgba(8, 8, 12, 0.88)',
|
|
103
|
+
color: '#f5f6fa',
|
|
104
|
+
font: '13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
105
|
+
padding: '2rem',
|
|
106
|
+
overflow: 'auto',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
justifyContent: 'center',
|
|
109
|
+
alignItems: 'flex-start',
|
|
110
|
+
};
|
|
111
|
+
const panelStyle: CSSProperties = {
|
|
112
|
+
maxWidth: 900,
|
|
113
|
+
width: '100%',
|
|
114
|
+
background: '#15151c',
|
|
115
|
+
border: '1px solid #ef4444',
|
|
116
|
+
borderRadius: 10,
|
|
117
|
+
padding: '1.25rem 1.5rem',
|
|
118
|
+
boxShadow: '0 12px 48px rgba(0,0,0,0.5)',
|
|
119
|
+
};
|
|
120
|
+
const titleStyle: CSSProperties = {
|
|
121
|
+
margin: 0,
|
|
122
|
+
color: '#ff6b6b',
|
|
123
|
+
fontSize: '1rem',
|
|
124
|
+
fontWeight: 700,
|
|
125
|
+
wordBreak: 'break-word',
|
|
126
|
+
};
|
|
127
|
+
const preStyle: CSSProperties = {
|
|
128
|
+
whiteSpace: 'pre-wrap',
|
|
129
|
+
wordBreak: 'break-word',
|
|
130
|
+
margin: '0.75rem 0 0',
|
|
131
|
+
color: '#c8cee0',
|
|
132
|
+
};
|
|
133
|
+
const buttonStyle: CSSProperties = {
|
|
134
|
+
font: 'inherit',
|
|
135
|
+
color: '#f5f6fa',
|
|
136
|
+
background: '#2a2a36',
|
|
137
|
+
border: '1px solid #3a3a48',
|
|
138
|
+
borderRadius: 6,
|
|
139
|
+
padding: '0.4em 1em',
|
|
140
|
+
cursor: 'pointer',
|
|
141
|
+
marginRight: '0.5rem',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const SOURCE_LABEL: Record<DevError['source'], string> = {
|
|
145
|
+
render: 'Render error',
|
|
146
|
+
window: 'Uncaught error',
|
|
147
|
+
unhandledrejection: 'Unhandled promise rejection',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Renders the overlay when a dev error is captured. Mount once at the app root (dev only). */
|
|
151
|
+
export function DevErrorOverlay(): ReactNode {
|
|
152
|
+
const devError = useSyncExternalStore(
|
|
153
|
+
subscribe,
|
|
154
|
+
() => current,
|
|
155
|
+
() => null,
|
|
156
|
+
);
|
|
157
|
+
if (!devError) return null;
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
style={overlayStyle}
|
|
161
|
+
role="alert">
|
|
162
|
+
<div style={panelStyle}>
|
|
163
|
+
<p style={titleStyle}>
|
|
164
|
+
{SOURCE_LABEL[devError.source]}, {devError.error.name}: {devError.error.message}
|
|
165
|
+
</p>
|
|
166
|
+
{devError.error.stack !== undefined && (
|
|
167
|
+
<pre style={preStyle}>{devError.error.stack}</pre>
|
|
168
|
+
)}
|
|
169
|
+
{devError.componentStack !== undefined && (
|
|
170
|
+
<pre style={{ ...preStyle, color: '#8b9ab4' }}>{devError.componentStack}</pre>
|
|
171
|
+
)}
|
|
172
|
+
<div style={{ marginTop: '1.25rem' }}>
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
style={buttonStyle}
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setDevError(null);
|
|
178
|
+
}}>
|
|
179
|
+
Dismiss
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
style={buttonStyle}
|
|
184
|
+
onClick={() => {
|
|
185
|
+
window.location.reload();
|
|
186
|
+
}}>
|
|
187
|
+
Reload
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -1,92 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route metadata, the declarative SEO counterpart to `useHead`/`<Head>`. A route file may
|
|
3
|
-
* `export const metadata` (static) or `export const generateMetadata` (dynamic, using its loader
|
|
4
|
-
* data); the compiler-driven loader resolves it to a {@link HeadSpec} that the router applies as the
|
|
5
|
-
* route's baseline head (component-level `useHead`/`<Head>` still compose on top and can override).
|
|
6
|
-
*/
|
|
7
|
-
import type { HeadSpec, LinkTag, MetaTag } from './head.js';
|
|
8
|
-
import type { RouteParams } from '../routing/match.js';
|
|
9
|
-
|
|
10
|
-
/** OpenGraph fields, expanded to `og:*` meta tags. */
|
|
11
|
-
export interface OpenGraph {
|
|
12
|
-
readonly title?: string;
|
|
13
|
-
readonly description?: string;
|
|
14
|
-
readonly type?: string;
|
|
15
|
-
readonly url?: string;
|
|
16
|
-
readonly image?: string;
|
|
17
|
-
readonly siteName?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** A route's metadata. Convenience fields expand to the right `<meta>`/`<link>` tags. */
|
|
21
|
-
export interface Metadata {
|
|
22
|
-
/** Document title. */
|
|
23
|
-
readonly title?: string;
|
|
24
|
-
/** Template applied to the title (`%s` = the title), e.g. `'%s · toiljs'`. */
|
|
25
|
-
readonly titleTemplate?: string;
|
|
26
|
-
/** `<meta name="description">`. */
|
|
27
|
-
readonly description?: string;
|
|
28
|
-
/** `<meta name="keywords">`, joined with `, ` if an array. */
|
|
29
|
-
readonly keywords?: string | readonly string[];
|
|
30
|
-
/** `<link rel="canonical">`. */
|
|
31
|
-
readonly canonical?: string;
|
|
32
|
-
/** `<meta name="robots">`, e.g. `'noindex, nofollow'`. */
|
|
33
|
-
readonly robots?: string;
|
|
34
|
-
/** `<meta name="theme-color">`. */
|
|
35
|
-
readonly themeColor?: string;
|
|
36
|
-
/** OpenGraph (`og:*`) tags. */
|
|
37
|
-
readonly openGraph?: OpenGraph;
|
|
38
|
-
/** Escape hatch: extra raw `<meta>` tags. */
|
|
39
|
-
readonly meta?: readonly MetaTag[];
|
|
40
|
-
/** Escape hatch: extra raw `<link>` tags. */
|
|
41
|
-
readonly link?: readonly LinkTag[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Arguments passed to {@link GenerateMetadata}: route params, query, and the loader's data. */
|
|
45
|
-
export interface GenerateMetadataArgs<T = unknown> {
|
|
46
|
-
readonly params: RouteParams;
|
|
47
|
-
readonly searchParams: URLSearchParams;
|
|
48
|
-
readonly data: T;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** A route's `export const generateMetadata`, dynamic metadata derived from params/query/loader data. */
|
|
52
|
-
export type GenerateMetadata<T = unknown> = (
|
|
53
|
-
args: GenerateMetadataArgs<T>,
|
|
54
|
-
) => Metadata | Promise<Metadata>;
|
|
55
|
-
|
|
56
|
-
/** Expands a {@link Metadata} into a {@link HeadSpec} (title + concrete meta/link tags). */
|
|
57
|
-
export function resolveMetadata(metadata: Metadata): HeadSpec {
|
|
58
|
-
const meta: MetaTag[] = [];
|
|
59
|
-
if (metadata.description !== undefined) {
|
|
60
|
-
meta.push({ name: 'description', content: metadata.description });
|
|
61
|
-
}
|
|
62
|
-
if (metadata.keywords !== undefined) {
|
|
63
|
-
const content =
|
|
64
|
-
typeof metadata.keywords === 'string'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
['og:
|
|
77
|
-
['og:
|
|
78
|
-
['og:
|
|
79
|
-
['og:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Route metadata, the declarative SEO counterpart to `useHead`/`<Head>`. A route file may
|
|
3
|
+
* `export const metadata` (static) or `export const generateMetadata` (dynamic, using its loader
|
|
4
|
+
* data); the compiler-driven loader resolves it to a {@link HeadSpec} that the router applies as the
|
|
5
|
+
* route's baseline head (component-level `useHead`/`<Head>` still compose on top and can override).
|
|
6
|
+
*/
|
|
7
|
+
import type { HeadSpec, LinkTag, MetaTag } from './head.js';
|
|
8
|
+
import type { RouteParams } from '../routing/match.js';
|
|
9
|
+
|
|
10
|
+
/** OpenGraph fields, expanded to `og:*` meta tags. */
|
|
11
|
+
export interface OpenGraph {
|
|
12
|
+
readonly title?: string;
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
readonly type?: string;
|
|
15
|
+
readonly url?: string;
|
|
16
|
+
readonly image?: string;
|
|
17
|
+
readonly siteName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A route's metadata. Convenience fields expand to the right `<meta>`/`<link>` tags. */
|
|
21
|
+
export interface Metadata {
|
|
22
|
+
/** Document title. */
|
|
23
|
+
readonly title?: string;
|
|
24
|
+
/** Template applied to the title (`%s` = the title), e.g. `'%s · toiljs'`. */
|
|
25
|
+
readonly titleTemplate?: string;
|
|
26
|
+
/** `<meta name="description">`. */
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
/** `<meta name="keywords">`, joined with `, ` if an array. */
|
|
29
|
+
readonly keywords?: string | readonly string[];
|
|
30
|
+
/** `<link rel="canonical">`. */
|
|
31
|
+
readonly canonical?: string;
|
|
32
|
+
/** `<meta name="robots">`, e.g. `'noindex, nofollow'`. */
|
|
33
|
+
readonly robots?: string;
|
|
34
|
+
/** `<meta name="theme-color">`. */
|
|
35
|
+
readonly themeColor?: string;
|
|
36
|
+
/** OpenGraph (`og:*`) tags. */
|
|
37
|
+
readonly openGraph?: OpenGraph;
|
|
38
|
+
/** Escape hatch: extra raw `<meta>` tags. */
|
|
39
|
+
readonly meta?: readonly MetaTag[];
|
|
40
|
+
/** Escape hatch: extra raw `<link>` tags. */
|
|
41
|
+
readonly link?: readonly LinkTag[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Arguments passed to {@link GenerateMetadata}: route params, query, and the loader's data. */
|
|
45
|
+
export interface GenerateMetadataArgs<T = unknown> {
|
|
46
|
+
readonly params: RouteParams;
|
|
47
|
+
readonly searchParams: URLSearchParams;
|
|
48
|
+
readonly data: T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A route's `export const generateMetadata`, dynamic metadata derived from params/query/loader data. */
|
|
52
|
+
export type GenerateMetadata<T = unknown> = (
|
|
53
|
+
args: GenerateMetadataArgs<T>,
|
|
54
|
+
) => Metadata | Promise<Metadata>;
|
|
55
|
+
|
|
56
|
+
/** Expands a {@link Metadata} into a {@link HeadSpec} (title + concrete meta/link tags). */
|
|
57
|
+
export function resolveMetadata(metadata: Metadata): HeadSpec {
|
|
58
|
+
const meta: MetaTag[] = [];
|
|
59
|
+
if (metadata.description !== undefined) {
|
|
60
|
+
meta.push({ name: 'description', content: metadata.description });
|
|
61
|
+
}
|
|
62
|
+
if (metadata.keywords !== undefined) {
|
|
63
|
+
const content =
|
|
64
|
+
typeof metadata.keywords === 'string'
|
|
65
|
+
? metadata.keywords
|
|
66
|
+
: metadata.keywords.join(', ');
|
|
67
|
+
meta.push({ name: 'keywords', content });
|
|
68
|
+
}
|
|
69
|
+
if (metadata.robots !== undefined) meta.push({ name: 'robots', content: metadata.robots });
|
|
70
|
+
if (metadata.themeColor !== undefined) {
|
|
71
|
+
meta.push({ name: 'theme-color', content: metadata.themeColor });
|
|
72
|
+
}
|
|
73
|
+
const og = metadata.openGraph;
|
|
74
|
+
if (og) {
|
|
75
|
+
const pairs: readonly [string, string | undefined][] = [
|
|
76
|
+
['og:title', og.title],
|
|
77
|
+
['og:description', og.description],
|
|
78
|
+
['og:type', og.type],
|
|
79
|
+
['og:url', og.url],
|
|
80
|
+
['og:image', og.image],
|
|
81
|
+
['og:site_name', og.siteName],
|
|
82
|
+
];
|
|
83
|
+
for (const [property, content] of pairs) {
|
|
84
|
+
if (content !== undefined) meta.push({ property, content });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (metadata.meta) meta.push(...metadata.meta);
|
|
88
|
+
|
|
89
|
+
const link: LinkTag[] = [];
|
|
90
|
+
if (metadata.canonical !== undefined) link.push({ rel: 'canonical', href: metadata.canonical });
|
|
91
|
+
if (metadata.link) link.push(...metadata.link);
|
|
92
|
+
|
|
93
|
+
return { title: metadata.title, titleTemplate: metadata.titleTemplate, meta, link };
|
|
94
|
+
}
|