// Post-build audit of dist/client: one h1, alt text, SEO tags, unique ids, internal links/anchors.
// Usage: npm run build && npm run check:site
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
const ROOT = 'dist/client';
const SERVER_ROUTES = [/^\/api\//]; // on-demand routes, not files
const walk = (dir) =>
readdirSync(dir).flatMap((f) => {
const p = join(dir, f);
return statSync(p).isDirectory() ? walk(p) : [p];
});
const pages = walk(ROOT).filter((f) => f.endsWith('.html') && !f.includes('/pagefind/'));
const idsByPage = new Map();
const problems = [];
const report = (page, msg) => problems.push(`${page}: ${msg}`);
const urlOf = (file) =>
'/' +
relative(ROOT, file)
.replace(/index\.html$/, '')
.replace(/\.html$/, '');
const html = new Map(pages.map((f) => [urlOf(f), readFileSync(f, 'utf8')]));
for (const [url, src] of html) {
const ids = [...src.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
idsByPage.set(url, new Set(ids));
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
if (dupes.length) report(url, `duplicate id(s): ${[...new Set(dupes)].join(', ')}`);
const h1 = (src.match(/
]/g) ?? []).length;
if (h1 !== 1) report(url, `${h1} elements`);
if (!/]+lang="/.test(src)) report(url, 'missing ');
if (!/[^<]+<\/title>/.test(src)) report(url, 'missing ');
if (url !== '/404' && !/ m[1]);
for (const ref of refs) {
if (/^(https?:|mailto:|tel:|data:|javascript:)/.test(ref) || ref.startsWith('//')) {
if (!ref.startsWith('https://ta1sph.info')) continue;
}
const target = new URL(ref.replaceAll('&', '&'), `https://ta1sph.info${url}`);
if (target.host !== 'ta1sph.info') continue;
const path = decodeURIComponent(target.pathname);
if (SERVER_ROUTES.some((r) => r.test(path))) continue;
const file = path.endsWith('/')
? join(ROOT, path, 'index.html')
: ([join(ROOT, path), join(ROOT, `${path}.html`)].find(existsSync) ?? join(ROOT, path));
if (!existsSync(file)) {
report(url, `broken link → ${path}`);
continue;
}
if (target.hash && idsByPage.has(path)) {
const id = decodeURIComponent(target.hash.slice(1));
if (!idsByPage.get(path).has(id)) report(url, `missing anchor → ${path}${target.hash}`);
}
}
}
console.log(`Checked ${html.size} pages.`);
if (problems.length) {
console.error(problems.map((p) => ` ✗ ${p}`).join('\n'));
process.exit(1);
}
console.log(' ✓ no problems found');