Routes that need a no-inline Content Security Policy can opt into FaceTheory’s strict CSP path: no inline scripts, no inline styles, no raw head HTML, with hydration data moved to a same-origin sidecar instead of an inline __FACETHEORY_DATA__ block.
Opting in
Set FaceRenderResult.csp to disable the inline channels:
return {
html: '<h1>Hello</h1>',
csp: {
inlineScripts: false,
inlineStyles: false,
rawHead: false,
},
hydration: externalHydrationForEntry(
manifest,
'src/entry-client.ts',
data,
{ dataUrl: '/hydration/hello.json' },
),
};
When csp.inlineScripts === false, FaceTheory refuses to emit inline <script> bodies. Hydration must be external already or be externalized through a configured framework-owned path (SSR hydration sidecars, SSG build sidecars, or ISR sidecars); otherwise inline or Vite hydration fails closed.
Building the CSP header
import { buildStrictCspHeader, createCspNonce } from '@theory-cloud/facetheory';
const nonce = createCspNonce();
const cspHeader = buildStrictCspHeader({ cspNonce: nonce });
buildStrictCspHeader emits the canonical strict directive set:
default-src 'self'
base-uri 'self'
object-src 'none'
frame-ancestors 'none'
script-src 'self'
style-src 'self'
img-src 'self' data:
font-src 'self'
connect-src 'self'
form-action 'self'
Nonces are unique per response and consistent within a response so that <script nonce> attributes match the Content-Security-Policy header.
Hosts can extend the canonical directives without abandoning the strict helper:
const cspHeader = buildStrictCspHeader({
directives: {
'connect-src': ['https://api.example.com', 'wss://events.example.com'],
'img-src': 'https://img.example.com',
'report-to': 'facetheory-csp',
},
});
Extension values are appended to existing directives when the directive is part
of the baseline (connect-src, img-src, and so on). New directives such as
report-to are appended after the baseline in deterministic name order. Values
are individual CSP tokens; pass multiple values as an array instead of a
space-separated string.
The strict builder remains fail-closed. It rejects directive injection shapes
(invalid names, semicolons, or whitespace inside a single value), and it refuses
'unsafe-inline' and 'unsafe-eval' with actionable errors. Use external assets
or FaceTheory-owned request nonces; do not weaken the strict CSP baseline.
JSON-LD structured data uses the head helper path:
import { jsonLd } from '@theory-cloud/facetheory';
return {
csp: { inlineScripts: false, inlineStyles: false, rawHead: false },
headTags: [
jsonLd({
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Strict page',
}),
],
html: '<main>Strict page</main>',
};
createFaceApp() carries the request nonce into head emission. The JSON-LD tag
must remain in <head>, use type="application/ld+json", and carry the
matching nonce. This allowance does not apply to inline hydration data or generic
inline scripts.
Document validation
For defense in depth, FaceTheory can validate the rendered document against the policy before emitting it:
import {
validateStrictCspDocument,
requiresStrictCspDocumentValidation,
} from '@theory-cloud/facetheory';
if (requiresStrictCspDocumentValidation(policy)) {
validateStrictCspDocument(html, { policy });
}
The validator throws on inline <script> bodies, inline style attributes, and raw head HTML that the policy forbids.
Sidecar hydration
Strict CSP requires hydration data, when present, to be external. Use one of:
- Framework-owned same-origin sidecars — configure
createFaceApp({ ssrHydrationSidecars })and return normalviteHydrationForEntry()data from the SSR Face. FaceTheory writes the exact render-time payload once before emitting a same-origin/_facetheory/ssr-data/...link. - Caller-managed external sidecars — use
externalHydrationForEntry()when the host owns the same-origin JSON URL.
See SSR hydration sidecars for the full path.
Examples in the repo
ts/examples/vite-strict-csp-svelte/— strict CSP delivery with Svelte + Vite