M4: security pass — strict CSP, header split, build-time scan

All JS moved to external /site.js → script-src 'self' with no inline JS,
hashes or eval. Full header set via nginx (CSP, nosniff, frame-deny,
referrer, permissions, COOP/CORP); HSTS stays at the CF edge. Shared
headers include avoids the location add_header reset footgun. Build-time
secret/inline-script/third-party scan gate. SECURITY.md documents posture.
This commit is contained in:
2026-06-17 17:12:57 +10:00
parent cb76a87c36
commit c1db5cec86
9 changed files with 210 additions and 64 deletions
+2 -13
View File
@@ -62,16 +62,5 @@
}
</style>
<script>
const btn = document.getElementById("theme-toggle");
btn?.addEventListener("click", () => {
const root = document.documentElement;
const next = root.dataset.theme === "light" ? "dark" : "light";
root.dataset.theme = next;
try {
localStorage.setItem("theme", next);
} catch (e) {
/* ignore */
}
});
</script>
<!-- Toggle behaviour lives in /public/site.js (no inline JS → strict CSP). -->
+4 -38
View File
@@ -50,23 +50,10 @@ const ogImage = new URL(site.ogImage, site.url).href;
<meta name="theme-color" content="#090c14" />
<!-- Pre-paint theme bootstrap: avoids a flash of the wrong theme.
The ONE inline script on the site; hashed by Astro CSP in M4. -->
<script is:inline>
(function () {
try {
var stored = localStorage.getItem("theme");
var theme =
stored ||
(window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark");
document.documentElement.dataset.theme = theme;
} catch (e) {
document.documentElement.dataset.theme = "dark";
}
})();
</script>
<!-- Single external script: pre-paint theme + toggle + scroll reveal.
Blocking in <head> so the theme is set before first paint (no flash).
No inline JS anywhere on the site → strict CSP script-src 'self'. -->
<script is:inline src="/site.js"></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
@@ -75,26 +62,5 @@ const ogImage = new URL(site.ogImage, site.url).href;
<slot />
</main>
<Footer />
<!-- Reveal-on-scroll: progressive enhancement, motion-aware. -->
<script>
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const targets = document.querySelectorAll<HTMLElement>("[data-reveal]");
if (!reduce && "IntersectionObserver" in window) {
targets.forEach((el) => el.classList.add("reveal"));
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
}
},
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
);
targets.forEach((el) => io.observe(el));
}
</script>
</body>
</html>