@rsc-kit/core 0.16.3 → 0.18.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/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- 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/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.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 +32 -17
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +2 -2
- package/dist/js/devNotice.js +17 -7
- package/dist/js/devNotice.js.map +1 -1
- 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/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.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.js +10 -1
- package/dist/js/staleAssets.js.map +1 -1
- 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/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- 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/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +61 -0
- package/dist/metadataRoutes.js +173 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +44 -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/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.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 +86 -9
- package/dist/vite.js +827 -24
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
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,61 @@
|
|
|
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>;
|
|
42
|
+
/**
|
|
43
|
+
* The sitemap the build writes when the app has no sitemap.ts.
|
|
44
|
+
*
|
|
45
|
+
* The build already knows what belongs in one: every page it stored, and
|
|
46
|
+
* every url generateStaticParams listed, which is the set a crawler should
|
|
47
|
+
* be told about. Left out: a route with middleware above it (a guard means
|
|
48
|
+
* "not for everyone"), a page the build could not store (it varies per
|
|
49
|
+
* visitor, so it has no one url worth listing... it does, but no freshness
|
|
50
|
+
* to claim - listed without lastModified), the not-found page, and the
|
|
51
|
+
* metadata files themselves. lastModified is the build, which is when the
|
|
52
|
+
* stored copy changed. A sitemap.ts beside the root layout replaces this.
|
|
53
|
+
*/
|
|
54
|
+
export declare function automaticSitemap(results: readonly {
|
|
55
|
+
url: string;
|
|
56
|
+
type: string;
|
|
57
|
+
component: string;
|
|
58
|
+
}[], routes: readonly {
|
|
59
|
+
component: string;
|
|
60
|
+
middleware: readonly string[];
|
|
61
|
+
}[], base: string | URL, builtAt?: Date): string;
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
/**
|
|
139
|
+
* The sitemap the build writes when the app has no sitemap.ts.
|
|
140
|
+
*
|
|
141
|
+
* The build already knows what belongs in one: every page it stored, and
|
|
142
|
+
* every url generateStaticParams listed, which is the set a crawler should
|
|
143
|
+
* be told about. Left out: a route with middleware above it (a guard means
|
|
144
|
+
* "not for everyone"), a page the build could not store (it varies per
|
|
145
|
+
* visitor, so it has no one url worth listing... it does, but no freshness
|
|
146
|
+
* to claim - listed without lastModified), the not-found page, and the
|
|
147
|
+
* metadata files themselves. lastModified is the build, which is when the
|
|
148
|
+
* stored copy changed. A sitemap.ts beside the root layout replaces this.
|
|
149
|
+
*/
|
|
150
|
+
export function automaticSitemap(results, routes, base, builtAt = new Date()) {
|
|
151
|
+
const guarded = new Set(routes.filter((r) => r.middleware.length > 0).map((r) => r.component));
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const entries = [];
|
|
154
|
+
for (const result of results) {
|
|
155
|
+
if (guarded.has(result.component))
|
|
156
|
+
continue;
|
|
157
|
+
if (result.type === "error" || result.type === "blocked")
|
|
158
|
+
continue;
|
|
159
|
+
if (/(^|\/)not-found$|\/_/.test(result.url))
|
|
160
|
+
continue;
|
|
161
|
+
if (/\[.*\]/.test(result.url))
|
|
162
|
+
continue; // a pattern, not a url
|
|
163
|
+
if (seen.has(result.url))
|
|
164
|
+
continue;
|
|
165
|
+
seen.add(result.url);
|
|
166
|
+
entries.push(result.type === "frozen" || result.type === "shell"
|
|
167
|
+
? { url: result.url, lastModified: builtAt }
|
|
168
|
+
: { url: result.url });
|
|
169
|
+
}
|
|
170
|
+
entries.sort((a, b) => a.url.localeCompare(b.url));
|
|
171
|
+
return sitemapXml(entries, base);
|
|
172
|
+
}
|
|
173
|
+
//# 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\n/**\n * The sitemap the build writes when the app has no sitemap.ts.\n *\n * The build already knows what belongs in one: every page it stored, and\n * every url generateStaticParams listed, which is the set a crawler should\n * be told about. Left out: a route with middleware above it (a guard means\n * \"not for everyone\"), a page the build could not store (it varies per\n * visitor, so it has no one url worth listing... it does, but no freshness\n * to claim - listed without lastModified), the not-found page, and the\n * metadata files themselves. lastModified is the build, which is when the\n * stored copy changed. A sitemap.ts beside the root layout replaces this.\n */\nexport function automaticSitemap(\n results: readonly { url: string; type: string; component: string }[],\n routes: readonly { component: string; middleware: readonly string[] }[],\n base: string | URL,\n builtAt: Date = new Date(),\n): string {\n const guarded = new Set(\n routes.filter((r) => r.middleware.length > 0).map((r) => r.component),\n );\n const seen = new Set<string>();\n const entries: SitemapEntry[] = [];\n\n for (const result of results) {\n if (guarded.has(result.component)) continue;\n if (result.type === \"error\" || result.type === \"blocked\") continue;\n if (/(^|\\/)not-found$|\\/_/.test(result.url)) continue;\n if (/\\[.*\\]/.test(result.url)) continue; // a pattern, not a url\n if (seen.has(result.url)) continue;\n\n seen.add(result.url);\n entries.push(\n result.type === \"frozen\" || result.type === \"shell\"\n ? { url: result.url, lastModified: builtAt }\n : { url: result.url },\n );\n }\n\n entries.sort((a, b) => a.url.localeCompare(b.url));\n\n return sitemapXml(entries, base);\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
|
/**
|
|
@@ -49,6 +49,7 @@ const DEFAULT_PRERENDER_CONCURRENCY = 4;
|
|
|
49
49
|
* else in the list is the app's, and needs the runtime.
|
|
50
50
|
*/
|
|
51
51
|
const RUNTIME_OWN = new Set([
|
|
52
|
+
"DefaultRouteError",
|
|
52
53
|
"DocumentTitle",
|
|
53
54
|
"PathnameProvider",
|
|
54
55
|
"RouteErrorBoundary",
|
|
@@ -223,7 +224,7 @@ export function notes(results) {
|
|
|
223
224
|
!pages.some((r) => r.type === "frozen") &&
|
|
224
225
|
reasons.size === 1) {
|
|
225
226
|
const [reason] = reasons;
|
|
226
|
-
parts.push(` Every route ${reason}. When one read
|
|
227
|
+
parts.push(` Every route: ${reason}. When one read reaches every page it is usually\n` +
|
|
227
228
|
" in the root layout - a header reading the session, a locale from a cookie.\n" +
|
|
228
229
|
" Move that read into the component that needs it, under a <Suspense>, and\n" +
|
|
229
230
|
" the rest of the site can freeze around it.");
|
|
@@ -399,8 +400,12 @@ export async function prerender(options) {
|
|
|
399
400
|
* structural reasons, either of which may also be true.
|
|
400
401
|
*/
|
|
401
402
|
const shellReason = (calls) => {
|
|
403
|
+
// The shell is stored; what streams is named. "dynamic" would say the
|
|
404
|
+
// page is, and the mark beside the line says it is not.
|
|
402
405
|
if (calls.length)
|
|
403
|
-
return
|
|
406
|
+
return (calls.join(", ") +
|
|
407
|
+
(calls.length > 1 ? " stream" : " streams") +
|
|
408
|
+
" per request; the rest is stored");
|
|
404
409
|
// One shell serving every url the route matches. generateStaticParams is
|
|
405
410
|
// what turns it into a page per url.
|
|
406
411
|
if (unlistedNow)
|
|
@@ -434,7 +439,7 @@ export async function prerender(options) {
|
|
|
434
439
|
// must not share an answer just because they were built in the same run.
|
|
435
440
|
// No request, deliberately: a page that reads one is caught below rather
|
|
436
441
|
// 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) => {
|
|
442
|
+
const [{ shell, redirected, readRequest, readBy, readWhere, fallbacks }, nondeterministic,] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
438
443
|
try {
|
|
439
444
|
return {
|
|
440
445
|
shell: await engine.handleRscPprShell(route.component, props, layouts, route.loadings, route.slots,
|
|
@@ -450,6 +455,7 @@ export async function prerender(options) {
|
|
|
450
455
|
// And from which component, so the message is a line to open
|
|
451
456
|
// rather than a category to search for.
|
|
452
457
|
readWhere: requestReadWhere(),
|
|
458
|
+
fallbacks: requestFallbacks(),
|
|
453
459
|
};
|
|
454
460
|
}
|
|
455
461
|
catch (error) {
|
|
@@ -466,6 +472,7 @@ export async function prerender(options) {
|
|
|
466
472
|
readRequest: requestWasRead(),
|
|
467
473
|
readBy: requestReadBy(),
|
|
468
474
|
readWhere: requestReadWhere(),
|
|
475
|
+
fallbacks: requestFallbacks(),
|
|
469
476
|
};
|
|
470
477
|
}
|
|
471
478
|
}))));
|
|
@@ -494,7 +501,13 @@ export async function prerender(options) {
|
|
|
494
501
|
* the prerender.
|
|
495
502
|
*/
|
|
496
503
|
async function withRootFallbackChecked(result) {
|
|
497
|
-
|
|
504
|
+
// Also for a frozen page whose SSR threw and was caught at a boundary -
|
|
505
|
+
// useSearchParams() with nothing closer than the root loading.tsx. That
|
|
506
|
+
// render finished, with the root fallback standing in for the page;
|
|
507
|
+
// without it the throw reaches the root and nothing paints, which is
|
|
508
|
+
// the question this asks.
|
|
509
|
+
if (result.type !== "shell" &&
|
|
510
|
+
!(result.type === "frozen" && fallbacks.length))
|
|
498
511
|
return result;
|
|
499
512
|
if (!engine.handleRscPprShell)
|
|
500
513
|
return result;
|
|
@@ -508,8 +521,17 @@ export async function prerender(options) {
|
|
|
508
521
|
if (route.loadings.length !== 1 ||
|
|
509
522
|
!only ||
|
|
510
523
|
!root ||
|
|
511
|
-
dirOf(only) !== dirOf(root))
|
|
524
|
+
dirOf(only) !== dirOf(root)) {
|
|
525
|
+
// A boundary of its own caught it: the fallback there is stored and
|
|
526
|
+
// the value is read in the browser. Said, because the page is not
|
|
527
|
+
// quite what the source shows at first paint.
|
|
528
|
+
if (fallbacks.length && !result.note) {
|
|
529
|
+
result.note =
|
|
530
|
+
fallbacks.join(", ") +
|
|
531
|
+
" — its fallback is stored; the value is read in the browser";
|
|
532
|
+
}
|
|
512
533
|
return result;
|
|
534
|
+
}
|
|
513
535
|
// A tenth of the shell budget, because this is a different question.
|
|
514
536
|
//
|
|
515
537
|
// Deciding what a page's shell IS means waiting out everything that can
|
|
@@ -518,12 +540,20 @@ export async function prerender(options) {
|
|
|
518
540
|
// paints immediately. Given the full budget it cost two seconds a route
|
|
519
541
|
// to learn what the first millisecond already said, which on this
|
|
520
542
|
// 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);
|
|
543
|
+
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
544
|
if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) === "") {
|
|
523
|
-
result.warning =
|
|
524
|
-
|
|
545
|
+
result.warning = fallbacks.length
|
|
546
|
+
? fallbacks.join(", ") +
|
|
547
|
+
" has no boundary closer than the root loading.tsx, so that is the whole page at first paint. " +
|
|
548
|
+
"Put a <Suspense> around the component that reads, and the rest of the page is stored."
|
|
549
|
+
: "nothing painted without the root loading.tsx — the fallback the whole app shares " +
|
|
525
550
|
"is standing in for this page. Put a boundary where the waiting is.";
|
|
526
551
|
}
|
|
552
|
+
else if (fallbacks.length && !result.note) {
|
|
553
|
+
result.note =
|
|
554
|
+
fallbacks.join(", ") +
|
|
555
|
+
" — its fallback is stored; the value is read in the browser";
|
|
556
|
+
}
|
|
527
557
|
return result;
|
|
528
558
|
}
|
|
529
559
|
// A route that only redirects still has an answer to freeze — the answer
|
|
@@ -574,7 +604,11 @@ export async function prerender(options) {
|
|
|
574
604
|
: shell.usedDynamicApis
|
|
575
605
|
? "reaches for the host"
|
|
576
606
|
: "blocks") +
|
|
577
|
-
" before anything can paint.
|
|
607
|
+
" before anything can paint." +
|
|
608
|
+
(readWhere.some((w) => w.includes(" awaited by "))
|
|
609
|
+
? " One of those has no boundary above it."
|
|
610
|
+
: "") +
|
|
611
|
+
" Add a loading.tsx beside it, or put a " +
|
|
578
612
|
"<Suspense> above the waiting, and it has a skeleton to store.");
|
|
579
613
|
}
|
|
580
614
|
await writeShell(route, url, body, shell.postponed);
|