Embedly looks simple right up until your CSP starts blocking half the page.
I’ve seen this pattern a lot: somebody adds an Embedly card, video, or rich preview, ships a strict Content Security Policy, and suddenly gets blank embeds, broken thumbnails, console errors, or weird “refused to load” messages that only show up in production. Then the quick fix is to slap https: into every directive and call it a day. That works, but it also guts the whole point of CSP.
If you’re using Embedly, the trick is knowing which directives actually matter and avoiding the classic overcorrections.
The mistake: only allowing script-src
A lot of developers start here:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.embedly.com
Looks reasonable. It’s also usually not enough.
Embedly content often needs more than script execution. Depending on the embed type, you may need:
script-srcfor Embedly’s JavaScriptframe-srcfor iframe-based embedsimg-srcfor thumbnails and previewsconnect-srcfor API or metadata fetches- sometimes
style-srcif inline styles or remote styles are involved
If you only fix script-src, the script loads but the embed itself still fails.
Better
Start by identifying what Embedly is actually doing in your app. A more realistic baseline looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.embedly.com;
frame-src 'self' https://cdn.embedly.com https://*.embedly.com;
img-src 'self' data: https:;
connect-src 'self' https://*.embedly.com;
I’m deliberately keeping img-src broad there because Embedly often pulls assets from third-party origins depending on the source content. If you know the exact image hosts, lock it down further.
The mistake: assuming default-src covers everything
Technically, default-src acts as a fallback for directives you don’t define. In practice, relying on that for embeds is how you end up debugging browser console logs at 11 PM.
For example:
Content-Security-Policy: default-src 'self' https://cdn.embedly.com
That does not mean your whole Embedly integration is handled cleanly. It just means every unspecified fetch directive falls back to that source list, which is rarely what you want.
frame-src is the big one people forget. If Embedly renders content in an iframe and you haven’t explicitly allowed it, the browser blocks it.
Better
Be explicit:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.embedly.com;
frame-src 'self' https://cdn.embedly.com https://*.embedly.com;
img-src 'self' data: https:;
connect-src 'self' https://*.embedly.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
That shape is a lot closer to a real production policy.
For reference, a real CSP from HeaderTest looks like this:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YjQ1YjkzNjktMDEwNC00OTI4LWIzNGYtMDgyYjM0ZmY2YzU3' '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 how real policies tend to look: specific directives, specific hosts, and very little guessing.
The mistake: allowing all frames everywhere
I still see this one too often:
frame-src *
Yes, your embeds work. So does every other iframe you didn’t mean to trust.
If your app accepts user-generated content, this is especially bad. You’ve basically told the browser that any origin can frame content into your page.
Better
Scope frames to Embedly and whatever else you actually use:
frame-src 'self' https://cdn.embedly.com https://*.embedly.com
If Embedly is only used on one route, even better: send a route-specific CSP instead of widening the global one.
The mistake: using unsafe-inline because Embedly “needs it”
Usually, it doesn’t.
When people hit a CSP problem with third-party widgets, they often jump straight to this:
script-src 'self' 'unsafe-inline' https://cdn.embedly.com
That’s a nasty downgrade. Inline script allowance is one of the easiest ways to weaken CSP enough that XSS becomes much easier to exploit.
If your own application uses inline bootstrapping code near the Embedly integration, fix that with nonces or hashes rather than opening the door globally.
Better
Use a nonce-based policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123' https://cdn.embedly.com;
frame-src 'self' https://cdn.embedly.com https://*.embedly.com;
img-src 'self' data: https:;
connect-src 'self' https://*.embedly.com;
object-src 'none';
base-uri 'self';
And in your HTML:
<script nonce="rAnd0m123">
window.embedlyConfig = { key: "YOUR_KEY" };
</script>
<script nonce="rAnd0m123" src="/app.js"></script>
If you’re already using strict-dynamic, test carefully. It changes how host allowlists are interpreted once a nonce- or hash-trusted script loads other scripts.
The mistake: forgetting thumbnails and provider assets
Embedly often aggregates content from somewhere else. That means the final rendered output may include images from the original provider, not just from Embedly.
A too-strict image policy causes the classic symptom: the card renders, but the image is missing.
Broken version:
img-src 'self'
Better
At minimum:
img-src 'self' data: https:
That’s broad, sure, but for embeds it’s often practical. If you can inspect the exact domains in your specific use case, narrow it down later.
Same idea applies to media-heavy embeds. If audio or video providers are involved, you may also need media-src.
The mistake: not checking connect-src
This one sneaks past people because the UI may partially work. The script loads. Maybe the frame loads. But metadata requests, API calls, or background requests fail silently.
Typical console error:
Refused to connect to 'https://api.embedly.com/...' because it violates the following Content Security Policy directive: "connect-src 'self'".
Better
Add the actual API hosts used by your integration:
connect-src 'self' https://api.embedly.com https://*.embedly.com
If you’re not sure which requests are being blocked, open DevTools and filter the Network tab by blocked requests. CSP debugging gets much easier once you stop guessing.
The mistake: debugging in production only
CSP failures are annoying because they often depend on content type. One Embedly URL works, another breaks. A YouTube preview behaves differently from a random blog card. A staging page with one test embed passes, but real content in production pulls assets from ten more domains.
Use Content-Security-Policy-Report-Only before enforcement:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.embedly.com;
frame-src 'self' https://cdn.embedly.com https://*.embedly.com;
img-src 'self' data: https:;
connect-src 'self' https://api.embedly.com https://*.embedly.com;
report-to default-endpoint;
That lets you see what would break without actually breaking it.
If you want ready-made policy patterns to compare against, csp-examples.com is useful for sanity checks.
The mistake: copying a policy without matching your embed mode
Embedly integrations aren’t all the same. Some are pure script transforms. Some rely on iframes. Some pull remote metadata server-side and only render client-side HTML. Some sit inside React or Next.js apps with their own CSP constraints.
So the “correct” CSP depends on your actual implementation.
Here’s a decent starting point for a typical client-side Embedly setup:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.embedly.com;
frame-src 'self' https://cdn.embedly.com https://*.embedly.com;
img-src 'self' data: https:;
connect-src 'self' https://api.embedly.com https://*.embedly.com;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
I don’t love 'unsafe-inline' in style-src, but it’s often tolerated as a transitional compromise if a widget injects inline styles. If you can avoid it, do.
A practical workflow that saves time
When I’m locking down CSP for embeds, I do this:
- Start with a strict base policy.
- Add only the Embedly directives I know I need.
- Run in report-only mode.
- Test multiple real embed types, not just one sample URL.
- Promote the policy to enforcement.
- Revisit broad directives like
img-src https:later and tighten them if possible.
That approach is slower than copying a giant allowlist from a forum post, but you end up with a policy you actually understand.
And that’s the real fix for most CSP mistakes with Embedly: stop treating CSP like a list of random domains and start treating it like a description of how your page is supposed to behave. Once you do that, the broken embeds get a lot less mysterious.