Ghost embeds are easy to drop into a page and easy to forget from a CSP perspective. That’s where people get burned: the embed works in development, then production CSP blocks it, or worse, someone loosens the policy with script-src * and calls it done.
Don’t do that.
If you’re embedding Ghost content, membership widgets, or Portal-related UI on your site, you need to explicitly allow the right sources and keep the policy tight everywhere else. The good news is Ghost’s embed surface is pretty manageable if you approach it methodically.
What “Ghost embeds” usually mean
For most teams, this lands in one of these buckets:
- Embedding a Ghost post or card via script
- Loading Ghost Portal for signup/login UI
- Calling Ghost APIs from the frontend
- Rendering Ghost-hosted images, fonts, or iframes
Each one touches different CSP directives.
A lot of developers start with script-src, but Ghost integrations usually need at least:
script-srcconnect-srcframe-srcimg-srcstyle-src- sometimes
font-src
If you already run analytics, consent tools, and tag managers, your CSP is probably already crowded. Here’s a real-world header from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MGQ1ZmFkMmUtZDYwZC00OTBkLWE2MGItOTE1ZjlhNmJmOGI3' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s a decent baseline. To support Ghost embeds, you’d extend it surgically rather than blowing it open.
The Ghost embed you’re likely adding
A typical Ghost embed looks something like this:
<div id="ghost-embed"></div>
<script src="https://your-publication.example.com/public/cards.min.js"></script>
<script>
// Example initialization if your integration needs one
console.log('Ghost embed loaded');
</script>
Or for Portal/membership features, you may load a script hosted on your Ghost site and then trigger UI:
<script src="https://your-publication.example.com/public/portal.min.js"></script>
<button id="open-signup">Sign up</button>
<script>
document.getElementById('open-signup').addEventListener('click', function () {
if (window.GhostPortal) {
window.GhostPortal.open();
}
});
</script>
The exact file names can vary by Ghost setup and version, but the CSP pattern stays the same: trust your Ghost origin, and only your Ghost origin.
Start with a strict baseline
If I’m adding Ghost to an existing site, I usually begin with something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
frame-ancestors 'none';
Then I add Ghost’s domain where it’s actually needed.
Assume your Ghost publication lives at:
https://news.example.com
Now update the policy.
CSP directives Ghost embeds usually need
script-src
If you load Ghost JavaScript from your Ghost host:
script-src 'self' 'nonce-{RANDOM_NONCE}' https://news.example.com;
If your page also contains inline bootstrapping code, nonce it instead of using 'unsafe-inline'.
Example HTML:
<script nonce="{{ .CSPNonce }}" src="https://news.example.com/public/cards.min.js"></script>
<script nonce="{{ .CSPNonce }}">
console.log('Ghost cards loaded');
</script>
I strongly prefer nonces here. Once people add 'unsafe-inline', it tends to stick around forever.
If you want a refresher on how script-src, nonces, and strict-dynamic behave, the writeups on https://csp-guide.com are solid.
connect-src
Ghost scripts may fetch content or interact with membership/session endpoints. If anything on the page talks back to Ghost over fetch, XHR, or WebSocket-like channels, allow the Ghost origin:
connect-src 'self' https://news.example.com;
If you skip this, the script may load fine but fail later when it tries to retrieve data.
That’s one of the most annoying CSP failure modes: “the script is there, but nothing happens.”
frame-src
Portal or other embedded UI can use frames depending on your Ghost features and theme behavior. If there’s any iframe-backed component, allow your Ghost origin:
frame-src 'self' https://news.example.com;
Don’t confuse this with frame-ancestors. frame-src controls what your page can embed. frame-ancestors controls who can embed your page.
img-src
Ghost content often includes hosted images. If your embedded post cards or membership UI render images from the Ghost site, include it:
img-src 'self' data: https://news.example.com;
If your current policy already has img-src 'self' data: https:, Ghost images will work, but that’s broader than necessary. I’d rather keep it explicit when possible.
style-src
This one depends on how the Ghost assets behave. Some integrations inject styles or rely on inline styles. If the Ghost embed requires external CSS from your Ghost host:
style-src 'self' https://news.example.com;
If it injects inline styles, you may be forced into 'unsafe-inline' for styles:
style-src 'self' 'unsafe-inline' https://news.example.com;
I don’t love 'unsafe-inline', but for style-src it’s often the compromise teams make. I try to avoid it for scripts, not die on the hill for styles unless I fully control the rendering.
font-src
If Ghost-hosted UI ships fonts from the same origin:
font-src 'self' https://news.example.com;
Many setups won’t need this, but if text renders oddly or the console complains about blocked fonts, check here first.
A practical CSP for a site embedding Ghost
Let’s take the real headertest.com header and extend it with a Ghost host at https://news.example.com.
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://news.example.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://news.example.com;
img-src 'self' data: https: https://news.example.com;
font-src 'self' https://news.example.com;
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://news.example.com;
frame-src 'self' https://consentcdn.cookiebot.com https://news.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self' https://news.example.com;
object-src 'none';
A few notes:
- I added Ghost only where needed.
- I did not throw Ghost into
default-srcand hope for the best. - I extended
form-actiontoo, because signup/login/payment-related flows sometimes submit to the Ghost origin. If your embed never posts forms, you may not need it.
Example: Express middleware generating a nonce
If your site is server-rendered, generate a nonce per request and inject it into both the CSP header and the markup.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
const ghostOrigin = 'https://news.example.com';
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' ${ghostOrigin}`,
`style-src 'self' 'unsafe-inline' ${ghostOrigin}`,
`img-src 'self' data: ${ghostOrigin}`,
`font-src 'self' ${ghostOrigin}`,
`connect-src 'self' ${ghostOrigin}`,
`frame-src 'self' ${ghostOrigin}`,
`form-action 'self' ${ghostOrigin}`,
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'"
].join('; ');
res.setHeader('Content-Security-Policy', csp);
next();
});
app.get('/', (req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<title>Ghost Embed Demo</title>
</head>
<body>
<button id="open-signup">Open Ghost Portal</button>
<script nonce="${res.locals.cspNonce}" src="https://news.example.com/public/portal.min.js"></script>
<script nonce="${res.locals.cspNonce}">
document.getElementById('open-signup').addEventListener('click', function () {
if (window.GhostPortal) {
window.GhostPortal.open();
}
});
</script>
</body>
</html>
`);
});
app.listen(3000);
That’s the pattern I trust: explicit origin allowlists plus nonced inline code.
Debugging blocked Ghost embeds
When Ghost breaks under CSP, the browser console usually tells you exactly which directive failed. Read the full message. Don’t skim it.
Common failures:
Script blocked
You’ll see something like:
Refused to load the script 'https://news.example.com/public/portal.min.js'
because it violates the following Content Security Policy directive: "script-src ..."
Fix: add the Ghost origin to script-src.
API call blocked
Refused to connect to 'https://news.example.com/members/api/...'
because it violates the following Content Security Policy directive: "connect-src ..."
Fix: add the Ghost origin to connect-src.
Frame blocked
Refused to frame 'https://news.example.com/' because it violates
the following Content Security Policy directive: "frame-src ..."
Fix: add the Ghost origin to frame-src.
Form submission blocked
Refused to send form data to 'https://news.example.com/...'
because it violates the following Content Security Policy directive: "form-action ..."
Fix: add the Ghost origin to form-action.
Use report-only before enforcing
If you’re adding Ghost to an existing production app, start with Content-Security-Policy-Report-Only first. I’ve watched too many teams ship an enforced policy on Friday and spend the weekend debugging signup flows.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' https://news.example.com;
connect-src 'self' https://news.example.com;
frame-src 'self' https://news.example.com;
img-src 'self' data: https://news.example.com;
style-src 'self' 'unsafe-inline' https://news.example.com;
form-action 'self' https://news.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Watch violations, tighten, then enforce.
My rule of thumb for Ghost CSP
If Ghost is hosted at https://news.example.com, whitelist exactly that origin in the directives the embed needs. Don’t add wildcards unless you can prove Ghost actually uses them. Don’t dump it into every directive. Don’t “fix” inline script issues with 'unsafe-inline'.
A clean Ghost CSP usually looks boring, and boring is good.
If your embed still fails after this, inspect network requests and console violations together. CSP debugging gets much easier once you stop treating “Ghost embed” as one thing and instead break it into script load, data fetch, frame rendering, image load, and form submission. That’s where the real fix usually is.