The bug behind the bug
Someone pointed out that the “On GitHub” section of this site was showing broken image icons instead of my stats and pinned repos. Simple bug, I figured - probably a CSP thing, since I’d just locked down the security headers a couple issues back. It wasn’t. But fixing it surfaced a second, worse bug that had nothing to do with GitHub at all.
The reported bug: a dead demo, not a dead config. The stats card and all four pinned-repo cards load from github-readme-stats.vercel.app, a free public tool a lot of GitHub profiles rely on. Curling it directly returned 503 DEPLOYMENT_PAUSED - not a rate limit, not a blip. A quick search turned up a string of open issues on the upstream repo going back to January: the maintainer paused the public demo deployment, and it’s stayed paused. The project’s own README already says as much - the public endpoint is “best-effort,” and the recommended fix is to self-host or generate stats via GitHub Actions.
Neither of those fit well here. Actions needs a private-repo billing tier I’ve deliberately opted out of, and self-hosting means a second service to maintain for what’s a decorative widget. I looked for a genuine alternative - something that doesn’t just trade one single point of failure for an identical one - and didn’t find one. Every option is either “someone else’s free demo” or “bring your own token.” I ended up doing the pragmatic thing: swapping the domain to an actively maintained fork, github-stats-extended.vercel.app, that’s a drop-in replacement for the exact same API. It’s not a permanent fix. It’s the same risk with a healthier maintainer behind it, and I wrote that down plainly rather than pretending otherwise.
The bug I wasn’t looking for. While verifying the fix in a real browser, I checked the console for CSP violations out of habit - and found one that had nothing to do with GitHub. A while back, I’d added a single line to the game-launch script for keyboard accessibility: move focus to the game iframe after the button click. Reasonable, small, unrelated to security in any obvious way.
Except the CSP for this site pins that exact inline script by its SHA-256 hash - a legitimate way to allow one specific inline script without opening the door to arbitrary inline execution. Change one character in the script, and the hash changes. Nobody had recomputed it. The build passed, typecheck passed, formatting passed - none of those know or care what a CSP hash is. The button still looked normal. Click it, though, and nothing happened, silently, in production, since the previous deploy.
That’s the part that stuck with me. A hash-pinned CSP is more secure than 'unsafe-inline', and I’d argue for it again. But it’s also a trap with no alarm: there’s no linter, no test, no build step that ties a script’s content to the CSP that’s supposed to allow it. The only way I caught this was habit - running a real browser against a real build before calling something done, instead of trusting the fact that nothing had errored.
Fixed both in the same pass, recomputed the hash, re-verified with the same headless-browser check. Wrote the failure mode down properly this time, with the actual command to recompute the hash, so next time a one-line a11y tweak touches that script, there’s something to jog the memory instead of relying on catching it by accident again.
Related reading
Making a CSP hash a build output instead of a hand-maintained one
A game launch button broke silently two days before anyone noticed. The fix wasn't recomputing a hash - it was making sure nobody ever has to again.
A one-line token swap that found a broken button and a GDPR gap
Fixing a placeholder analytics token turned into a lesson about how CSP hashes go stale silently, and why a dev server can lie to you about production behavior.
Three ways "the spec said X" was wrong
A dead symlink target, a fix already queued by automation, and docs describing components that don't exist - three sources of truth, same lesson each time.