// Fails if any page in dist/ links to a site path or an #anchor that does not exist. // The docs are written for the forge and turned into routes at build time, so a // renamed page or heading surfaces here instead of as a 404 for a reader. import { readFileSync, readdirSync, existsSync } from "node:fs"; import { join, relative } from "node:path"; const dist = new URL("../dist/", import.meta.url).pathname; const pages = readdirSync(dist, { recursive: true }) .filter((f) => f.endsWith(".html")) .map((f) => join(dist, f)); const ids = new Map(); const idsOf = (file) => { if (!ids.has(file)) ids.set(file, new Set([...readFileSync(file, "utf8").matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]))); return ids.get(file); }; const pageOf = (path) => { const file = path.endsWith("/") ? join(dist, path, "index.html") : join(dist, path); return existsSync(file) ? file : null; }; const broken = []; for (const page of pages) { const from = "/" + relative(dist, page).replace(/index\.html$/, ""); for (const [, href] of readFileSync(page, "utf8").matchAll(/\shref="([^"]+)"/g)) { if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//")) continue; const [path, hash] = href.split("#"); const target = path === "" ? page : pageOf(path); if (!target) broken.push(`${from} -> ${href} (no page)`); else if (hash && !idsOf(target).has(decodeURIComponent(hash))) broken.push(`${from} -> ${href} (no #${hash})`); } } if (broken.length) { console.error(`${broken.length} broken link(s):\n${[...new Set(broken)].join("\n")}`); process.exit(1); } console.log(`links: ${pages.length} pages checked, no broken links`);