Portal code only; editorial content lives in the ctao/content repo on the demo machine's Gitea and is overlaid at build time (see README.md and deploy/README.md). Live demo: https://astro.isl-dev.grid.cyfronet.pl
39 lines
1.5 KiB
JavaScript
39 lines
1.5 KiB
JavaScript
// 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';
|
|
|
|
const DIST = new URL('../dist', import.meta.url).pathname;
|
|
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);
|