A GDPR-clean frontend: self-hosting fonts, icons and audio
This site has no cookie banner. Not because I'm ignoring the rules, but because the frontend is built so that there is nothing to consent to: no third-party requests at page load, no tracking storage, no analytics. Here's what that takes in practice.
The principle: no third party touches the page load
The moment your page loads a font from Google Fonts or an icon set from a CDN, the visitor's IP address goes to a third party before they've made any choice. So the rule here is simple: everything the page needs at load time is served from this server.
Fonts
JetBrains Mono, Fira Code and Inter are self-hosted. The font files live under assets/fonts/, declared with plain @font-face rules in a single fonts.css. That's the entire integration — no fonts.googleapis.com, no preconnect to anyone. Self-hosting fonts is genuinely easy now: download the woff2 files once, write the @font-face blocks, done. The German court rulings on Google Fonts made this a compliance topic, but it's also just faster — one origin, no extra DNS lookup and TLS handshake.
Icons
The icon set (Boxicons) is vendored into the repo under assets/vendor/boxicons/ — CSS and font files served from the same origin. Same deal: no CDN, version pinned, and the site keeps working if the upstream project ever disappears.
Audio
The site has a built-in music player with a small self-produced soundtrack. The files are encoded twice — Opus in a WebM container for modern browsers, MP3 as fallback — and the player picks via canPlayType('audio/webm; codecs=opus') at runtime. Self-hosting audio instead of embedding a streaming widget matters more than fonts: a single embedded player iframe would pull in third-party scripts and cookies on every page view. Two encodes and a manifest file replace all of that.
External links
Links to GitHub, LinkedIn and so on are static <a> tags with rel="noopener noreferrer" — no embeds, no social widgets, no pixels. Nothing is transmitted to those providers until the visitor actively clicks, and the referrer isn't passed along either.
Why there's no cookie banner
Consent banners are required for storage access that isn't strictly necessary. This site only uses two kinds of storage:
- One session cookie (
voidcore_sid), set only when you open one of the server-backed forms (contact, guestbook, highscore submit). It binds the CSRF token to your session, isHttpOnly,Secure,SameSite=Lax, and dies when the browser closes. That's squarely in the strictly-necessary exemption (§ 25 Abs. 2 TDDDG) — it exists to protect you from cross-site request forgery. - localStorage for UI preferences — window layout, sound on/off, game scores. Local, functional, never transmitted.
No tracking, analytics or marketing storage means no consent requirement, which means no banner. The privacy policy still documents every single localStorage key with its purpose, and there's a hard rule in the project docs: any code change that adds a storage key, a cookie or a POST endpoint must update the privacy policy in the same change. Privacy text that drifts from the code is worse than no text.
The server side: REMOTE_ADDR only
One backend principle worth stealing: the API endpoints determine the client IP from REMOTE_ADDR exclusively — the address the reverse proxy actually observed on the TCP connection. X-Forwarded-For is ignored, because any client can write whatever it wants into that header. This matters for data minimization and security: rate limits keyed on a spoofable header are decorative, and a privacy policy describing IP handling should describe the address you actually saw, not one the client claimed.
The contact and guestbook endpoints store IPs only in hashed, peppered form for rate limiting, and the rate limiter itself is a flock'd JSON file — no third-party anti-spam service sees the traffic.
Takeaway
"GDPR-clean" here wasn't a compliance retrofit; it's an architecture property. No build system pulling CDN URLs into templates, no marketing snippets, and a short checklist: self-host fonts, vendor your icons, encode your own audio, make external content click-activated, keep storage essential, and trust only REMOTE_ADDR. The banner you don't need is the best banner UX there is.