Who Owns the CSP
Hydrogen 2026.4 shipscreateContentSecurityPolicy, NonceProvider, and useNonce. Hydrogen 2026.10 removes them, so the theme builds the header and nonce itself.
Pilot already does this (Weaverse/pilot#204). It works on Hydrogen 2026.4 and 2026.10, so you can adopt it before you upgrade. Two files are involved:
createContentSecurityPolicy keeps the header semantics of Hydrogen 2026.4’s helper:
- It starts from Hydrogen’s defaults:
base-uri 'self',default-src 'self' https://cdn.shopify.com https://shopify.com,frame-ancestors 'none',style-src 'self' 'unsafe-inline' https://cdn.shopify.com, and aconnect-srcwith Shopify’s CDN, Monorail, and your checkout and store domains. - Each directive you pass extends the matching default. A
'none'default (such asframe-ancestors) is replaced instead. - The nonce goes on
script-srcwhen you set it, otherwise ondefault-src. - It returns the header string. You choose the header name.
Wire It Into entry.server.tsx
Generate one nonce per request, build the header, and pass the same nonce to React’s renderer, ServerRouter, and NonceProvider:
app/entry.server.tsx
HydrogenRouterContextProvider: the context is React Router’s RouterContextProvider, which server.ts fills with your app context. context.env is typed once env.d.ts declares interface RouterContextProvider extends AppContext {} (see Step 2 of the integration guide). On classic Hydrogen 2026.4, type the parameter as HydrogenRouterContextProvider from @shopify/hydrogen instead.
Unlike Hydrogen’s
NonceProvider, this one is a plain React context provider, so it needs value={nonce}.Wire It Into entry.client.tsx
On the client, read the nonce back from the server-rendered scripts so useNonce() returns the same value during hydration:
app/entry.client.tsx
Use the Nonce in Components
ImportuseNonce from ~/utils/csp, not from @shopify/hydrogen, and pass it to inline scripts, <ScrollRestoration>, and <Scripts>:
app/root.tsx
Sources Weaverse Studio Requires
getWeaverseCsp detects design mode from the weaverseHost query parameter that Studio adds to the preview URL. The Weaverse hosts are *.weaverse.io, *.shopify.com, *.myshopify.com, plus the weaverseHost value (or WEAVERSE_HOST from your env).
Common Third-Party Sources
Pilot’s
getWeaverseCsp covers all of these:
app/weaverse/csp.ts
scriptSrc, not defaultSrc.
Rolling Out Enforcement
Pilot sendsContent-Security-Policy-Report-Only: the browser logs violations but blocks nothing. Switch to the enforcing header only after a clean pass.
1
Run report-only
Keep
Content-Security-Policy-Report-Only and deploy to a preview environment.2
Test every surface
With the browser console open, check:
- Home page
- A product page, including one with a 3D model
- A page with a YouTube section, and play the video
- Cart drawer, variant change, and the mobile menu (these confirm hydration works)
- GTM loads (
window.dataLayerexists and the GTM request succeeds) - Studio design mode connects, and edits update the preview
- Revision preview and section preview in Studio (both load
preview.js)
3
Fix each violation
Look for messages like
Refused to load the script '<url>' because it violates the following Content Security Policy directive: "script-src ..." (prefixed with [Report Only] in report-only mode). Add the blocked origin to the directive the message names, in app/weaverse/csp.ts, and test again.4
Enforce
Change the header name in Repeat the checklist on the enforced build.
entry.server.tsx:Harmless Console Noise
Not every red line is a CSP problem in your policy:- CORS errors for
googleads.g.doubleclick.netcome from inside the YouTube iframe. That frame runs under YouTube’s own policy, not yours, and playback still works. - Blocked source-map (
.map) fetches underconnect-srconly happen while DevTools is open. Shoppers never trigger them, so you don’t need to allow them.
Troubleshooting
- Studio shows a blank preview or never connects: check that
frame-ancestors *is present in design mode and that the Weaverse hosts are inscript-srcandconnect-src. See Preview Errors. - Studio connects but conditional settings misbehave:
'unsafe-eval'is missing fromscript-srcin design mode. - The page renders but nothing is interactive: the nonce on React Router’s scripts doesn’t match the header. Make sure
entry.server.tsxpasses the same nonce tocreateContentSecurityPolicy,renderToReadableStream,ServerRouter, andNonceProvider.