@iterant/site-runtime 3.11.0 → 3.11.2
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/docs/runtime-contract.md
CHANGED
|
@@ -50,7 +50,7 @@ runtime and says so.
|
|
|
50
50
|
|
|
51
51
|
<!-- generated: available libraries -->
|
|
52
52
|
|
|
53
|
-
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.11.
|
|
53
|
+
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.11.2._
|
|
54
54
|
|
|
55
55
|
**Toolchain** (this package owns the version; do NOT declare these):
|
|
56
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iterant/site-runtime",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The platform layer every Iterant brand site runs on: content grammar, collection schemas, SEO head and JSON-LD, layout core, Astro config preset, dev integrations and the verify gates.",
|
|
6
6
|
"scripts": {
|
|
@@ -33,11 +33,15 @@ function resolvePagesDir({ pagesDir, root }: SitemapPathsOptions): string {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Every advertised page entry's route (neither draft nor noindex), home
|
|
36
|
-
* excluded (@astrojs/sitemap already emits the site root)
|
|
36
|
+
* excluded (@astrojs/sitemap already emits the site root), beside `deny`: the
|
|
37
|
+
* routes of the entries that are NOT advertised. The integration crawls the
|
|
38
|
+
* prerendered pages itself and would list those a second way, so the caller
|
|
39
|
+
* drops them through its filter. Returns `pagesDir` so a caller can report
|
|
37
40
|
* WHERE it looked when the answer is empty.
|
|
38
41
|
*/
|
|
39
42
|
export function getSitemapPaths(options: SitemapPathsOptions = {}): {
|
|
40
43
|
paths: string[];
|
|
44
|
+
deny: string[];
|
|
41
45
|
entryCount: number;
|
|
42
46
|
pagesDir: string;
|
|
43
47
|
} {
|
|
@@ -46,22 +50,24 @@ export function getSitemapPaths(options: SitemapPathsOptions = {}): {
|
|
|
46
50
|
try {
|
|
47
51
|
entries = readdirSync(pagesDir, { recursive: true }) as string[];
|
|
48
52
|
} catch {
|
|
49
|
-
return { paths: [], pagesDir, entryCount: 0 }; // no page entries yet
|
|
53
|
+
return { paths: [], deny: [], pagesDir, entryCount: 0 }; // no page entries yet
|
|
50
54
|
}
|
|
51
55
|
const jsonEntries = entries.filter((entry) => entry.endsWith(".json"));
|
|
52
56
|
const entryCount = jsonEntries.length;
|
|
53
|
-
const paths =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
57
|
+
const paths: string[] = [];
|
|
58
|
+
const deny: string[] = [];
|
|
59
|
+
for (const rel of jsonEntries) {
|
|
60
|
+
try {
|
|
61
|
+
const data = JSON.parse(readFileSync(join(pagesDir, rel), "utf8"));
|
|
62
|
+
if (typeof data.route !== "string") continue;
|
|
63
|
+
if (!isAdvertised(data, { includeDrafts: false })) {
|
|
64
|
+
deny.push(data.route as string);
|
|
65
|
+
} else if (data.route !== "/") {
|
|
66
|
+
paths.push(data.route as string);
|
|
63
67
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
} catch {
|
|
69
|
+
// malformed entry; the build will surface the schema error
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { paths, deny, pagesDir, entryCount };
|
|
67
73
|
}
|
|
@@ -14,16 +14,41 @@ const PLACEHOLDER = "https://starter.invalid";
|
|
|
14
14
|
// names them here.
|
|
15
15
|
const PLATFORM_DENY = new Set(["/under-construction"]);
|
|
16
16
|
|
|
17
|
-
/** Whether a sitemap URL may be listed:
|
|
18
|
-
|
|
17
|
+
/** Whether a sitemap URL may be listed: neither a platform route nor a denied
|
|
18
|
+
* entry route (a draft or a noindex page), whichever way the integration
|
|
19
|
+
* found it. */
|
|
20
|
+
export function sitemapPageAllowed(
|
|
21
|
+
url: string,
|
|
22
|
+
deny: ReadonlySet<string> = new Set(),
|
|
23
|
+
): boolean {
|
|
19
24
|
try {
|
|
20
25
|
const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
|
|
21
|
-
return !PLATFORM_DENY.has(pathname);
|
|
26
|
+
return !PLATFORM_DENY.has(pathname) && !deny.has(pathname);
|
|
22
27
|
} catch {
|
|
23
28
|
return true;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/** The serialize step's URL work, extracted so it can be tested: resolve the
|
|
33
|
+
* placeholder origin against the real site, then drop a URL this build has
|
|
34
|
+
* already emitted. The integration de-duplicates by exact string before
|
|
35
|
+
* serialize runs, so a route that is both a page entry and a prerendered
|
|
36
|
+
* shell arrives twice, under the crawl's real origin and under ours, and
|
|
37
|
+
* becomes one URL only here. Returning nothing drops the item. */
|
|
38
|
+
export function resolveSitemapUrl(
|
|
39
|
+
url: string,
|
|
40
|
+
site: string,
|
|
41
|
+
emitted: Set<string>,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
const resolved =
|
|
44
|
+
site && url.startsWith(PLACEHOLDER)
|
|
45
|
+
? site + url.slice(PLACEHOLDER.length)
|
|
46
|
+
: url;
|
|
47
|
+
if (emitted.has(resolved)) return undefined;
|
|
48
|
+
emitted.add(resolved);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
export type SitemapWithCustomPagesOptions = SitemapOptions &
|
|
28
53
|
SitemapPathsOptions;
|
|
29
54
|
|
|
@@ -31,6 +56,8 @@ export function sitemapWithCustomPages(
|
|
|
31
56
|
options: SitemapWithCustomPagesOptions = {},
|
|
32
57
|
): AstroIntegration[] {
|
|
33
58
|
let resolvedSite = "";
|
|
59
|
+
// Emptied per build in astro:config:done.
|
|
60
|
+
const emitted = new Set<string>();
|
|
34
61
|
const { pagesDir, root, ...sitemapOptions } = options;
|
|
35
62
|
const userSerialize = sitemapOptions.serialize;
|
|
36
63
|
const userFilter = sitemapOptions.filter;
|
|
@@ -41,6 +68,9 @@ export function sitemapWithCustomPages(
|
|
|
41
68
|
// This integration is ordered ahead of @astrojs/sitemap, so the array is
|
|
42
69
|
// populated before anything reads it.
|
|
43
70
|
const customPages: string[] = [...(sitemapOptions.customPages ?? [])];
|
|
71
|
+
// Filled beside customPages: the entry routes the integration must not list
|
|
72
|
+
// from its own crawl of prerendered pages.
|
|
73
|
+
const denied = new Set<string>();
|
|
44
74
|
|
|
45
75
|
return [
|
|
46
76
|
{
|
|
@@ -49,6 +79,7 @@ export function sitemapWithCustomPages(
|
|
|
49
79
|
"astro:config:setup": ({ config, logger }) => {
|
|
50
80
|
const {
|
|
51
81
|
paths,
|
|
82
|
+
deny,
|
|
52
83
|
pagesDir: searched,
|
|
53
84
|
entryCount,
|
|
54
85
|
} = getSitemapPaths({
|
|
@@ -68,6 +99,9 @@ export function sitemapWithCustomPages(
|
|
|
68
99
|
`page entries in ${searched} are all drafts or the site root; the sitemap lists no SSR routes yet`,
|
|
69
100
|
);
|
|
70
101
|
}
|
|
102
|
+
for (const path of deny) {
|
|
103
|
+
denied.add(path.replace(/\/+$/, "") || "/");
|
|
104
|
+
}
|
|
71
105
|
for (const path of paths) {
|
|
72
106
|
customPages.push(
|
|
73
107
|
`${PLACEHOLDER}${path.startsWith("/") ? path : `/${path}`}`,
|
|
@@ -75,6 +109,7 @@ export function sitemapWithCustomPages(
|
|
|
75
109
|
}
|
|
76
110
|
},
|
|
77
111
|
"astro:config:done": ({ config }) => {
|
|
112
|
+
emitted.clear();
|
|
78
113
|
if (config.site) {
|
|
79
114
|
resolvedSite = String(config.site).replace(/\/$/, "");
|
|
80
115
|
}
|
|
@@ -85,11 +120,12 @@ export function sitemapWithCustomPages(
|
|
|
85
120
|
...sitemapOptions,
|
|
86
121
|
customPages,
|
|
87
122
|
filter: (page) =>
|
|
88
|
-
sitemapPageAllowed(page) &&
|
|
123
|
+
sitemapPageAllowed(page, denied) &&
|
|
124
|
+
(userFilter ? userFilter(page) : true),
|
|
89
125
|
serialize(item) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
const url = resolveSitemapUrl(item.url, resolvedSite, emitted);
|
|
127
|
+
if (!url) return undefined;
|
|
128
|
+
item.url = url;
|
|
93
129
|
return userSerialize ? userSerialize(item) : item;
|
|
94
130
|
},
|
|
95
131
|
}),
|