Most portfolio and blog templates quietly work against your visitors. They pull fonts from Google's CDN (leaking every visitor's IP to a third party), ship analytics that set cookies before anyone consents, and sprinkle inline scripts around — which forces a loose Content-Security-Policy, if there's one at all.
I wanted the opposite: a site that's fast, private, and secure by default — green on securityheaders.com before I touch a single setting. Here's exactly how, with Astro, and how you can do the same.
Why Astro makes this easy
Astro ships zero JavaScript by default. Components render to static HTML at build time; you opt into client-side JS only where you actually need it. That's already half the battle: less JS means a smaller attack surface and a CSP that can stay strict.
1. A strict Content-Security-Policy
The single biggest win is a CSP that only allows resources from your own origin. The header that matters most:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
The critical part is script-src 'self' with no 'unsafe-inline'. That single omission neutralizes the entire class of injected-inline-script XSS — an attacker who manages to inject <script>…</script> still can't get it to run. The rule you have to respect in return: no inline scripts and no inline event handlers anywhere.
Astro makes that painless. <script> tags inside .astro components are processed at build time and emitted as external, hashed files served from your own origin, so they satisfy script-src 'self' automatically. You just avoid onclick="…"-style handlers and inline <script> blocks — use addEventListener in a real script file instead. Writing a theme toggle or view-transition logic? Put it in a .js/.ts file, not an inline blob.
A note on style-src: I keep 'unsafe-inline' there because Astro emits some scoped <style> inline, and styles can't execute code — the risk is minimal compared to scripts. If you want it fully strict, you can hash or externalize your styles, but the security payoff is small. Spend your strictness budget on script-src.
2. Self-hosted fonts (kill the Google Fonts request)
@import url('https://fonts.googleapis.com/…') is the most common privacy leak on the web: every visitor's browser hits Google, handing over their IP and a referer — plus you pay a DNS lookup and a connection to a third party on the critical path.
Self-hosting fixes privacy and performance in one move:
- Grab the
woff2files (from the font's repo, or a helper likegoogle-webfonts-helper). - Drop them in
public/fonts/. - Declare them locally:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
- Preload the ones above the fold:
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin />
Now font-src 'self' holds, there are zero third-party requests, and text paints sooner. Same change, two wins.
3. Security headers
CSP is one header; a few more close the remaining gaps. On Cloudflare Pages or Netlify you set these in a _headers file; elsewhere, in your server or edge config:
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(), browsing-topics=()
X-Frame-Options: DENY
What each buys you:
-
HSTS forces HTTPS and removes the downgrade window. Test it before adding
preload— that step is hard to undo. - X-Content-Type-Options: nosniff stops the browser from MIME-guessing a response into something executable.
- Referrer-Policy trims the referer you leak to other sites.
-
Permissions-Policy disables APIs you don't use and opts out of interest-based ad cohorts (
browsing-topics=()). -
X-Frame-Options /
frame-ancestors 'none'stop clickjacking.
4. Zero trackers by default
The default build ships no analytics at all — no Google Analytics, no cookies, no consent banner to bolt on. If you want numbers later, add a cookieless, privacy-respecting analytics and open exactly one connect-src entry for it. The point is that privacy is the default, not something you retrofit and apologize for with a cookie wall.
The result
Put together, this gets you:
- Green (A+) on securityheaders.com with no manual tuning.
- No third-party requests on a cold load — nothing to block, nothing to leak.
- A genuinely fast site, because "secure by default" and "few requests" are the same discipline.
None of it is exotic: a strict script-src, local fonts, a handful of headers, and the restraint to not ship trackers. Astro makes it painless because it starts from zero JS.
Don't want to wire all this yourself?
I got tired of re-doing it on every project — so I did it once, properly, and packaged it: Auric, an Astro portfolio + blog theme with the strict CSP, self-hosted fonts, security headers and zero-tracking baked in (gold-on-dark, a light mode too, Markdown/MDX blog + RSS). Everything you edit lives in one config file.
- Live demo: https://auric-theme.netlify.app
- Get it: https://aurictheme.gumroad.com/l/auric
But honestly — even if you roll your own, do the four things above. Your visitors' browsers will thank you, and your Lighthouse and securityheaders scores will too.
Top comments (0)