/index.html` and 301s the slashless form to it, so the slash form is the URL
+ * that actually answers 200 — the one canonical tags and the sitemap should name.
+ * The home page is excluded; Vite's own index.html already sits at the dist root.
+ */
+export function blogRoutes() {
+ return [
+ { route: "/blog/" },
+ ...slugsIn(BLOG_DIR).map((slug) => ({
+ route: `/blog/${slug}/`,
+ lastmod: newestDate(BLOG_DIR, slug),
+ })),
+ ];
+}
+
+/** Routes served by the docs SPA, whose own postbuild already writes their shells. */
+export function docsRoutes() {
+ return [{ route: "/docs/" }, ...slugsIn(DOCS_DIR).map((slug) => ({ route: `/docs/${slug}/` }))];
+}
diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs
index 64c5dd4..4165731 100644
--- a/scripts/build-site.mjs
+++ b/scripts/build-site.mjs
@@ -11,9 +11,10 @@
* single artifact — one GitHub Pages deployment hosts both sites.
*/
import { execSync } from "node:child_process";
-import { cpSync, rmSync } from "node:fs";
+import { cpSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
+import { absoluteUrl, blogRoutes, docsRoutes } from "../packages/landing/scripts/site-routes.mjs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const base = process.env.BASE_PATH ?? "/";
@@ -34,4 +35,27 @@ const landingDist = join(root, "packages", "landing", "dist");
const docsTarget = join(landingDist, "docs");
rmSync(docsTarget, { recursive: true, force: true });
cpSync(join(root, "packages", "docs", "dist"), docsTarget, { recursive: true });
+
+// sitemap.xml goes in last, because only here do both dists exist: it is the one file
+// that has to name routes from both sites. It is also the only way a crawler learns
+// those routes — both pages ship an empty and build their navigation
+// in JS, so following links is not an option.
+const routes = [{ route: "/" }, ...blogRoutes(), ...docsRoutes()];
+const sitemap = [
+ '',
+ '',
+ ...routes.map(({ route, lastmod }) =>
+ [
+ " ",
+ ` ${absoluteUrl(route)}`,
+ ...(lastmod === undefined ? [] : [` ${lastmod}`]),
+ " ",
+ ].join("\n"),
+ ),
+ "",
+ "",
+].join("\n");
+writeFileSync(join(landingDist, "sitemap.xml"), sitemap);
+
console.log(`\n[build-site] assembled site at ${landingDist} (docs under /docs/)`);
+console.log(`[build-site] wrote sitemap.xml (${routes.length} URLs)`);