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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// 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('&', '&'), `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');
|
||||
Reference in New Issue
Block a user