TLDR: A Content Security Policy isn't just a list of trusted domains. It's a matrix of domains × resource types — and one missing intersection means a blank screen.
The Setup
We were hardening a resource finder app for a cancer education business — a Next.js app pulling content from TakeShape (our legacy CMS we're migrating off), media from Supabase (our new backend), and fonts from Google.
Adding security headers felt straightforward. CSP (Content Security Policy — a browser-enforced whitelist of where your page is allowed to load resources from) went into next.config.ts in about ten minutes.
Then things quietly broke.
What Broke (And Why I Thought It Was Fixed Twice)
First: fonts were gone. Bricolage Grotesque, the display face — just not there.
I added fonts.googleapis.com to style-src and redeployed. Fonts came back. Done, right?
Wrong. I'd actually needed TWO separate directives: style-src for the CSS stylesheet Google Fonts serves, AND font-src for the actual .woff2 files. One origin, two resource types, two directives. I'd gotten lucky that both happened to be missing and both got caught in the same console sweep.
A day later — the hero video was blank.
The video is hosted on TakeShape's CDN at assets.takeshape.io. I already had that domain in img-src. Should've been fine.
It wasn't. Because <video> isn't governed by img-src.
There's a completely separate directive called media-src. Video and audio elements live there. default-src doesn't cover it. img-src doesn't cover it. The browser blocks it silently — no visible error on screen, just a blank space where your video should be.
Added media-src assets.takeshape.io images.takeshape.io. Video rendered immediately.
The Thing That Makes This Tricky
CSP failures don't fail your build. They don't show up in your Vercel logs. A green deploy can be visually broken and you won't know unless you check the browser console manually.
That's the real danger. It feels like an ops problem but it's actually a QA surface that almost nobody is watching.
The Mental Model That Actually Helps
Before you write a single CSP line, enumerate every external origin AND every resource type it serves:
- Google Fonts →
style-src(the CSS) +font-src(the woff2s) - A CMS CDN →
img-src(images) +media-src(video/audio) - A backend API →
connect-src(fetch/XHR calls) - An embed or widget → probably
script-src+frame-src
The pattern is: domain tells you who. Directive tells you what. You need both or the browser says no.
Why This Matters to Me
I've shipped security headers on probably a dozen projects now and I still got burned here. The mental model I had was "list the domains you trust." The correct mental model is "list the domains × the resource types they serve."
That's a small reframe with a big blast radius if you get it wrong. Build the matrix before you write the header — not after the hero video is blank in production.