// Deterministic internal-link check over the built site (zero dependencies). // Run AFTER `astro build`: scans dist/**/*.html for root-relative href/src // values and fails (exit 1) if any target has no file in dist/. External // URLs, mailto:, data: and pure-#fragment links are out of scope. import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; const DIST = fileURLToPath(new URL('../dist', import.meta.url)); if (!existsSync(DIST)) { console.error('dist/ not found — run `npm run build` first'); process.exit(1); } const htmlFiles = []; (function walk(dir) { for (const name of readdirSync(dir)) { const p = join(dir, name); if (statSync(p).isDirectory()) walk(p); else if (name.endsWith('.html')) htmlFiles.push(p); } })(DIST); const resolves = (path) => { const clean = decodeURI(path.split(/[?#]/)[0]); if (clean === '/') return true; return ['', '.html', '/index.html'].some((suffix) => existsSync(join(DIST, clean.replace(/\/$/, '') + suffix))); }; let broken = 0; for (const file of htmlFiles) { const html = readFileSync(file, 'utf8'); for (const [, , url] of html.matchAll(/\s(href|src)="(\/[^"]*)"/g)) { if (!resolves(url)) { console.error(`broken: ${url} (in ${file.slice(DIST.length + 1)})`); broken++; } } } console.log(broken ? `${broken} broken internal link(s)` : `OK — ${htmlFiles.length} pages, all internal links resolve`); process.exit(broken ? 1 : 0);