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.
This commit is contained in:
1 parent
57f861510e
commit
54b93055ba
42 files changed
+544
-88
No files matched your search
@@ -0,0 +1,78 @@
|
||||
#!/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.');
|
||||
Reference in new issue
Block a user