Static Website Config and CSP
Purpose
Own the staticwebapp.config.json and front-end layer of a live static site: security headers, cache policy, MIME types and routes (hidden files, redirects, rewrites), a Content Security Policy that is tightened without breaking pages, and the hard-won rules for editing a multi-page static HTML site that is already in production. Split from static-website-hosting, which provisions and deploys the site.
When to use
After the site is live (see static-website-hosting), whenever you:
- Add or change headers, cache rules, MIME types, redirects, or rewrites in
staticwebapp.config.json - Introduce or tighten a CSP, or add JavaScript/CSS to a CSP-protected site
- Reorganise pages into subfolders, add a mobile menu, or edit shared markup across pages
- Need to hide committed internal files, serve unusual file types (
.vcf, extensionless well-known files), or host an auth page on the static site
Inputs expected
Partial inputs are fine — infer from the repo and ask only where needed.
- The current
staticwebapp.config.jsonand the site's folder layout - The current CSP (if any) and where scripts/styles live (inline vs external)
- Which pages/URLs are public, transient (auth), or app deep-link targets
- What is being changed (new page, moved page, new script, header change, new file type)
Guiding principles
- Keep
staticwebapp.config.jsonin source control and use it for headers, MIME types, and explicit routes for files (e.g.sitemap.xml) that the SPA fallback would otherwise intercept. - Security headers go in
globalHeaders, not a/*route. Route-based headers apply only to HTML responses;globalHeaderscovers CSS, JS, images — every response type. - Cache:
Cache-Control: public, must-revalidate, max-age=30globally on Free tier (no long-lived invalidation), then override per asset type with routes placed before the/*catch-all:/*.cssand/*.jsatmax-age=3600,/assets/*atmax-age=86400, HTML atmax-age=0, must-revalidate. Route order matters — the catch-all must be last. - Everything under
app_locationis public unless a route hides it. A committed doc, script or infra file is fetchable; a route with"statusCode": 404and no rewrite/redirect returns 404 without serving the body (verified live). Wildcards match only at the END of a route (/docs/*), so/*.mdis unreliable — block directories plus exact-match root files (/OPERATIONS.md,/.gitignore). Dot-path routes work (/.github/*); the built-in/.well-known/assetlinks.jsonroute proves it. See Hiding internal files inreference.md. - SWA rejects
statusCodecombined withrewriteon a route ("Status code cannot be specified for a rule with Rewrite") — the deploy fails config validation in about 30 seconds and the site keeps serving the previous version. To mask blocked paths as a branded 404 page, put"allowedRoles": ["administrator"]on the routes plus a"401": { "rewrite": "/404.html", "statusCode": 404 }responseOverride —statusCodeplusrewriteIS allowed insideresponseOverrides. - An
<iframe srcDoc=...>(about:srcdoc) inherits the EMBEDDING page's CSP, so images/fonts loaded inside the iframe from an external host (e.g. an email preview pulling template images from the marketing site) are blocked unless the parent page'simg-srcallows that host — widen the global CSPimg-src, not anything on the iframe. - Register MIME types explicitly —
.xml application/xml,.txt text/plain(crawlers),.json,.vcf text/vcard(iOS/Android "add to contacts" is unreliable on octet-stream). Extensionless files such asapple-app-site-associationare served with the wrong content type unless an explicit route setsContent-Type: application/json. - A static
redirectroute drops the query string (/x?a=1→ bare target). Fine for a QR encoding a bare path; revisit if a link needs UTM pass-through. script-src 'self'silently blocks inline<script>blocks and inline handler attributes (onclick,onchange). They work locally (file://has no CSP) and fail on the live site. All JS lives in external same-origin.jsfiles; attach handlers withaddEventListener.style-srcstill needs'unsafe-inline'until every<style>block andstyle=attribute is externalised — extracting scripts alone does not let you tighten it. Deploy any new CSP asContent-Security-Policy-Report-Onlyfirst and clear the DevTools Console violations before enforcing.- Guard every shared-script DOM lookup with a null check.
document.getElementById('year').textContent = …on a page without that element throws and halts all later script on the page. Writevar el = …; if (el) el.textContent = …. - Cache-bust JS/CSS with a version query string (
scripts.js?v=2) and bump it whenever a change would break cached behaviour; after deploying, verify against the live URL (or hard refresh, Ctrl+Shift+R) — atmax-age=3600the user may still be seeing the old file. - On a templating-free static site every shared component is a separate copy per page. Read each page's actual nav markup before editing — CTA text, hrefs and aria attributes routinely differ.
- Mobile menu dropdowns live inside
<header>, not after it — a sticky header is a containing block; a nav outside it losesbackdrop-filterand sticky positioning. When adding a hamburger menu, audit and replace stale mobile media-query rules for the nav rather than appending. - Reorganising into subfolders: redirect every old URL — 301 for public/SEO pages, 302 for transient/auth pages — so links and app deep-links keep working. SWA serves
index.htmlfor a directory andfoo.htmlfor/foo; add an explicitrewritefor the no-trailing-slash folder URL to pin the canonical. Convert relative asset references (styles.css,assets/…) to root-absolute (/styles.css) on any relocated page — they otherwise 404. - A static page can double as a Supabase auth page (password reset): read the recovery token from the URL hash, load supabase-js from a CDN,
setSessionthenupdateUser; give its route arewriteto the.htmland a per-route CSP allowingconnect-src https://<project-ref>.supabase.coandscript-src … https://cdn.jsdelivr.net. Seesupabase-auth-emailfor hardening those pages. - A page that is an App Link / Universal Link target is verified domain-wide by
.well-known/assetlinks.json(handle_all_urls) — moving its URL needs no assetlinks edit, but the app's intent-filter paths and any auth redirect URLs must be updated app-side.
Process
- Read the current config and CSP, and list every page and its shared components (nav, footer, scripts).
- Headers & cache — security headers in
globalHeaders; global short cache; per-asset routes before/*. - MIME & routes — sitemap/robots/well-known/
.vcfentries; 404 routes for internal files (directory + exact-match); redirects/rewrites for moved pages. - CSP — externalise scripts and handlers, then styles; deploy Report-Only, clear violations, enforce; per-route CSP for pages that need CDN/Supabase.
- Front-end edits — inspect each page's markup, place mobile nav in
<header>, replace stale media rules, null-guard shared scripts, root-absolute assets on moved pages, bump?v=. - Deploy and verify live — fetch the live URL for headers, 404s on hidden files, redirects, CSP console; hard refresh for cached JS/CSS.
Output format
staticwebapp.config.json— complete file (routes in order,globalHeaders,mimeTypes,responseOverrides)- CSP plan — current → target policy, what must be externalised, Report-Only → enforce steps
- Page change list — per page: markup/script/CSS edits, moved-page redirects and asset path fixes
- Live verification — headers on non-HTML assets, hidden files 404, redirects, console clean, cache-busting confirmed
Quality checklist
- Security headers in
globalHeaders, present on CSS/JS/image responses - Per-asset
Cache-Controlroutes precede the/*catch-all - Internal files return 404 live via directory + exact-match routes (no
/*.mdwildcards) -
sitemap.xml,robots.txt, extensionless well-known files, and.vcfserved with correctContent-Type - No inline
<script>blocks oronclick-style attributes on a CSP site; new CSP deployed Report-Only first, console clean - Shared-script DOM lookups null-guarded
- Relocated pages: 301/302 redirects from old URLs, rewrite for the folder URL, root-absolute asset paths
- Mobile nav inside
<header>; stale mobile media rules replaced -
?v=bumped on changed JS/CSS and behaviour verified against the live URL
Avoid
- Adding security headers to the
/*route — they only reach HTML responses - Hiding files with
/*.md-style wildcards — wildcards match only at the end of a route - Combining
statusCodewithrewriteon a route — config validation rejects it and the deploy fails; that combination lives inresponseOverridesonly - Adding CSP directives to an
<iframe srcDoc>— it inherits the parent page's policy; fix the parent'simg-src - Assuming a
redirectroute forwards the query string — it drops it - Adding inline scripts or handler attributes to a
script-src 'self'site — they pass locally and fail live - Dropping
'unsafe-inline'fromstyle-srcbefore every inline style is extracted - Enforcing a new CSP without a Report-Only pass
- Leaving shared-script DOM lookups unguarded — one missing element halts the page's JS
- Keeping relative asset paths when moving a page into a subfolder — they 404
- Assuming nav markup is identical across pages, or placing a mobile menu after
</header> - Judging a JS/CSS deploy from a normally-cached tab — verify the live URL or hard refresh
Example usage
"Our marketing site is live on Azure Static Web Apps. I need to move
/reset-password.htmlinto/account/, add a hamburger menu, tighten the CSP toscript-src 'self', and make sure thedocs/folder andOPERATIONS.mdin the repo aren't publicly fetchable — without breaking the app's deep links."
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.