fix(deploy): content-hash cache-busting + block source/config leaks + real 404s

- build.sh stamps ?v=<md5> on JS/CSS refs; Cloudflare was serving 4h-stale bundles
- nginx: 404 for .mjs/.md/.yml/.sh/.conf/Dockerfile/dotfiles (build-pages.mjs, README, compose were public)
- nginx: unknown paths 404 instead of soft-404 SPA fallback (Search Console soft-404s)
- track live nginx.conf in repo
This commit is contained in:
Zac Gaetano 2026-10-01 20:43:16 -04:00
parent 413ed7591f
commit d4d46b085e
24 changed files with 68 additions and 45 deletions

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -6,3 +6,8 @@ for f in components tweaks-panel app; do
npx -y esbuild@0.24 "$f.jsx" --loader:.jsx=jsx --jsx=transform --minify --target=es2020 --outfile="$f.min.js" --log-level=warning
done
node build-pages.mjs
# Cache-bust: Cloudflare caches assets 4h; stamp content hashes into HTML refs.
for a in components.min.js tweaks-panel.min.js app.min.js styles.css; do
h=$(md5sum "$a" | cut -c1-8)
find . -name index.html -not -path './node_modules/*' -exec sed -i -E "s#(\"/$a)(\?v=[0-9a-f]+)?\"#\1?v=$h\"#g" {} +
done

View file

@ -47,7 +47,7 @@
<link rel="dns-prefetch" href="//vimeo.com" />
<link rel="preload" as="image" href="/images/photos/production-switcher.jpg" fetchpriority="high" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<!-- Structured data -->
@ -276,9 +276,9 @@
<script src="/data.js"></script>
<script src="/projects-data.js"></script>
<script src="/components.min.js"></script>
<script src="/tweaks-panel.min.js"></script>
<script src="/app.min.js"></script>
<script src="/components.min.js?v=8bda577a"></script>
<script src="/tweaks-panel.min.js?v=fe3876ca"></script>
<script src="/app.min.js?v=41713656"></script>
<style>
/* image treatments */

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -1,6 +1,5 @@
server {
listen 43036 default_server;
listen [::]:43036 default_server;
listen 80;
server_name _;
root /usr/share/nginx/html;
@ -9,33 +8,52 @@ server {
charset utf-8;
gzip on;
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_vary on;
# Long cache for static assets — they're content-addressed by path,
# bust via filename when they change.
location ~* \.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf)$ {
expires 30d;
# Security headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Vendored libs — versioned, cache hard
location /vendor/ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# Short cache for the HTML/JS that drive the SPA so edits show up fast.
location ~* \.(html|css|js|jsx)$ {
expires 5m;
# Images/fonts — long cache
location ~* \.(png|jpg|jpeg|gif|webp|avif|svg|ico|woff2?|ttf)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# App JS/CSS — short cache so edits show fast
location ~* \.(css|js)$ {
expires 10m;
add_header Cache-Control "public, must-revalidate";
}
# Single-page app — unknown paths fall back to index.html, which
# handles routing via location.hash (#/projects/<slug>).
location / {
try_files $uri $uri/ /index.html;
}
# Never serve build sources, docs, or deploy config
location ~* (\.(jsx|mjs|py|sh|md|ya?ml|conf|tgz)|/Dockerfile)$ { return 404; }
location ~ /\. { return 404; }
location /scripts/ { return 404; }
# Crawler files.
location = /llms.txt { default_type text/plain; }
location = /robots.txt { default_type text/plain; }
location = /llms.txt { default_type text/plain; }
location = /sitemap.xml { default_type application/xml; }
# Source maps / raw sources aren't shipped; keep vendor un-listed.
location /vendor/ { autoindex off; }
# HTML — no stale cache
location = /index.html {
add_header Cache-Control "no-cache, must-revalidate";
}
# Real 404s for unknown paths (SPA routes via location.hash, so / covers them).
# ponytail: plain nginx 404 page; add a branded 404.html if traffic warrants it
location / {
try_files $uri $uri/ =404;
}
}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}

View file

@ -16,7 +16,7 @@
<link rel="icon" href="/images/dragon-mark.png" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/styles.css?v=01730c57">
<style>:root{--font-inter:"Inter";--font-mono-stack:"JetBrains Mono"}</style>
<style>
.sp-nav{background:var(--color-primary);padding:1.1rem 0}.sp-nav-inner{display:flex;align-items:center;justify-content:space-between}