41 lines
1.6 KiB
JavaScript
41 lines
1.6 KiB
JavaScript
// 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`);
|