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
+59
View File
@@ -0,0 +1,59 @@
/* Single external script (no inline JS anywhere → strict script-src 'self').
Loaded blocking in <head> so the theme is set before first paint (no flash). */
(function () {
// --- Pre-paint theme -----------------------------------------------------
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";
}
function onReady(fn) {
if (document.readyState !== "loading") fn();
else document.addEventListener("DOMContentLoaded", fn);
}
onReady(function () {
// --- Theme toggle ------------------------------------------------------
var btn = document.getElementById("theme-toggle");
if (btn) {
btn.addEventListener("click", function () {
var root = document.documentElement;
var next = root.dataset.theme === "light" ? "dark" : "light";
root.dataset.theme = next;
try {
localStorage.setItem("theme", next);
} catch (e) {
/* ignore */
}
});
}
// --- Reveal on scroll (progressive enhancement, motion-aware) ----------
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var targets = document.querySelectorAll("[data-reveal]");
if (!reduce && "IntersectionObserver" in window) {
targets.forEach(function (el) {
el.classList.add("reveal");
});
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
);
targets.forEach(function (el) {
io.observe(el);
});
}
});
})();