@rsc-kit/core 0.16.2 → 0.17.0
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/dist/appAssets.d.ts +4 -1
- package/dist/appAssets.js +55 -19
- package/dist/appAssets.js.map +1 -1
- package/dist/buildReport.d.ts +6 -1
- package/dist/buildReport.js +16 -18
- package/dist/buildReport.js.map +1 -1
- package/dist/cache.js +8 -4
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.d.ts +32 -0
- package/dist/clientEntries.js +107 -0
- package/dist/clientEntries.js.map +1 -0
- package/dist/clientImports.d.ts +36 -0
- package/dist/clientImports.js +36 -0
- package/dist/clientImports.js.map +1 -0
- package/dist/devReload.d.ts +19 -0
- package/dist/devReload.js +32 -0
- package/dist/devReload.js.map +1 -0
- package/dist/js/Form.js +54 -6
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +0 -3
- package/dist/js/Link.js +3 -6
- package/dist/js/Link.js.map +1 -1
- package/dist/js/SegmentBoundary.d.ts +38 -2
- package/dist/js/SegmentBoundary.js +51 -13
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +72 -11
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +13 -0
- package/dist/js/devNotice.js +55 -0
- package/dist/js/devNotice.js.map +1 -0
- package/dist/js/fallbackReport.d.ts +2 -0
- package/dist/js/fallbackReport.js +51 -0
- package/dist/js/fallbackReport.js.map +1 -0
- package/dist/js/navigate.d.ts +1 -1
- package/dist/js/navigate.js +55 -23
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/segmentStore.d.ts +2 -0
- package/dist/js/segmentStore.js +21 -1
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.d.ts +17 -0
- package/dist/js/staleAssets.js +48 -0
- package/dist/js/staleAssets.js.map +1 -0
- package/dist/js/standardSchema.d.ts +6 -0
- package/dist/js/standardSchema.js +19 -1
- package/dist/js/standardSchema.js.map +1 -1
- package/dist/js/useLinkStatus.d.ts +14 -1
- package/dist/js/useLinkStatus.js +15 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/js/useSearchParams.d.ts +6 -0
- package/dist/js/useSearchParams.js +16 -3
- package/dist/js/useSearchParams.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +41 -0
- package/dist/metadataRoutes.js +138 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +43 -10
- package/dist/prerender.js.map +1 -1
- package/dist/reactCache.d.ts +1 -0
- package/dist/reactCache.js +52 -0
- package/dist/reactCache.js.map +1 -0
- package/dist/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.js.map +1 -1
- package/dist/useSsr.d.ts +19 -0
- package/dist/useSsr.js +104 -0
- package/dist/useSsr.js.map +1 -0
- package/dist/vite.d.ts +48 -11
- package/dist/vite.js +526 -26
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* root, nothing paints, and the build refuses the route and says why — which
|
|
15
15
|
* is the same answer it gives for reading the request too early on the server.
|
|
16
16
|
*
|
|
17
|
+
* The dev server renders per request and could answer with that request's
|
|
18
|
+
* query string; it does not, on purpose. A page has one shape — a stored
|
|
19
|
+
* shell with the query read in the browser — and dev shows that shape, so a
|
|
20
|
+
* boundary missing in dev is the boundary the build will refuse.
|
|
21
|
+
*
|
|
17
22
|
* Returning an empty URLSearchParams instead would be worse than either: the
|
|
18
23
|
* page would be stored showing results for no query at all, and nothing would
|
|
19
24
|
* say so.
|
|
@@ -56,10 +61,18 @@ function getSnapshot() {
|
|
|
56
61
|
}
|
|
57
62
|
return cached.params;
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The digest a stored page carries where the query was read under the
|
|
66
|
+
* boundary that caught it. The client recognises it on hydration and does
|
|
67
|
+
* not report the fallback as an error, because it is the designed path.
|
|
68
|
+
*/
|
|
69
|
+
export const SEARCH_PARAMS_FALLBACK = "rsc-kit:search-params-fallback";
|
|
59
70
|
function getServerSnapshot() {
|
|
60
|
-
|
|
61
|
-
"Wrap the component in <Suspense> — or add a
|
|
62
|
-
"is stored and the real value arrives in the browser.");
|
|
71
|
+
const error = new Error("useSearchParams() was read while rendering on the server, where there is no query string to give: " +
|
|
72
|
+
"a stored page would serve one visitor's query to everyone. Wrap the component in <Suspense> — or add a " +
|
|
73
|
+
"loading.tsx beside the page — so the fallback is stored and the real value arrives in the browser.");
|
|
74
|
+
error.digest = SEARCH_PARAMS_FALLBACK;
|
|
75
|
+
throw error;
|
|
63
76
|
}
|
|
64
77
|
export function useSearchParams() {
|
|
65
78
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSearchParams.js","sourceRoot":"","sources":["../../src/js/useSearchParams.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb
|
|
1
|
+
{"version":3,"file":"useSearchParams.js","sourceRoot":"","sources":["../../src/js/useSearchParams.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AAExC;;;;;;GAMG;AACH,IAAI,MAAM,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,eAAe,EAAE,EAAE,CAAC;AAE3D,SAAS,aAAa;IACpB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;AACrE,CAAC;AAED,SAAS,MAAM;IACb,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB,SAAS,SAAS,CAAC,QAAoB;IACrC,0EAA0E;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,SAAS,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC5C,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAExB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAE/B,IAAI,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3D,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,gCAAgC,CAAC;AAEvE,SAAS,iBAAiB;IACxB,MAAM,KAAK,GAAG,IAAI,KAAK,CACrB,oGAAoG;QAClG,yGAAyG;QACzG,oGAAoG,CACxE,CAAC;IAEjC,KAAK,CAAC,MAAM,GAAG,sBAAsB,CAAC;IAEtC,MAAM,KAAK,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The query string, live across navigations.\n *\n * Deliberately unlike usePathname, which answers with the url being rendered.\n * A pathname is fixed for a given stored page; a query string is not — the\n * same route is asked for with `?q=shoes` and `?q=hats`, and a page stored\n * holding one of them would serve it to everyone.\n *\n * So there is no server snapshot to give, and this throws rather than\n * inventing one. React treats an error thrown during SSR as recoverable at the\n * nearest Suspense boundary: the fallback is what gets stored, and the client\n * renders the real thing on hydration. Without a boundary the error reaches the\n * root, nothing paints, and the build refuses the route and says why — which\n * is the same answer it gives for reading the request too early on the server.\n *\n * The dev server renders per request and could answer with that request's\n * query string; it does not, on purpose. A page has one shape — a stored\n * shell with the query read in the browser — and dev shows that shape, so a\n * boundary missing in dev is the boundary the build will refuse.\n *\n * Returning an empty URLSearchParams instead would be worse than either: the\n * page would be stored showing results for no query at all, and nothing would\n * say so.\n */\n\nimport { useSyncExternalStore } from \"react\";\n\nconst listeners = new Set<() => void>();\n\n/**\n * Cached by the string it was parsed from.\n *\n * useSyncExternalStore compares snapshots by identity, so handing back a fresh\n * URLSearchParams on every read reads as \"changed every time\" and loops until\n * React gives up.\n */\nlet cached = { search: \"\", params: new URLSearchParams() };\n\nfunction currentSearch(): string {\n return typeof window === \"undefined\" ? \"\" : window.location.search;\n}\n\nfunction notify(): void {\n listeners.forEach((fn) => fn());\n}\n\nlet listening = false;\n\nfunction subscribe(callback: () => void): () => void {\n // On the first subscriber rather than at module load. A listener attached\n // when the module is evaluated is attached to whatever `window` exists at\n // that moment — which in a test that installs a DOM after its imports is\n // none, and the hook silently never updates. Attaching here is the shape\n // useSyncExternalStore expects and is correct wherever the module loads.\n if (!listening && typeof window !== \"undefined\") {\n window.addEventListener(\"rsc-navigate\", notify);\n window.addEventListener(\"popstate\", notify);\n listening = true;\n }\n\n listeners.add(callback);\n\n return () => listeners.delete(callback);\n}\n\nfunction getSnapshot(): URLSearchParams {\n const search = currentSearch();\n\n if (search !== cached.search) {\n cached = { search, params: new URLSearchParams(search) };\n }\n\n return cached.params;\n}\n\n/**\n * The digest a stored page carries where the query was read under the\n * boundary that caught it. The client recognises it on hydration and does\n * not report the fallback as an error, because it is the designed path.\n */\nexport const SEARCH_PARAMS_FALLBACK = \"rsc-kit:search-params-fallback\";\n\nfunction getServerSnapshot(): URLSearchParams {\n const error = new Error(\n \"useSearchParams() was read while rendering on the server, where there is no query string to give: \" +\n \"a stored page would serve one visitor's query to everyone. Wrap the component in <Suspense> — or add a \" +\n \"loading.tsx beside the page — so the fallback is stored and the real value arrives in the browser.\",\n ) as Error & { digest?: string };\n\n error.digest = SEARCH_PARAMS_FALLBACK;\n\n throw error;\n}\n\nexport function useSearchParams(): URLSearchParams {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"]}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface IconDescriptor {
|
|
|
5
5
|
color?: string;
|
|
6
6
|
rel?: string;
|
|
7
7
|
media?: string;
|
|
8
|
-
fetchPriority?:
|
|
8
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
9
9
|
}
|
|
10
10
|
export type IconURL = string | URL;
|
|
11
11
|
export interface Icons {
|
|
@@ -40,13 +40,13 @@ export interface OpenGraph {
|
|
|
40
40
|
/** Absolute, or relative to `metadataBase`. */
|
|
41
41
|
url?: string | URL;
|
|
42
42
|
siteName?: string;
|
|
43
|
-
type?:
|
|
43
|
+
type?: "website" | "article" | "profile" | "book" | (string & {});
|
|
44
44
|
locale?: string;
|
|
45
45
|
images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];
|
|
46
46
|
}
|
|
47
47
|
/** The same card for X, which reads `name=` rather than `property=`. */
|
|
48
48
|
export interface Twitter {
|
|
49
|
-
card?:
|
|
49
|
+
card?: "summary" | "summary_large_image" | "app" | "player";
|
|
50
50
|
title?: string;
|
|
51
51
|
description?: string;
|
|
52
52
|
/** The site's account, `@handle`. */
|
|
@@ -55,13 +55,35 @@ export interface Twitter {
|
|
|
55
55
|
creator?: string;
|
|
56
56
|
images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];
|
|
57
57
|
}
|
|
58
|
+
export interface Robots {
|
|
59
|
+
index?: boolean;
|
|
60
|
+
follow?: boolean;
|
|
61
|
+
noarchive?: boolean;
|
|
62
|
+
nosnippet?: boolean;
|
|
63
|
+
noimageindex?: boolean;
|
|
64
|
+
nocache?: boolean;
|
|
65
|
+
notranslate?: boolean;
|
|
66
|
+
indexifembedded?: boolean;
|
|
67
|
+
nositelinkssearchbox?: boolean;
|
|
68
|
+
unavailable_after?: string;
|
|
69
|
+
"max-video-preview"?: number | string;
|
|
70
|
+
"max-image-preview"?: "none" | "standard" | "large";
|
|
71
|
+
"max-snippet"?: number;
|
|
72
|
+
googleBot?: string | Omit<Robots, "googleBot">;
|
|
73
|
+
}
|
|
58
74
|
export interface Metadata {
|
|
59
75
|
/** A string on a page; a template on a layout, applied to the pages below it. */
|
|
60
76
|
title?: string | TitleTemplate;
|
|
61
77
|
description?: string;
|
|
62
78
|
keywords?: string | string[];
|
|
63
79
|
author?: string;
|
|
64
|
-
|
|
80
|
+
/**
|
|
81
|
+
* A string, or the object Next takes: `{ index: false, follow: false }`
|
|
82
|
+
* becomes `<meta name="robots" content="noindex, nofollow">`, the flags
|
|
83
|
+
* by name, the limits as `name:value`; `googleBot` is the same shape for
|
|
84
|
+
* `<meta name="googlebot">`.
|
|
85
|
+
*/
|
|
86
|
+
robots?: string | Robots;
|
|
65
87
|
/**
|
|
66
88
|
* Where the site lives, so a relative image or url can be made absolute.
|
|
67
89
|
*
|
|
@@ -77,27 +99,27 @@ export interface Metadata {
|
|
|
77
99
|
openGraph?: OpenGraph;
|
|
78
100
|
twitter?: Twitter;
|
|
79
101
|
/** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */
|
|
80
|
-
|
|
102
|
+
"og:title"?: string;
|
|
81
103
|
/** @deprecated Use `openGraph.description`. */
|
|
82
|
-
|
|
104
|
+
"og:description"?: string;
|
|
83
105
|
/** @deprecated Use `openGraph.images`. */
|
|
84
|
-
|
|
106
|
+
"og:image"?: string;
|
|
85
107
|
/** @deprecated Use `openGraph.url`. */
|
|
86
|
-
|
|
108
|
+
"og:url"?: string;
|
|
87
109
|
/** @deprecated Use `openGraph.type`. */
|
|
88
|
-
|
|
110
|
+
"og:type"?: string;
|
|
89
111
|
/** @deprecated Use `openGraph.siteName`. */
|
|
90
|
-
|
|
112
|
+
"og:site_name"?: string;
|
|
91
113
|
/** @deprecated Use `twitter.card`. */
|
|
92
|
-
|
|
114
|
+
"twitter:card"?: string;
|
|
93
115
|
/** @deprecated Use `twitter.title`. */
|
|
94
|
-
|
|
116
|
+
"twitter:title"?: string;
|
|
95
117
|
/** @deprecated Use `twitter.description`. */
|
|
96
|
-
|
|
118
|
+
"twitter:description"?: string;
|
|
97
119
|
/** @deprecated Use `twitter.images`. */
|
|
98
|
-
|
|
120
|
+
"twitter:image"?: string;
|
|
99
121
|
/** @deprecated Use `twitter.site`. */
|
|
100
|
-
|
|
122
|
+
"twitter:site"?: string;
|
|
101
123
|
/**
|
|
102
124
|
* Any other meta tag, by name.
|
|
103
125
|
*
|
|
@@ -125,3 +147,60 @@ export type GenerateMetadata<P = Record<string, string>> = (args: {
|
|
|
125
147
|
params: Promise<P>;
|
|
126
148
|
searchParams: Promise<URLSearchParams>;
|
|
127
149
|
}) => Metadata | Promise<Metadata>;
|
|
150
|
+
/** One rule block of a robots.txt: which agents, what they may and may not fetch. */
|
|
151
|
+
export interface RobotsRule {
|
|
152
|
+
userAgent?: string | string[];
|
|
153
|
+
allow?: string | string[];
|
|
154
|
+
disallow?: string | string[];
|
|
155
|
+
crawlDelay?: number;
|
|
156
|
+
}
|
|
157
|
+
/** One url of a sitemap. `url` may be relative when the root layout has a metadataBase. */
|
|
158
|
+
export interface SitemapEntry {
|
|
159
|
+
url: string;
|
|
160
|
+
lastModified?: string | Date;
|
|
161
|
+
changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
162
|
+
priority?: number;
|
|
163
|
+
/** Image urls on this page, for image search. */
|
|
164
|
+
images?: string[];
|
|
165
|
+
/** Translations of this page: language tag to url. */
|
|
166
|
+
alternates?: {
|
|
167
|
+
languages?: Record<string, string>;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** A link in an llms.txt section. */
|
|
171
|
+
export interface LlmsLink {
|
|
172
|
+
title: string;
|
|
173
|
+
url: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
}
|
|
176
|
+
export interface LlmsSection {
|
|
177
|
+
title: string;
|
|
178
|
+
links: LlmsLink[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The files a site describes itself with, each from a file beside the root
|
|
182
|
+
* layout and written the way Next writes them:
|
|
183
|
+
*
|
|
184
|
+
* app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots
|
|
185
|
+
* app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap
|
|
186
|
+
* app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms
|
|
187
|
+
*
|
|
188
|
+
* Each may return a string instead, served as written. A relative url in any
|
|
189
|
+
* of them is made absolute with the root layout's metadataBase.
|
|
190
|
+
*/
|
|
191
|
+
export declare namespace MetadataRoute {
|
|
192
|
+
type Robots = {
|
|
193
|
+
rules: RobotsRule | RobotsRule[];
|
|
194
|
+
sitemap?: string | string[];
|
|
195
|
+
host?: string;
|
|
196
|
+
};
|
|
197
|
+
type Sitemap = SitemapEntry[];
|
|
198
|
+
/** The llms.txt shape at llmstxt.org: a title, a summary, then sections of links. */
|
|
199
|
+
type Llms = {
|
|
200
|
+
title: string;
|
|
201
|
+
summary?: string;
|
|
202
|
+
/** Paragraphs after the summary, before the sections. */
|
|
203
|
+
details?: string | string[];
|
|
204
|
+
sections?: LlmsSection[];
|
|
205
|
+
};
|
|
206
|
+
}
|
package/dist/metadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM","sourcesContent":["// What a page says about itself, as importable types.\n//\n// import type { Metadata } from '@rsc-kit/core/metadata'\n//\n// export const metadata: Metadata = { title: 'Orders' }\n//\n// Imported rather than ambient, and that is the whole point of the move. An\n// ambient declaration has to be COPIED into the project, which means it is not\n// there until the build has run once — so a freshly cloned app reports \"Cannot\n// find name 'Metadata'\" on every page until someone runs the dev server. An\n// import resolves from node_modules the moment dependencies are installed.\n//\n// The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these\n// rather than restating them, so there is one definition and two ways to reach\n// it.\n\nexport interface IconDescriptor {\n url: string | URL
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM","sourcesContent":["// What a page says about itself, as importable types.\n//\n// import type { Metadata } from '@rsc-kit/core/metadata'\n//\n// export const metadata: Metadata = { title: 'Orders' }\n//\n// Imported rather than ambient, and that is the whole point of the move. An\n// ambient declaration has to be COPIED into the project, which means it is not\n// there until the build has run once — so a freshly cloned app reports \"Cannot\n// find name 'Metadata'\" on every page until someone runs the dev server. An\n// import resolves from node_modules the moment dependencies are installed.\n//\n// The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these\n// rather than restating them, so there is one definition and two ways to reach\n// it.\n\nexport interface IconDescriptor {\n url: string | URL;\n type?: string;\n sizes?: string;\n color?: string;\n rel?: string;\n media?: string;\n fetchPriority?: \"high\" | \"low\" | \"auto\";\n}\n\nexport type IconURL = string | URL;\n\nexport interface Icons {\n icon?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n apple?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n shortcut?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n other?: IconDescriptor | IconDescriptor[];\n}\n\n/** A layout's title, wrapping the titles of the pages beneath it. */\nexport interface TitleTemplate {\n /** `%s` stands in for the page's own title. */\n template?: string;\n /** Used by a page that exports no title of its own. */\n default?: string;\n}\n\n/** One image a share card may show. A string is its url. */\nexport interface OpenGraphImage {\n url: string | URL;\n width?: number;\n height?: number;\n alt?: string;\n type?: string;\n}\n\n/**\n * The card a link to this page unfurls into on Facebook, Slack, LinkedIn and\n * most of the rest. Rendered with `property=`, which is what those scrapers\n * read — a `name=` attribute is ignored by every one of them.\n */\nexport interface OpenGraph {\n title?: string;\n description?: string;\n /** Absolute, or relative to `metadataBase`. */\n url?: string | URL;\n siteName?: string;\n type?: \"website\" | \"article\" | \"profile\" | \"book\" | (string & {});\n locale?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\n/** The same card for X, which reads `name=` rather than `property=`. */\nexport interface Twitter {\n card?: \"summary\" | \"summary_large_image\" | \"app\" | \"player\";\n title?: string;\n description?: string;\n /** The site's account, `@handle`. */\n site?: string;\n /** The author's account, `@handle`. */\n creator?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\nexport interface Robots {\n index?: boolean;\n follow?: boolean;\n noarchive?: boolean;\n nosnippet?: boolean;\n noimageindex?: boolean;\n nocache?: boolean;\n notranslate?: boolean;\n indexifembedded?: boolean;\n nositelinkssearchbox?: boolean;\n unavailable_after?: string;\n \"max-video-preview\"?: number | string;\n \"max-image-preview\"?: \"none\" | \"standard\" | \"large\";\n \"max-snippet\"?: number;\n googleBot?: string | Omit<Robots, \"googleBot\">;\n}\n\nexport interface Metadata {\n /** A string on a page; a template on a layout, applied to the pages below it. */\n title?: string | TitleTemplate;\n description?: string;\n keywords?: string | string[];\n author?: string;\n /**\n * A string, or the object Next takes: `{ index: false, follow: false }`\n * becomes `<meta name=\"robots\" content=\"noindex, nofollow\">`, the flags\n * by name, the limits as `name:value`; `googleBot` is the same shape for\n * `<meta name=\"googlebot\">`.\n */\n robots?: string | Robots;\n /**\n * Where the site lives, so a relative image or url can be made absolute.\n *\n * metadataBase: new URL('https://example.com')\n *\n * On the root layout, once. A share-card scraper needs an absolute url and\n * several refuse a relative one; without this, `og:image` for an image in\n * `app/` is emitted relative and works in some places and not others. The\n * same name as Next, so a port carries it across unchanged.\n */\n metadataBase?: string | URL;\n icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null;\n openGraph?: OpenGraph;\n twitter?: Twitter;\n /** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */\n \"og:title\"?: string;\n /** @deprecated Use `openGraph.description`. */\n \"og:description\"?: string;\n /** @deprecated Use `openGraph.images`. */\n \"og:image\"?: string;\n /** @deprecated Use `openGraph.url`. */\n \"og:url\"?: string;\n /** @deprecated Use `openGraph.type`. */\n \"og:type\"?: string;\n /** @deprecated Use `openGraph.siteName`. */\n \"og:site_name\"?: string;\n /** @deprecated Use `twitter.card`. */\n \"twitter:card\"?: string;\n /** @deprecated Use `twitter.title`. */\n \"twitter:title\"?: string;\n /** @deprecated Use `twitter.description`. */\n \"twitter:description\"?: string;\n /** @deprecated Use `twitter.images`. */\n \"twitter:image\"?: string;\n /** @deprecated Use `twitter.site`. */\n \"twitter:site\"?: string;\n\n /**\n * Any other meta tag, by name.\n *\n * other: { 'fb:app_id': '123', 'theme-color': '#000' }\n *\n * Here rather than alongside the named keys, and that is what makes the rest\n * of this interface worth annotating. An index signature on the interface\n * itself made every key legal — so `titel` was accepted in silence, and an\n * editor offered no completions at all, because with any identifier valid\n * TypeScript reads an unfinished key as a shorthand property and goes looking\n * for a variable by that name.\n */\n other?: Record<string, string | string[] | null | undefined>;\n}\n\n/**\n * Metadata that depends on the request.\n *\n * Receives the same awaitables a page does, so one shape is learned rather\n * than two:\n *\n * export const generateMetadata: GenerateMetadata<{ slug: string }> =\n * async ({ params }) => ({ title: (await params).slug })\n */\nexport type GenerateMetadata<P = Record<string, string>> = (args: {\n params: Promise<P>;\n searchParams: Promise<URLSearchParams>;\n}) => Metadata | Promise<Metadata>;\n\n/** One rule block of a robots.txt: which agents, what they may and may not fetch. */\nexport interface RobotsRule {\n userAgent?: string | string[];\n allow?: string | string[];\n disallow?: string | string[];\n crawlDelay?: number;\n}\n\n/** One url of a sitemap. `url` may be relative when the root layout has a metadataBase. */\nexport interface SitemapEntry {\n url: string;\n lastModified?: string | Date;\n changeFrequency?:\n \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n priority?: number;\n /** Image urls on this page, for image search. */\n images?: string[];\n /** Translations of this page: language tag to url. */\n alternates?: { languages?: Record<string, string> };\n}\n\n/** A link in an llms.txt section. */\nexport interface LlmsLink {\n title: string;\n url: string;\n description?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\n/**\n * The files a site describes itself with, each from a file beside the root\n * layout and written the way Next writes them:\n *\n * app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots\n * app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap\n * app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms\n *\n * Each may return a string instead, served as written. A relative url in any\n * of them is made absolute with the root layout's metadataBase.\n */\nexport namespace MetadataRoute {\n export type Robots = {\n rules: RobotsRule | RobotsRule[];\n sitemap?: string | string[];\n host?: string;\n };\n export type Sitemap = SitemapEntry[];\n /** The llms.txt shape at llmstxt.org: a title, a summary, then sections of links. */\n export type Llms = {\n title: string;\n summary?: string;\n /** Paragraphs after the summary, before the sections. */\n details?: string | string[];\n sections?: LlmsSection[];\n };\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MetadataRoute } from "./metadata.js";
|
|
2
|
+
export declare const METADATA_ROUTES: {
|
|
3
|
+
readonly robots: {
|
|
4
|
+
readonly file: "robots.txt";
|
|
5
|
+
readonly type: "text/plain; charset=utf-8";
|
|
6
|
+
};
|
|
7
|
+
readonly sitemap: {
|
|
8
|
+
readonly file: "sitemap.xml";
|
|
9
|
+
readonly type: "application/xml; charset=utf-8";
|
|
10
|
+
};
|
|
11
|
+
readonly llms: {
|
|
12
|
+
readonly file: "llms.txt";
|
|
13
|
+
readonly type: "text/plain; charset=utf-8";
|
|
14
|
+
};
|
|
15
|
+
readonly "llms-full": {
|
|
16
|
+
readonly file: "llms-full.txt";
|
|
17
|
+
readonly type: "text/plain; charset=utf-8";
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type MetadataRouteKind = keyof typeof METADATA_ROUTES;
|
|
21
|
+
/**
|
|
22
|
+
* Files an app may drop beside the root layout to be served at the root as
|
|
23
|
+
* they are: the same three, hand-written, and the handful of other files a
|
|
24
|
+
* site is asked for at its root.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ROOT_FILES: RegExp;
|
|
27
|
+
export declare function rootFileType(name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* A url made absolute. A relative one needs the root layout's metadataBase -
|
|
30
|
+
* a crawler is handed these with no page to resolve them against.
|
|
31
|
+
*/
|
|
32
|
+
export declare function absoluteUrl(url: string, base: string | URL | null | undefined, file: string): string;
|
|
33
|
+
export declare function robotsText(robots: MetadataRoute.Robots, base: string | URL | null | undefined): string;
|
|
34
|
+
export declare function sitemapXml(entries: MetadataRoute.Sitemap, base: string | URL | null | undefined): string;
|
|
35
|
+
export declare function llmsText(llms: MetadataRoute.Llms, base: string | URL | null | undefined): string;
|
|
36
|
+
/**
|
|
37
|
+
* The response for one of these routes: what the app's function returned,
|
|
38
|
+
* formatted, unless it returned the text itself. Called by the route the
|
|
39
|
+
* build synthesised, never by an app directly.
|
|
40
|
+
*/
|
|
41
|
+
export declare function metadataResponse(kind: MetadataRouteKind, produce: () => unknown, base: string | URL | null | undefined): Promise<Response>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// robots.txt, sitemap.xml and llms.txt, from a file beside the root layout.
|
|
2
|
+
//
|
|
3
|
+
// Each convention file becomes an api route the build synthesises - so a
|
|
4
|
+
// sitemap that reads nothing per request is stored at build like any frozen
|
|
5
|
+
// route, one that reads the request stays dynamic, and the build's table says
|
|
6
|
+
// which. What lives here is the formatting: the value the app's function
|
|
7
|
+
// returns, written the way the crawler expects it.
|
|
8
|
+
export const METADATA_ROUTES = {
|
|
9
|
+
robots: { file: "robots.txt", type: "text/plain; charset=utf-8" },
|
|
10
|
+
sitemap: { file: "sitemap.xml", type: "application/xml; charset=utf-8" },
|
|
11
|
+
llms: { file: "llms.txt", type: "text/plain; charset=utf-8" },
|
|
12
|
+
"llms-full": { file: "llms-full.txt", type: "text/plain; charset=utf-8" },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Files an app may drop beside the root layout to be served at the root as
|
|
16
|
+
* they are: the same three, hand-written, and the handful of other files a
|
|
17
|
+
* site is asked for at its root.
|
|
18
|
+
*/
|
|
19
|
+
export const ROOT_FILES = /^(?:robots\.txt|sitemap(?:-[\w.-]+)?\.xml|llms(?:-full)?\.txt|humans\.txt|security\.txt|ads\.txt|app-ads\.txt)$/;
|
|
20
|
+
export function rootFileType(name) {
|
|
21
|
+
return name.endsWith(".xml")
|
|
22
|
+
? "application/xml; charset=utf-8"
|
|
23
|
+
: "text/plain; charset=utf-8";
|
|
24
|
+
}
|
|
25
|
+
function list(value) {
|
|
26
|
+
return value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A url made absolute. A relative one needs the root layout's metadataBase -
|
|
30
|
+
* a crawler is handed these with no page to resolve them against.
|
|
31
|
+
*/
|
|
32
|
+
export function absoluteUrl(url, base, file) {
|
|
33
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
34
|
+
return url;
|
|
35
|
+
if (!base) {
|
|
36
|
+
throw new Error(`[rsc-kit] ${file} names a relative url, ${JSON.stringify(url)}, and the root layout has no metadataBase to make it absolute. ` +
|
|
37
|
+
"Add metadataBase: new URL('https://example.com') to the root layout's metadata, or write the url in full.");
|
|
38
|
+
}
|
|
39
|
+
return new URL(url, base).href;
|
|
40
|
+
}
|
|
41
|
+
export function robotsText(robots, base) {
|
|
42
|
+
const blocks = list(robots.rules).map((rule) => {
|
|
43
|
+
const lines = [];
|
|
44
|
+
for (const agent of list(rule.userAgent).length
|
|
45
|
+
? list(rule.userAgent)
|
|
46
|
+
: ["*"])
|
|
47
|
+
lines.push(`User-Agent: ${agent}`);
|
|
48
|
+
for (const path of list(rule.allow))
|
|
49
|
+
lines.push(`Allow: ${path}`);
|
|
50
|
+
for (const path of list(rule.disallow))
|
|
51
|
+
lines.push(`Disallow: ${path}`);
|
|
52
|
+
if (rule.crawlDelay !== undefined)
|
|
53
|
+
lines.push(`Crawl-delay: ${rule.crawlDelay}`);
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
});
|
|
56
|
+
const tail = [];
|
|
57
|
+
for (const sitemap of list(robots.sitemap))
|
|
58
|
+
tail.push(`Sitemap: ${absoluteUrl(sitemap, base, "robots.ts")}`);
|
|
59
|
+
if (robots.host)
|
|
60
|
+
tail.push(`Host: ${robots.host}`);
|
|
61
|
+
return ([...blocks, ...(tail.length ? [tail.join("\n")] : [])].join("\n\n") + "\n");
|
|
62
|
+
}
|
|
63
|
+
function escapeXml(text) {
|
|
64
|
+
return text.replace(/[&<>"']/g, (c) => ({
|
|
65
|
+
"&": "&",
|
|
66
|
+
"<": "<",
|
|
67
|
+
">": ">",
|
|
68
|
+
'"': """,
|
|
69
|
+
"'": "'",
|
|
70
|
+
})[c]);
|
|
71
|
+
}
|
|
72
|
+
export function sitemapXml(entries, base) {
|
|
73
|
+
const hasAlternates = entries.some((e) => e.alternates?.languages && Object.keys(e.alternates.languages).length > 0);
|
|
74
|
+
const hasImages = entries.some((e) => e.images && e.images.length > 0);
|
|
75
|
+
const urls = entries.map((entry) => {
|
|
76
|
+
const parts = [
|
|
77
|
+
`<loc>${escapeXml(absoluteUrl(entry.url, base, "sitemap.ts"))}</loc>`,
|
|
78
|
+
];
|
|
79
|
+
if (entry.lastModified !== undefined) {
|
|
80
|
+
const when = entry.lastModified instanceof Date
|
|
81
|
+
? entry.lastModified.toISOString()
|
|
82
|
+
: entry.lastModified;
|
|
83
|
+
parts.push(`<lastmod>${escapeXml(when)}</lastmod>`);
|
|
84
|
+
}
|
|
85
|
+
if (entry.changeFrequency)
|
|
86
|
+
parts.push(`<changefreq>${entry.changeFrequency}</changefreq>`);
|
|
87
|
+
if (entry.priority !== undefined)
|
|
88
|
+
parts.push(`<priority>${entry.priority}</priority>`);
|
|
89
|
+
for (const [lang, href] of Object.entries(entry.alternates?.languages ?? {})) {
|
|
90
|
+
parts.push(`<xhtml:link rel="alternate" hreflang="${escapeXml(lang)}" href="${escapeXml(absoluteUrl(href, base, "sitemap.ts"))}"/>`);
|
|
91
|
+
}
|
|
92
|
+
for (const image of entry.images ?? []) {
|
|
93
|
+
parts.push(`<image:image><image:loc>${escapeXml(absoluteUrl(image, base, "sitemap.ts"))}</image:loc></image:image>`);
|
|
94
|
+
}
|
|
95
|
+
return `<url>${parts.join("")}</url>`;
|
|
96
|
+
});
|
|
97
|
+
const namespaces = [
|
|
98
|
+
'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
|
|
99
|
+
...(hasAlternates ? ['xmlns:xhtml="http://www.w3.org/1999/xhtml"'] : []),
|
|
100
|
+
...(hasImages
|
|
101
|
+
? ['xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"']
|
|
102
|
+
: []),
|
|
103
|
+
];
|
|
104
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset ${namespaces.join(" ")}>\n${urls.join("\n")}\n</urlset>\n`;
|
|
105
|
+
}
|
|
106
|
+
export function llmsText(llms, base) {
|
|
107
|
+
const out = [`# ${llms.title}`];
|
|
108
|
+
if (llms.summary)
|
|
109
|
+
out.push("", `> ${llms.summary}`);
|
|
110
|
+
for (const paragraph of list(llms.details))
|
|
111
|
+
out.push("", paragraph);
|
|
112
|
+
for (const section of llms.sections ?? []) {
|
|
113
|
+
out.push("", `## ${section.title}`, "");
|
|
114
|
+
for (const link of section.links) {
|
|
115
|
+
out.push(`- [${link.title}](${absoluteUrl(link.url, base, "llms.ts")})${link.description ? `: ${link.description}` : ""}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out.join("\n") + "\n";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The response for one of these routes: what the app's function returned,
|
|
122
|
+
* formatted, unless it returned the text itself. Called by the route the
|
|
123
|
+
* build synthesised, never by an app directly.
|
|
124
|
+
*/
|
|
125
|
+
export async function metadataResponse(kind, produce, base) {
|
|
126
|
+
const value = await produce();
|
|
127
|
+
const body = typeof value === "string"
|
|
128
|
+
? value
|
|
129
|
+
: kind === "robots"
|
|
130
|
+
? robotsText(value, base)
|
|
131
|
+
: kind === "sitemap"
|
|
132
|
+
? sitemapXml(value, base)
|
|
133
|
+
: llmsText(value, base);
|
|
134
|
+
return new Response(body, {
|
|
135
|
+
headers: { "Content-Type": METADATA_ROUTES[kind].type },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=metadataRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadataRoutes.js","sourceRoot":"","sources":["../src/metadataRoutes.ts"],"names":[],"mappings":"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","sourcesContent":["// robots.txt, sitemap.xml and llms.txt, from a file beside the root layout.\n//\n// Each convention file becomes an api route the build synthesises - so a\n// sitemap that reads nothing per request is stored at build like any frozen\n// route, one that reads the request stays dynamic, and the build's table says\n// which. What lives here is the formatting: the value the app's function\n// returns, written the way the crawler expects it.\n\nimport type { MetadataRoute, RobotsRule, SitemapEntry } from \"./metadata.js\";\n\nexport const METADATA_ROUTES = {\n robots: { file: \"robots.txt\", type: \"text/plain; charset=utf-8\" },\n sitemap: { file: \"sitemap.xml\", type: \"application/xml; charset=utf-8\" },\n llms: { file: \"llms.txt\", type: \"text/plain; charset=utf-8\" },\n \"llms-full\": { file: \"llms-full.txt\", type: \"text/plain; charset=utf-8\" },\n} as const;\n\nexport type MetadataRouteKind = keyof typeof METADATA_ROUTES;\n\n/**\n * Files an app may drop beside the root layout to be served at the root as\n * they are: the same three, hand-written, and the handful of other files a\n * site is asked for at its root.\n */\nexport const ROOT_FILES =\n /^(?:robots\\.txt|sitemap(?:-[\\w.-]+)?\\.xml|llms(?:-full)?\\.txt|humans\\.txt|security\\.txt|ads\\.txt|app-ads\\.txt)$/;\n\nexport function rootFileType(name: string): string {\n return name.endsWith(\".xml\")\n ? \"application/xml; charset=utf-8\"\n : \"text/plain; charset=utf-8\";\n}\n\nfunction list<T>(value: T | T[] | undefined): T[] {\n return value === undefined ? [] : Array.isArray(value) ? value : [value];\n}\n\n/**\n * A url made absolute. A relative one needs the root layout's metadataBase -\n * a crawler is handed these with no page to resolve them against.\n */\nexport function absoluteUrl(\n url: string,\n base: string | URL | null | undefined,\n file: string,\n): string {\n if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;\n\n if (!base) {\n throw new Error(\n `[rsc-kit] ${file} names a relative url, ${JSON.stringify(url)}, and the root layout has no metadataBase to make it absolute. ` +\n \"Add metadataBase: new URL('https://example.com') to the root layout's metadata, or write the url in full.\",\n );\n }\n\n return new URL(url, base).href;\n}\n\nexport function robotsText(\n robots: MetadataRoute.Robots,\n base: string | URL | null | undefined,\n): string {\n const blocks = list(robots.rules).map((rule: RobotsRule) => {\n const lines: string[] = [];\n\n for (const agent of list(rule.userAgent).length\n ? list(rule.userAgent)\n : [\"*\"])\n lines.push(`User-Agent: ${agent}`);\n for (const path of list(rule.allow)) lines.push(`Allow: ${path}`);\n for (const path of list(rule.disallow)) lines.push(`Disallow: ${path}`);\n if (rule.crawlDelay !== undefined)\n lines.push(`Crawl-delay: ${rule.crawlDelay}`);\n\n return lines.join(\"\\n\");\n });\n\n const tail: string[] = [];\n\n for (const sitemap of list(robots.sitemap))\n tail.push(`Sitemap: ${absoluteUrl(sitemap, base, \"robots.ts\")}`);\n if (robots.host) tail.push(`Host: ${robots.host}`);\n\n return (\n [...blocks, ...(tail.length ? [tail.join(\"\\n\")] : [])].join(\"\\n\\n\") + \"\\n\"\n );\n}\n\nfunction escapeXml(text: string): string {\n return text.replace(\n /[&<>\"']/g,\n (c) =>\n ({\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n })[c]!,\n );\n}\n\nexport function sitemapXml(\n entries: MetadataRoute.Sitemap,\n base: string | URL | null | undefined,\n): string {\n const hasAlternates = entries.some(\n (e) =>\n e.alternates?.languages && Object.keys(e.alternates.languages).length > 0,\n );\n const hasImages = entries.some((e) => e.images && e.images.length > 0);\n\n const urls = entries.map((entry: SitemapEntry) => {\n const parts = [\n `<loc>${escapeXml(absoluteUrl(entry.url, base, \"sitemap.ts\"))}</loc>`,\n ];\n\n if (entry.lastModified !== undefined) {\n const when =\n entry.lastModified instanceof Date\n ? entry.lastModified.toISOString()\n : entry.lastModified;\n\n parts.push(`<lastmod>${escapeXml(when)}</lastmod>`);\n }\n if (entry.changeFrequency)\n parts.push(`<changefreq>${entry.changeFrequency}</changefreq>`);\n if (entry.priority !== undefined)\n parts.push(`<priority>${entry.priority}</priority>`);\n for (const [lang, href] of Object.entries(\n entry.alternates?.languages ?? {},\n )) {\n parts.push(\n `<xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(lang)}\" href=\"${escapeXml(absoluteUrl(href, base, \"sitemap.ts\"))}\"/>`,\n );\n }\n for (const image of entry.images ?? []) {\n parts.push(\n `<image:image><image:loc>${escapeXml(absoluteUrl(image, base, \"sitemap.ts\"))}</image:loc></image:image>`,\n );\n }\n\n return `<url>${parts.join(\"\")}</url>`;\n });\n\n const namespaces = [\n 'xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"',\n ...(hasAlternates ? ['xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"'] : []),\n ...(hasImages\n ? ['xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"']\n : []),\n ];\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset ${namespaces.join(\" \")}>\\n${urls.join(\"\\n\")}\\n</urlset>\\n`;\n}\n\nexport function llmsText(\n llms: MetadataRoute.Llms,\n base: string | URL | null | undefined,\n): string {\n const out: string[] = [`# ${llms.title}`];\n\n if (llms.summary) out.push(\"\", `> ${llms.summary}`);\n for (const paragraph of list(llms.details)) out.push(\"\", paragraph);\n\n for (const section of llms.sections ?? []) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const link of section.links) {\n out.push(\n `- [${link.title}](${absoluteUrl(link.url, base, \"llms.ts\")})${link.description ? `: ${link.description}` : \"\"}`,\n );\n }\n }\n\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * The response for one of these routes: what the app's function returned,\n * formatted, unless it returned the text itself. Called by the route the\n * build synthesised, never by an app directly.\n */\nexport async function metadataResponse(\n kind: MetadataRouteKind,\n produce: () => unknown,\n base: string | URL | null | undefined,\n): Promise<Response> {\n const value = await produce();\n const body =\n typeof value === \"string\"\n ? value\n : kind === \"robots\"\n ? robotsText(value as MetadataRoute.Robots, base)\n : kind === \"sitemap\"\n ? sitemapXml(value as MetadataRoute.Sitemap, base)\n : llmsText(value as MetadataRoute.Llms, base);\n\n return new Response(body, {\n headers: { \"Content-Type\": METADATA_ROUTES[kind].type },\n });\n}\n"]}
|
package/dist/prerender.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// shapes and anything that reads them works for either.
|
|
17
17
|
import { withRedirect } from "./redirect.js";
|
|
18
18
|
import { withCache } from "./cache.js";
|
|
19
|
-
import { requestReadBy, requestReadWhere, requestWasRead, withRequest, } from "./request.js";
|
|
19
|
+
import { requestFallbacks, requestReadBy, requestReadWhere, requestWasRead, withRequest, } from "./request.js";
|
|
20
20
|
import { watchNondeterminism, whileRendering } from "./nondeterminism.js";
|
|
21
21
|
/** What a prerenderer needs from the built bundle, beyond serving a request. */
|
|
22
22
|
/**
|
|
@@ -223,7 +223,7 @@ export function notes(results) {
|
|
|
223
223
|
!pages.some((r) => r.type === "frozen") &&
|
|
224
224
|
reasons.size === 1) {
|
|
225
225
|
const [reason] = reasons;
|
|
226
|
-
parts.push(` Every route ${reason}. When one read
|
|
226
|
+
parts.push(` Every route: ${reason}. When one read reaches every page it is usually\n` +
|
|
227
227
|
" in the root layout - a header reading the session, a locale from a cookie.\n" +
|
|
228
228
|
" Move that read into the component that needs it, under a <Suspense>, and\n" +
|
|
229
229
|
" the rest of the site can freeze around it.");
|
|
@@ -399,8 +399,12 @@ export async function prerender(options) {
|
|
|
399
399
|
* structural reasons, either of which may also be true.
|
|
400
400
|
*/
|
|
401
401
|
const shellReason = (calls) => {
|
|
402
|
+
// The shell is stored; what streams is named. "dynamic" would say the
|
|
403
|
+
// page is, and the mark beside the line says it is not.
|
|
402
404
|
if (calls.length)
|
|
403
|
-
return
|
|
405
|
+
return (calls.join(", ") +
|
|
406
|
+
(calls.length > 1 ? " stream" : " streams") +
|
|
407
|
+
" per request; the rest is stored");
|
|
404
408
|
// One shell serving every url the route matches. generateStaticParams is
|
|
405
409
|
// what turns it into a page per url.
|
|
406
410
|
if (unlistedNow)
|
|
@@ -434,7 +438,7 @@ export async function prerender(options) {
|
|
|
434
438
|
// must not share an answer just because they were built in the same run.
|
|
435
439
|
// No request, deliberately: a page that reads one is caught below rather
|
|
436
440
|
// than frozen holding whatever the build machine happened to send.
|
|
437
|
-
const [{ shell, redirected, readRequest, readBy, readWhere }, nondeterministic,] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
441
|
+
const [{ shell, redirected, readRequest, readBy, readWhere, fallbacks }, nondeterministic,] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
438
442
|
try {
|
|
439
443
|
return {
|
|
440
444
|
shell: await engine.handleRscPprShell(route.component, props, layouts, route.loadings, route.slots,
|
|
@@ -450,6 +454,7 @@ export async function prerender(options) {
|
|
|
450
454
|
// And from which component, so the message is a line to open
|
|
451
455
|
// rather than a category to search for.
|
|
452
456
|
readWhere: requestReadWhere(),
|
|
457
|
+
fallbacks: requestFallbacks(),
|
|
453
458
|
};
|
|
454
459
|
}
|
|
455
460
|
catch (error) {
|
|
@@ -466,6 +471,7 @@ export async function prerender(options) {
|
|
|
466
471
|
readRequest: requestWasRead(),
|
|
467
472
|
readBy: requestReadBy(),
|
|
468
473
|
readWhere: requestReadWhere(),
|
|
474
|
+
fallbacks: requestFallbacks(),
|
|
469
475
|
};
|
|
470
476
|
}
|
|
471
477
|
}))));
|
|
@@ -494,7 +500,13 @@ export async function prerender(options) {
|
|
|
494
500
|
* the prerender.
|
|
495
501
|
*/
|
|
496
502
|
async function withRootFallbackChecked(result) {
|
|
497
|
-
|
|
503
|
+
// Also for a frozen page whose SSR threw and was caught at a boundary -
|
|
504
|
+
// useSearchParams() with nothing closer than the root loading.tsx. That
|
|
505
|
+
// render finished, with the root fallback standing in for the page;
|
|
506
|
+
// without it the throw reaches the root and nothing paints, which is
|
|
507
|
+
// the question this asks.
|
|
508
|
+
if (result.type !== "shell" &&
|
|
509
|
+
!(result.type === "frozen" && fallbacks.length))
|
|
498
510
|
return result;
|
|
499
511
|
if (!engine.handleRscPprShell)
|
|
500
512
|
return result;
|
|
@@ -508,8 +520,17 @@ export async function prerender(options) {
|
|
|
508
520
|
if (route.loadings.length !== 1 ||
|
|
509
521
|
!only ||
|
|
510
522
|
!root ||
|
|
511
|
-
dirOf(only) !== dirOf(root))
|
|
523
|
+
dirOf(only) !== dirOf(root)) {
|
|
524
|
+
// A boundary of its own caught it: the fallback there is stored and
|
|
525
|
+
// the value is read in the browser. Said, because the page is not
|
|
526
|
+
// quite what the source shows at first paint.
|
|
527
|
+
if (fallbacks.length && !result.note) {
|
|
528
|
+
result.note =
|
|
529
|
+
fallbacks.join(", ") +
|
|
530
|
+
" — its fallback is stored; the value is read in the browser";
|
|
531
|
+
}
|
|
512
532
|
return result;
|
|
533
|
+
}
|
|
513
534
|
// A tenth of the shell budget, because this is a different question.
|
|
514
535
|
//
|
|
515
536
|
// Deciding what a page's shell IS means waiting out everything that can
|
|
@@ -518,12 +539,20 @@ export async function prerender(options) {
|
|
|
518
539
|
// paints immediately. Given the full budget it cost two seconds a route
|
|
519
540
|
// to learn what the first millisecond already said, which on this
|
|
520
541
|
// example was 40% of the entire prerender.
|
|
521
|
-
const withoutRoot = await withRequest(null, () => withCache(() => withRedirect(async () => engine.handleRscPprShell(route.component, props, layouts, [], route.slots, url, ROOT_FALLBACK_BUDGET_MS)))).catch(() => null);
|
|
542
|
+
const withoutRoot = await withRequest(null, () => withCache(() => withRedirect(async () => engine.handleRscPprShell(route.component, props, layouts, [], route.slots, url, ROOT_FALLBACK_BUDGET_MS)))).catch(() => (fallbacks.length ? { shellHtml: "" } : null));
|
|
522
543
|
if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) === "") {
|
|
523
|
-
result.warning =
|
|
524
|
-
|
|
544
|
+
result.warning = fallbacks.length
|
|
545
|
+
? fallbacks.join(", ") +
|
|
546
|
+
" has no boundary closer than the root loading.tsx, so that is the whole page at first paint. " +
|
|
547
|
+
"Put a <Suspense> around the component that reads, and the rest of the page is stored."
|
|
548
|
+
: "nothing painted without the root loading.tsx — the fallback the whole app shares " +
|
|
525
549
|
"is standing in for this page. Put a boundary where the waiting is.";
|
|
526
550
|
}
|
|
551
|
+
else if (fallbacks.length && !result.note) {
|
|
552
|
+
result.note =
|
|
553
|
+
fallbacks.join(", ") +
|
|
554
|
+
" — its fallback is stored; the value is read in the browser";
|
|
555
|
+
}
|
|
527
556
|
return result;
|
|
528
557
|
}
|
|
529
558
|
// A route that only redirects still has an answer to freeze — the answer
|
|
@@ -574,7 +603,11 @@ export async function prerender(options) {
|
|
|
574
603
|
: shell.usedDynamicApis
|
|
575
604
|
? "reaches for the host"
|
|
576
605
|
: "blocks") +
|
|
577
|
-
" before anything can paint.
|
|
606
|
+
" before anything can paint." +
|
|
607
|
+
(readWhere.some((w) => w.includes(" awaited by "))
|
|
608
|
+
? " One of those has no boundary above it."
|
|
609
|
+
: "") +
|
|
610
|
+
" Add a loading.tsx beside it, or put a " +
|
|
578
611
|
"<Suspense> above the waiting, and it has a skeleton to store.");
|
|
579
612
|
}
|
|
580
613
|
await writeShell(route, url, body, shell.postponed);
|