From 17daf446848443644538d9100b28018d190f5efa Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Mon, 25 May 2026 09:49:13 +0100 Subject: [PATCH] same --- public/_headers | 21 +++++++++ scripts/check-links.mjs | 98 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 public/_headers create mode 100644 scripts/check-links.mjs diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..76e5aa6 --- /dev/null +++ b/public/_headers @@ -0,0 +1,21 @@ +/* + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + +/_astro/* + Cache-Control: public, max-age=31536000, immutable + +/fonts/* + Cache-Control: public, max-age=31536000, immutable + +/media/* + Cache-Control: public, max-age=86400, stale-while-revalidate=604800 + +/favicon.ico + Cache-Control: public, max-age=604800 + +/*.xml + Cache-Control: public, max-age=300 + +/*.webmanifest + Cache-Control: public, max-age=300 diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs new file mode 100644 index 0000000..ded49c4 --- /dev/null +++ b/scripts/check-links.mjs @@ -0,0 +1,98 @@ +import { readdir, readFile, stat } from 'node:fs/promises'; +import path from 'node:path'; + +const dist = path.resolve('dist'); +const allowedPreservedRoutes = new Set(['/fleeting/', '/reconcile/']); +const failures = []; + +async function walk(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walk(fullPath))); + } else { + files.push(fullPath); + } + } + + return files; +} + +async function exists(file) { + try { + return (await stat(file)).isFile(); + } catch { + return false; + } +} + +async function targetExists(pathname) { + if (allowedPreservedRoutes.has(pathname)) return true; + + const safePath = path + .normalize(decodeURIComponent(pathname)) + .replace(/^\/+/, '') + .replace(/^(\.\.(\/|\\|$))+/, ''); + const candidate = path.join(dist, safePath); + const candidates = [ + candidate, + path.join(candidate, 'index.html'), + path.join(dist, `${safePath}.html`), + ]; + + for (const file of candidates) { + if (await exists(file)) return true; + } + return false; +} + +try { + await stat(dist); +} catch { + throw new Error('dist/ does not exist. Run npm run build first.'); +} + +const files = await walk(dist); +const htmlAndXml = files.filter((file) => /\.(html|xml)$/.test(file)); + +function pagePathname(file) { + const rel = path.relative(dist, file).replaceAll(path.sep, '/'); + if (rel === 'index.html') return '/'; + if (rel.endsWith('/index.html')) return `/${rel.slice(0, -'index.html'.length)}`; + return `/${rel}`; +} + +for (const file of htmlAndXml) { + const body = await readFile(file, 'utf8'); + const rel = path.relative(dist, file); + const baseUrl = new URL(pagePathname(file), 'https://schmelczer.dev'); + const matches = body.matchAll(/\b(?:href|src)=["']([^"'#?]+)(?:[?#][^"']*)?["']/g); + + for (const match of matches) { + const raw = match[1]; + if (/^(mailto:|tel:|data:)/i.test(raw)) continue; + + let parsed; + try { + parsed = new URL(raw, baseUrl); + } catch { + failures.push(`${rel}: invalid URL ${raw}`); + continue; + } + + if (parsed.origin !== 'https://schmelczer.dev') continue; + if (!(await targetExists(parsed.pathname))) { + failures.push(`${rel}: missing local target ${parsed.pathname}`); + } + } +} + +if (failures.length > 0) { + console.error(failures.join('\n')); + process.exit(1); +} + +console.log('No missing local href/src targets found in dist/.');