Allow embedding on Netlify
Add a Content-Security-Policy header with frame-ancestors in a _headers file or in netlify.toml, and remove X-Frame-Options. These rules apply only to static files. Pages from functions, edge functions, or SSR must set the header in code.
4 min read · Updated
Option 1: the _headers file
Create a plain text file named _headers in your publish directory, for example dist/ or public/:
/*
Content-Security-Policy: frame-ancestors 'self' https://partner.example
The first line is the path. The indented lines below it are the headers. /* matches every path.
Some site generators skip files that start with _. If the header does not show up, check that _headers is in the build output.
Option 2: netlify.toml
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "frame-ancestors 'self' https://partner.example"
Use one of the two files for headers, not both. Then you have one place to look.
In both files, delete any X-Frame-Options line. Starter templates often include X-Frame-Options: DENY.
Allow framing on one path only
Give each section of the site its own rule, and keep the paths apart:
/
Content-Security-Policy: frame-ancestors 'self'
/app/*
Content-Security-Policy: frame-ancestors 'self'
/embed/*
Content-Security-Policy: frame-ancestors https://partner.example
Do not put a site-wide /* CSP rule next to an /embed/* rule. Both rules match /embed/ pages. Netlify does not document how the two values combine. A page with two CSP headers must pass both policies, so 'self' blocks the partner.
SSR pages and functions: set the header in code
Netlify applies custom headers only to files it serves from its own storage. Responses from Netlify Functions, edge functions, proxies, and server-rendered pages do not get them. This includes Next.js, Astro, Remix, and Nuxt pages that render on the server.
For those, set the header where the response is built. Or change it on the way out with an edge function:
// netlify/edge-functions/frame-headers.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
const response = await context.next();
const result = new Response(response.body, response);
result.headers.delete("X-Frame-Options");
result.headers.set("Content-Security-Policy", "frame-ancestors 'self' https://partner.example");
return result;
};
export const config: Config = { path: "/embed/*" };
headers.set replaces any CSP the page sent. If the page had other CSP directives, copy them into this value.
Check the result
Deploy, then read the headers:
curl -sI https://your-site.com/embed/ | grep -iE "x-frame-options|content-security-policy"
You want one CSP line with your frame-ancestors and no X-Frame-Options. Test a static page and a server-rendered page. They follow different rules.
Common mistakes
- Rules in
_headersfor an SSR page. They have no effect there. Use code or an edge function. _headersin the wrong folder. It must be in the publish directory after the build.ALLOW-FROM. Browsers ignore it, so every site can frame the page.- CSP in a
<meta>tag.frame-ancestorsdoes not work there.
Test the live site
Paste the deployed URL into the iframe tester. It tells you if the page can load in a frame and which header blocks it.
To check many pages at once, use the bulk check. It shows X-Frame-Options and frame-ancestors for up to 100 URLs.