Allow embedding on WordPress
WordPress core blocks framing only on wp-admin and the login page, not on posts and pages. If a post is blocked, a security plugin, your host, or the web server adds the header. Turn it off at the source, then send CSP frame-ancestors.
5 min read · Updated
What WordPress core does
Core calls send_frame_options_header() on admin_init and login_init. On those screens it sends:
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self';
The current code sends both lines. Keep this on. It protects wp-admin and wp-login.php from clickjacking. Do not try to frame the admin or login screens on another site.
Front-end pages, such as posts, pages, and archives, get no framing header from core.
Find the real source
Read the headers of a blocked page:
curl -sI https://your-site.com/sample-page/ | grep -iE "x-frame-options|content-security-policy"
Then check these places, in this order:
- Security plugins. Many have a "security headers" or "clickjacking" setting that adds X-Frame-Options. Turn off that one setting, or switch it to allow framing.
- Your host. Managed WordPress hosts often add security headers at the server or CDN. Look for a setting in the host dashboard, or ask support.
- The web server. Search
.htaccessand the server config. See Apache or Nginx. - A CDN. Cloudflare can add the header too. See Allow embedding on Cloudflare.
A quick test: turn off plugins one at a time on a staging copy. Read the headers after each change.
Set frame-ancestors in PHP
If the header comes from WordPress or a plugin, you can change it in PHP. Create a must-use plugin at wp-content/mu-plugins/frame-headers.php:
<?php
/**
* Plugin Name: Frame headers
*/
add_action( 'send_headers', function () {
header_remove( 'X-Frame-Options' );
header( "Content-Security-Policy: frame-ancestors 'self' https://partner.example" );
}, 99 );
send_headersruns in the front-end WordPress setup only. The admin and login scripts do not run it, so those screens keep the core protection.- Priority
99makes it run after most plugins. header()replaces an earlier CSP that PHP set in the same request. If a plugin sent a full CSP, copy its other directives into this line.header_remove()only removes headers that PHP set. It cannot remove a header that Apache, Nginx, or a CDN adds later.
To limit framing to some pages, add a check inside the function, for example if ( ! is_page( 'booking' ) ) { return; }.
The wp_headers filter also works. It receives the headers that WordPress builds in its own list, so a plugin that calls header() directly skips that filter. With send_headers and header_remove(), you also remove headers that plugins set earlier with header(). If a plugin adds the header at a later hook, turn off that plugin setting.
Set it in .htaccess
On Apache hosts, you can set the headers in the .htaccess file in the WordPress root. Put the lines above the # BEGIN WordPress block. WordPress can rewrite the lines inside that block.
<IfModule mod_headers.c>
Header unset X-Frame-Options
Header always unset X-Frame-Options
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example"
</IfModule>
This overrides headers from PHP too, including those for wp-admin. To keep the admin protected, use the PHP method, or wrap the lines in <If "%{REQUEST_URI} !~ m#^/wp-(admin|login)#">.
Common mistakes
- Framing the login page. Core blocks it on purpose. Log users in with a popup, not a frame. See Login doesn't stick in an iframe.
- Two CSP headers. A plugin sends one and
.htaccessadds another. The browser enforces both. - A page cache. Caching plugins and hosts can keep old headers. Purge the cache after each change.
ALLOW-FROM. Some old plugins still offer it. Browsers ignore it.
Check a real post
Paste a post URL into the iframe tester. Test a page and a post. They can get different headers from different rules.
To check many pages at once, use the bulk check. It shows X-Frame-Options and frame-ancestors for up to 100 URLs.