Most of what I write here is about other projects. This page is about the blog itself — because how it’s built is part of the point.
The stack, and why it’s static
This is a static site: a folder of plain HTML, CSS, and JS with no server-side code and no database. I write posts in Markdown; Hugo (a single Go binary) builds them into files; Cloudflare Pages serves them worldwide.
I chose static deliberately. A dynamic CMS runs code for every visitor — a live
attack surface of plugins, a database, and a server that can be compromised. A
static site is built once, ahead of time, so there’s no per-request code to
exploit. For a blog that documents security work, the blog should be as
defensible as the work it shows. Hugo helps: one binary, no Node, no
node_modules tree to patch and audit.
- Generator: Hugo (extended) + the PaperMod theme, heavily customised
- Content: Markdown in
content/, YAML front matter - Host: Cloudflare Pages (global edge, automatic TLS, DDoS mitigation)
- Domain:
billsblog.dev— a.devTLD, which browsers force to HTTPS
The publishing pipeline
There’s no manual deploy step. The whole flow is one git push:
edit Markdown ─► git commit + push ─► GitHub ─► Cloudflare builds (hugo --gc --minify) ─► served at the edge
Cloudflare watches the repo and rebuilds on every push to main. The generated
public/ folder isn’t even committed — Cloudflare regenerates it, so tracking it
would just be noise.
Security — the part I actually enjoy
Because there’s no backend, the security story is about protecting the pipeline (accounts, tokens, DNS) and telling the browser how to behave (HTTP headers).
- Content Security Policy: a strict allowlist of what the page may load and
run.
script-srcis hash-locked — it lists the SHA-256 hash of each inline script instead of'unsafe-inline', so an injected<script>(different hash) is blocked. That’s the real XSS defence. - HSTS (
includeSubDomains; preload),X-Content-Type-Options: nosniff,X-Frame-Options: DENY/frame-ancestors 'none',Referrer-Policy,Permissions-Policy, andCross-Origin-Opener-Policy— served from a_headersfile at the edge. - The token that pushes to GitHub is a fine-grained PAT scoped to just this one repo, with an expiry and a rotation habit.
I wrote up the two big moves in detail: moving off GitHub Pages for real security headers and registering my own domain.
The over-engineering (I know)
The blog is also a playground. Under the hood there’s a WebGL shader background,
a preloader, smooth momentum scrolling, a command palette (Ctrl/⌘ + K), an
interactive terminal, scroll-driven text reveals, and — yes — a
Konami code. All of it is CSS-first; any JavaScript is external and served from
the site’s own origin so the hash-locked CSP stays clean, and everything respects
prefers-reduced-motion. It’s deliberately excessive. It’s also fast on repeat
visits — fingerprinted assets cached for a year, heavy media kept off mobile.
Colophon details
- Type: Rajdhani (self-hosted
woff2) - Theme accent:
#E81A1Aon a near-black background - Analytics: GoatCounter — no cookies, no cross-site tracking
- Comments: giscus, backed by GitHub Discussions (no database of my own)
- Build:
hugo --gc --minify,HUGO_VERSIONpinned
See the changelog for how it got here, and the stats for where it stands.