Resources / Field guide

Why your site looks broken only in production

The CSS deployed, the markup is right, it's perfect on localhost — and production renders like the stylesheet never loaded. The likely culprit is a Content-Security-Policy that only enforces in prod and is quietly stripping your inline styles. Here's the diagnosis trail and the fix.

The symptom that makes no sense

You publish a redesign. Locally it's flawless. In production the external stylesheet clearly loaded — you can see its classes in the file on the origin — yet the page renders "off": the hero grid collapses, custom banners vanish, the web font never shows up. It's broken in Incognito too, so it isn't a cache. Every instinct says "bad deploy," but the deploy is fine.

When local and production diverge with identical code, something environment-specific is rewriting the response. A very common one is Content-Security-Policy.

What's actually happening

Many apps ship a CSP that behaves differently per environment — and that difference is the trap:

  • In development the policy is sent as Content-Security-Policy-Report-Only. Report-only reports violations but blocks nothing, so everything renders and you never notice a problem.
  • In production the same policy is enforced. Now anything the policy doesn't explicitly allow is actually blocked.

So a policy like this looks safe in dev and silently breaks in prod:

Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'nonce-{nonce}';
  style-src   'self' 'nonce-{nonce}' https://cdn.jsdelivr.net;

There's no 'unsafe-inline' in style-src, so every inline <style> block and every style="…" attribute that lacks the nonce gets stripped. Your external CSS loads fine — it's 'self' — but the page-layout CSS that lived inline disappears. And if the font domains aren't listed (fonts.googleapis.com in style-src, fonts.gstatic.com in font-src), the web font never loads either.

The diagnosis trail

This is the sequence that pins it down fast:

  • Fetch the CSS straight from the origin. If the new classes are there, the deploy worked — rule out "files didn't ship."
  • Reproduce in Incognito. Still broken means it's not browser cache.
  • View source. Seeing the new markup confirms the app DLL/build is live too.
  • Open DevTools → Issues / Console. The smoking gun is a message like: "Content Security Policy blocks inline execution of scripts and stylesheets."

Once that message appears, the question shifts from "what didn't deploy" to "what is the policy refusing to render."

The fix

The pragmatic fix is to allow inline styles and the font domains, while keeping scripts locked down:

Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'nonce-{nonce}';                                  ← stays strict
  style-src   'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net;
  font-src    'self' https://fonts.gstatic.com;

Why split scripts and styles? Because script injection is the real XSS threat, and you want that nonce-strict. Inline styles are far lower risk, so 'unsafe-inline' there is a reasonable trade to get the site rendering. Keep the script policy tight; loosen only what you must.

Note on nonces: if a directive lists both a nonce and 'unsafe-inline', browsers ignore 'unsafe-inline' entirely — the nonce wins. So you can't "add unsafe-inline alongside the nonce" and expect inline styles to work; you either go nonce-based (and nonce every inline style) or drop the nonce from style-src and use 'unsafe-inline'.

The CSP often lives in compiled middleware. If the policy is built in a custom middleware class compiled into your DLL, changing it needs a rebuild and re-publish — an App Service restart does nothing, because the old policy is baked into the binary. More than one person has "fixed" the header, restarted, and been baffled that nothing changed.

Doing it properly later

If you want the strict posture back, the clean path is to move every inline style into your external stylesheet as real classes, then re-enable the nonce-based style-src and drop 'unsafe-inline'. That keeps the security benefit without the rendering surprise. Until then, the relaxed style-src is a fine, conscious trade-off — just make it a decision, not an accident.

The takeaway

When production and local disagree and the files clearly shipped, suspect something that only runs in production — and CSP is high on that list. Send it report-only first in lower environments so you see violations before they bite, keep script-src strict, and remember the policy may be compiled into your app.

Want production-grade security that doesn't break your UI?

Headers, auth, and hardening that protect the app without fighting it. I build and review real ASP.NET systems directly — if you want a senior developer on it, let's talk.

Work with me