// 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 <title>'); if (url !== '/404' && !/<meta name="description" content="[^"]+"/.test(src)) report(url, 'missing meta description'); if (url !== '/404' && !/<link rel="canonical" href="https:\/\/ta1sph\.info\//.test(src)) report(url, 'missing canonical'); for (const img of src.match(/<img\b[^>]*>/g) ?? []) { if (!/\salt="/.test(img)) report(url, `img without alt: ${img.slice(0, 80)}`); } } for (const [url, src] of html) { const refs = [...src.matchAll(/\s(?:href|src)="([^"]+)"/g)].map((m) => 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');