Files
ta1sph.info/scripts/check-site.mjs
T
suphicakir 7d8fcce2e1 feat: add OG images, favicon set, manifest and performance tuning
- Build-time Open Graph cards per post and a default card (Satori + Sharp)
- Favicon/ICO/apple-touch/manifest icons generated from one SVG (npm run icons)
- robots.txt and web manifest generated from site.json
- Subset self-hosted fonts to Latin + Turkish (188 KB -> 66 KB) with preload
- Inline the stylesheet to remove the render-blocking request
- Post-build site audit (npm run check:site): h1, alt, SEO tags, links
- Omit canonical on noindex pages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:36:56 +03:00

76 lines
3.0 KiB
JavaScript

// 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(/<h1[\s>]/g) ?? []).length;
if (h1 !== 1) report(url, `${h1} <h1> elements`);
if (!/<html[^>]+lang="/.test(src)) report(url, 'missing <html lang>');
if (!/<title>[^<]+<\/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('&amp;', '&'), `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');