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.
79 lines
2.5 KiB
JavaScript
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.');
|