Files
lobo 54b93055ba feat(app): add prerender, self-hosted fonts and privacy page
Fonts come from npm, not Google, so no third-party font requests.
Pages are prerendered at build time; browser-only code is guarded.
nginx serves per-route HTML, a real 404 and gzip.
2026-10-20 15:43:31 +02:00

79 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
// Verifies the production build: every route has prerendered HTML with its own
// canonical and og:url, the CSR shell exists, and no Google font host is left.
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const distDir = join(root, 'dist', 'playground-frontend', 'browser');
const ORIGIN = 'https://andreas-dahm.eu';
const FORBIDDEN_HOSTS = ['fonts.googleapis.com', 'fonts.gstatic.com'];
function* walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(path);
} else {
yield path;
}
}
}
function routePaths() {
const constants = readFileSync(join(root, 'src', 'app', 'constants', 'RouterConstants.ts'), 'utf8');
const paths = [...constants.matchAll(/PATH: '([^']+)'/g)].map(match => match[1]);
return ['', ...paths];
}
function checkRoute(route, failures) {
const file = join(distDir, route, 'index.html');
if (!existsSync(file)) {
failures.push(`missing ${file}`);
return;
}
const html = readFileSync(file, 'utf8');
const url = `${ORIGIN}/${route}`;
const label = route || '/';
if (!html.includes(`rel="canonical" href="${url}"`) && !html.includes(`href="${url}" rel="canonical"`)) {
failures.push(`${label}: canonical is not ${url}`);
}
if (!html.includes(`property="og:url" content="${url}"`) && !html.includes(`content="${url}" property="og:url"`)) {
failures.push(`${label}: og:url is not ${url}`);
}
if (!/<title>[^<]+<\/title>/.test(html)) {
failures.push(`${label}: empty <title>`);
}
}
function checkForbiddenHosts(failures) {
for (const file of walk(distDir)) {
const content = readFileSync(file, 'latin1');
for (const host of FORBIDDEN_HOSTS) {
if (content.includes(host)) {
failures.push(`${file} references ${host}`);
}
}
}
}
if (!existsSync(distDir)) {
console.error(`Build output not found: ${distDir}`);
process.exit(1);
}
const failures = [];
routePaths().forEach(route => checkRoute(route, failures));
if (!existsSync(join(distDir, 'index.csr.html'))) {
failures.push('missing index.csr.html');
}
checkForbiddenHosts(failures);
if (failures.length > 0) {
console.error(`Prerender check failed:\n${failures.join('\n')}`);
process.exit(1);
}
console.log('Prerender check passed.');