@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.
@@ -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.0._
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.0",
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). Returns `{ paths, pagesDir }` so a caller can report
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 = jsonEntries
54
- .filter((rel) => /\.json$/i.test(rel))
55
- .map((rel) => {
56
- try {
57
- const data = JSON.parse(readFileSync(join(pagesDir, rel), "utf8"));
58
- if (typeof data.route !== "string") return null;
59
- if (!isAdvertised(data, { includeDrafts: false })) return null;
60
- return data.route as string;
61
- } catch {
62
- return null; // malformed entry; the build will surface the schema error
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
- .filter((path): path is string => path !== null && path !== "/");
66
- return { paths, pagesDir, entryCount };
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: everything but the platform's own routes. */
18
- export function sitemapPageAllowed(url: string): boolean {
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) && (userFilter ? userFilter(page) : true),
123
+ sitemapPageAllowed(page, denied) &&
124
+ (userFilter ? userFilter(page) : true),
89
125
  serialize(item) {
90
- if (resolvedSite && item.url.startsWith(PLACEHOLDER)) {
91
- item.url = resolvedSite + item.url.slice(PLACEHOLDER.length);
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
  }),