Content-Security-Policy
The Product Genius hosts to allow on script-src and connect-src, and how to thread a nonce through.
<ProductGeniusFeed> injects the Product Genius script and the script opens connections to
the Product Genius backend, including a websocket. If your storefront sets a
Content-Security-Policy, it has to allow those hosts or nothing loads.
The hosts
script-src 'self' https://app.productgenius.io https://pg.gamalon.com;
connect-src 'self' https://app.productgenius.io wss://app.productgenius.io https://pg.gamalon.com;Merge these into your existing directives — the 'self' above is a reminder of what a
typical policy already has, not a replacement for the rest of your allowlist. Note that
connect-src needs both the https: and the wss: form of app.productgenius.io.
These two directives are what the component and the bundle need in order to load and
connect. The feed's own markup, styles, and images are produced by the bundle after it
loads, so if your policy is also strict about img-src, style-src, or font-src, verify
a rendered feed on staging and add whatever your console reports.
Nonce-based policies
If your CSP is purely host-allowlisted, you're done — omit the nonce prop.
If your script-src is nonce-based (commonly with 'strict-dynamic'), an un-nonced script
is blocked and the feed script never loads. Thread your per-request nonce through the prop:
<ProductGeniusFeed shop="store.myshopify.com" nonce={nonce} /* … */ />SSR frameworks generate a nonce per request — Hydrogen's createContentSecurityPolicy()
and useNonce(), Next.js proxy/middleware, React Router and TanStack Start entry servers. A
purely client-rendered SPA usually has no nonce at all, because its CSP comes from static
hosting headers or a <meta http-equiv> tag. The
framework guides cover where each one's nonce comes from.
The search bar takes its own nonce
<ProductGeniusSearchBar> renders an
inline <style> element, so it needs a nonce whenever your style-src doesn't allow
inline styles without one. Browsers ignore 'unsafe-inline' when the directive also carries
a nonce or a hash. It's usually the same per-request nonce value you pass to the feed, just
satisfying a different directive.
Hydrogen
Hydrogen builds the policy for you in app/entry.server.tsx; add the hosts to the existing
call:
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
scriptSrc: [
"'self'",
'https://cdn.shopify.com',
'https://app.productgenius.io',
'https://pg.gamalon.com',
],
connectSrc: [
"'self'",
'https://app.productgenius.io',
'wss://app.productgenius.io',
'https://pg.gamalon.com',
],
});Hydrogen's default policy is nonce-based, so also pass useNonce() to the feed's nonce
prop.
When CSP is the problem
A blocked script is the most common "the feed isn't showing" cause on the storefront side, and it is easy to confirm:
- No
tag.jsrequest at all in the Network tab — either the component isn't rendered on this route, orscript-srcblocked it. The console names the directive. tag.jsreturns 200 butwindow.GAMALONis undefined — the script loaded but was blocked from executing, which on a nonce-based policy usually means thenonceprop wasn't passed.- Feed requests failing with a console error naming
connect-src— the backend host or thewss:entry is missing.
The full checklist is in the integration guide.
How is this guide?