Skip to content

Security & CSP

Velocity ships with Astro’s hash-based Content Security Policy turned on through security.csp in astro.config.mjs. Every page gets a <meta http-equiv="content-security-policy"> tag.

DirectiveValue
script-src'self' plus a SHA-256 hash for each inline script. No 'unsafe-inline'.
style-src-elem'self' plus hashes
style-src-attr'unsafe-inline', so Shiki can color code blocks through style="" attributes
object-src'none'
base-uri'self'
form-action'self'

Inline style attributes can’t run script. <style> elements stay hash-locked. Astro logs a generic Shiki warning at build time, which you can ignore.

astro.config.mjs
security: {
checkOrigin: true,
csp: {
scriptDirective: { resources: ["'self'"] },
styleDirective: {
resources: [
{ resource: "'self'", kind: 'element' },
{ resource: "'unsafe-inline'", kind: 'attribute' },
],
},
directives: ["object-src 'none'", "base-uri 'self'", "form-action 'self'"],
},
},

Never add 'unsafe-inline' to script-src.

Keep inline scripts static. Astro can only hash a script whose content doesn’t change. define:vars and interpolated values won’t work. Pass data through a JSON element instead:

---
const config = JSON.stringify({ id: 'abc' });
---
<script type="application/json" id="my-config" set:html={config} />
<script>
const config = JSON.parse(document.getElementById('my-config').textContent);
</script>

Declare hosts in the component that loads them, so other pages keep the strict policy:

---
Astro.csp?.insertScriptResource('https://cdn.example.com');
---
<script is:inline src="https://cdn.example.com/widget.js"></script>

Analytics.astro does this, adding the Google hosts only when GA or GTM is configured. The search view adds 'wasm-unsafe-eval' for Pagefind’s WebAssembly engine.

For a host every page needs, add it to security.csp.scriptDirective.resources instead.

CSP isn’t applied in astro dev. Test it with a production build:

Terminal window
pnpm build && pnpm preview

pnpm test:e2e fails on any CSP violation.

A <meta> CSP ignores frame-ancestors and report-uri. Velocity sends X-Frame-Options: DENY from vercel.json and netlify.toml instead, along with other security headers.

Astro’s CSP doesn’t support <ClientRouter />. If you need transition:persist, transition:name or navigate(), set security.csp: false before adding it. Velocity’s components re-initialize on astro:after-swap, so they keep working.

Astro hashes the scripts it bundles, but not is:inline ones. A plain <script> is bundled and needs nothing extra. For a script that must run inline, such as one that runs before the page renders, put it in its own file and render it with InlineScript, which adds its hash to the page’s CSP:

---
import InlineScript from '@/components/seo/InlineScript.astro';
import code from './my-script.js?raw';
---
<InlineScript code={code} />

Inline event handlers such as onclick="..." are blocked. Use addEventListener in a script, with data- attributes to pass values.