Mixed content warnings in WordPress: what they mean and how to fix them

You enabled SSL and now your WordPress site shows a broken padlock or 'Not Secure' warning. That is mixed content, not a broken certificate. This article explains the difference, how to find the offending resources, and the four real fixes in order of thoroughness.

You installed an SSL certificate, updated Settings > General to https://, and expected a green padlock. Instead the browser shows a broken or open padlock, or a "Not Secure" label next to the URL, even though the certificate itself is valid. That is mixed content: the page is served over HTTPS, but at least one resource inside the page is still loading over plain HTTP.

What mixed content actually means

Mixed content is an HTTPS page that pulls in a subresource over HTTP. The certificate on the main document is fine. The browser is telling you that part of what you just loaded arrived without encryption, so the whole page can no longer be trusted as private.

Browsers split mixed content into two buckets:

  • Upgradable content: <img>, <audio>, <video>, and CSS background images. Modern browsers silently rewrite these to HTTPS and load the upgraded version if it exists. The page still shows as "Not Secure" because the source HTML contains the HTTP URL, even if the fetch itself succeeded.
  • Blockable content: <script>, <link> stylesheets, <iframe>, fetch() and XMLHttpRequest, and @font-face. Browsers refuse to load these outright. An HTTP script on an HTTPS page does not run.

The Chrome rollout happened in stages, not all at once. Chrome 80 in February 2020 started autoupgrading and blocking mixed audio and video. Image autoupgrade was originally scheduled for Chrome 81 but was delayed until Chrome 84 in August 2020. So by late 2020, mixed images were also blocked if the HTTPS version did not exist. Firefox and Safari landed similar behavior around the same window.

Three things this is not:

  • It is not a broken certificate. A broken certificate produces a full red interstitial ("Your connection is not private"), not a working page with a crossed-out padlock. If you see the page load and just the padlock is wrong, the certificate is fine (see SSL certificates in WordPress for the identity-vs-encryption framing). Source: WP Engine's mixed content guide.
  • It is not something an SSL plugin fully fixes. Plugins like Really Simple SSL buffer PHP output and rewrite HTTP URLs in the rendered HTML at request time. They cannot change URLs hardcoded in theme PHP files, and they do not touch the database. They also cannot intercept resources that a plugin injects via JavaScript at runtime.
  • It is not something Cloudflare magically handles at the edge. Cloudflare's Automatic HTTPS Rewrites rewrite some HTTP subresources on the fly, but Cloudflare's own docs state the rewrite misses anything added dynamically via JavaScript and anything on external domains that do not support HTTPS. On Flexible SSL, the problem is worse: the edge-to-origin hop is still HTTP, so WordPress keeps generating HTTP URLs.

Mixed content is a content problem, not a transport problem. The HTTP URLs are stored in your WordPress database, baked into theme and plugin files, or injected by JavaScript at runtime. Until those sources are corrected, the padlock stays broken.

How to find the offending resources with DevTools

Before you fix anything, identify what is actually failing. Do not start rewriting URLs blindly.

  1. Open the affected page in Chrome or Firefox and press F12 to open DevTools.
  2. Switch to the Console tab.
  3. Reload the page with Ctrl+Shift+R (or Cmd+Shift+R on macOS) to force a clean fetch.
  4. Read the console. Mixed content errors are unmistakable:
Mixed Content: The page at 'https://yoursite.nl/' was loaded over HTTPS,
but requested an insecure stylesheet 'http://yoursite.nl/wp-content/themes/old/style.css'.
This request has been blocked; the content must be served over HTTPS.

The word "blocked" tells you it is a script, stylesheet, iframe, or font. The word "loaded but will be upgraded" tells you it is an image or media element.

  1. For a complete list, switch to the Network tab, reload again, and in the filter field type scheme:http. Any row that appears is a subresource the page still references over HTTP. Right-click the row and choose Copy > Copy URL to capture it.
  2. Make a short list. In practice you will find one or two distinct hosts (often just http://yoursite.nl itself) repeated across many resources, not a sprawl of different offenders.

Expected output. For a healthy HTTPS page the console has no mixed content lines and the network filter scheme:http returns zero rows. That is your verification target.

If your browser console is empty but the padlock still looks wrong, check for JavaScript-injected resources. Open the Network tab, clear the filter, and look for requests that appear after the initial load (sort by initiator or by time). Third-party scripts that build image tags or iframes from an HTTP string will only show up at runtime.

Fix 1: wp-config.php scheme alignment (10 seconds, partial)

Before rewriting anything, make sure WordPress itself knows it is running on HTTPS. The WP_HOME and WP_SITEURL constants in wp-config.php are not a permanent fix, but they are the fastest way to test whether the core URLs are the problem.

Open wp-config.php in your hosting panel's file manager (or download it via SFTP) and add these lines above the /* That's all, stop editing! */ marker:

define( 'WP_HOME',    'https://yoursite.nl' );
define( 'WP_SITEURL', 'https://yoursite.nl' );

What these actually do. The official wp-config documentation is explicit: WP_SITEURL "will not change the database stored value" and "the URL will revert to the old database value if this line is ever removed from wp-config." These constants override the home and siteurl options in wp_options at runtime; they do not write to the database. Remove the lines and the HTTP URLs come back.

That is why this is a half-fix. You still need to rewrite the stored values (Fix 2 or Fix 3), but setting the constants first tells you whether HTTPS is even viable for this site, and it cleans up the core-generated URLs (menus, wp-login.php redirects, canonical links) immediately.

Verification. Reload the front end and check the page source. The <link rel="canonical"> tag, the admin URL in the wp-admin bar, and <base> tags should now all start with https://. DevTools console should show fewer mixed content lines. If it shows more, you have misidentified the domain or there is a reverse proxy issue (see Fix 5).

Fix 2: WP-CLI search-replace (the real fix)

No SSH access? Skip to Fix 3: Plugin alternative, which does the same operation from your WordPress dashboard.

This is the canonical fix. You tell WordPress to replace every literal http://yoursite.nl in the database with https://yoursite.nl, including serialized data. WP-CLI's search-replace command explicitly states: "Search/replace intelligently handles PHP serialized data, and does not change primary key values."

Prerequisites. SSH access to the server with WP-CLI installed. If you do not have SSH but your host offers WP-CLI through a web terminal (common on managed WordPress hosts), that works too. Back up the database first using your hosting panel's backup tool or phpMyAdmin export. This operation rewrites rows across every table; a mistake is expensive.

Run a dry run first so you can see exactly what would change:

wp search-replace 'http://yoursite.nl' 'https://yoursite.nl' \
  --dry-run \
  --skip-columns=guid \
  --all-tables

Expected output.

+------------------+-----------------------+--------------+------+
| Table            | Column                | Replacements | Type |
+------------------+-----------------------+--------------+------+
| wp_options       | option_value          | 2            | PHP  |
| wp_posts         | post_content          | 147          | SQL  |
| wp_postmeta      | meta_value            | 63           | PHP  |
| wp_comments      | comment_content       | 0            | SQL  |
+------------------+-----------------------+--------------+------+
Success: 212 replacements to be made.

The numbers vary, but wp_posts.post_content and wp_postmeta.meta_value are almost always the two biggest rows. If you see zero replacements across the board, your stored URLs already use https:// and the problem is elsewhere (theme files, hardcoded plugin assets, or external CDNs).

Then run it for real by dropping the --dry-run flag:

wp search-replace 'http://yoursite.nl' 'https://yoursite.nl' \
  --skip-columns=guid \
  --all-tables

Why --skip-columns=guid. GUIDs are permanent unique identifiers for posts. They look like URLs, but WordPress never uses them for routing. Changing them can confuse feed readers and analytics that deduplicate by GUID. Leave them alone.

Why --all-tables. Without this flag, WP-CLI only touches tables with the WordPress prefix. Multisite installs and plugins that create their own tables (WooCommerce, BuddyPress, Yoast) live outside that default scope. --all-tables catches them.

Verification. Reload the front end, open DevTools console, and reload with Ctrl+Shift+R. The mixed content warnings for yoursite.nl resources should be gone. What remains, if anything, is either hardcoded in theme files or referencing an external domain. Move on to Fix 3.

Fix 3: Plugin alternative (when you have no SSH access)

If WP-CLI is not available, use the Better Search Replace plugin to do the same operation from the dashboard. It is the same search-replace logic with the same serialization-safe handling, just wrapped in a UI.

  1. Install and activate Better Search Replace.
  2. Go to Tools > Better Search Replace.
  3. In Search for enter http://yoursite.nl. In Replace with enter https://yoursite.nl. No trailing slash.
  4. Select every table in the list (use Ctrl+A or click each).
  5. Leave "Replace GUIDs" unchecked. Same reason as above.
  6. Tick "Run as dry run" and click Run Search/Replace.
  7. Read the report. Confirm it matches your expectations (hundreds of changes in posts and postmeta, zero or near-zero in comments).
  8. Uncheck "Run as dry run" and run it again.

Verification. Same as Fix 2. Reload the front end and check DevTools.

Really Simple SSL is a legitimate plugin, but it does not replace Fix 2 or Fix 3. It buffers PHP output and rewrites URLs in rendered HTML at request time, which means every request pays a runtime cost and the database still contains HTTP URLs. I recommend Better Search Replace as a one-time operation over Really Simple SSL as a permanent band-aid.

Fix 4: Hardcoded assets in theme and plugin files

After Fixes 1 through 3, the database is clean. If DevTools still shows mixed content for resources on yoursite.nl (not an external domain), the URLs are hardcoded in PHP files. This happens when a developer embedded a literal http://yoursite.nl/... URL into a theme template's image or stylesheet reference instead of using home_url() or get_template_directory_uri().

Search the active theme and any custom plugins for hardcoded HTTP URLs. The easiest way without SSH is to use Appearance > Theme File Editor in wp-admin and search each template file (header.php, footer.php, functions.php) for http://yoursite.nl. Alternatively, download the theme folder via your hosting panel's file manager and search locally with a text editor.

If you have SSH access:

grep -rn 'http://yoursite.nl' wp-content/themes/your-active-theme/
grep -rn 'http://yoursite.nl' wp-content/plugins/your-custom-plugin/

Expected output.

wp-content/themes/your-active-theme/header.php:42:  href="http://yoursite.nl/favicon.ico"
wp-content/themes/your-active-theme/footer.php:18:  src="http://yoursite.nl/assets/logo.png"
wp-content/themes/your-active-theme/single.php:89:  href="http://yoursite.nl/css/print.css"

Fix each line by either replacing http:// with https://, or better, by using a WordPress function that generates the correct scheme at runtime. For example, replace a hardcoded favicon URL with home_url( '/favicon.ico' ) wrapped in esc_url(), and replace a hardcoded theme asset URL with get_template_directory_uri() . '/assets/logo.png' wrapped in esc_url(). Both calls produce the correct scheme (HTTP or HTTPS) based on the current request.

The second form is portable: it will still work if you later move the site to a staging domain or a different host.

Do not edit a theme you did not build. If the hardcoded URLs live in a theme from a marketplace, create a child theme or override the template in your own code. Direct edits to a parent theme get wiped on the next update.

Verification. Reload and reload again with DevTools open. The specific resources you found should now load over HTTPS. If they still fail, the HTTPS version genuinely does not exist (the file is gone, or the domain serving it does not support HTTPS) and you need to host the asset yourself or choose a different one.

Fix 5: Reverse proxy special case — see also how to force WordPress to HTTPS without the redirect loop for the complete server-level setup guide. This section covers (Cloudflare Flexible, AWS ALB, load balancers)

If you run behind Cloudflare Flexible SSL, an AWS Application Load Balancer, or any reverse proxy that terminates HTTPS and then talks HTTP to the origin, WordPress sees an HTTP connection and generates HTTP URLs. No amount of database rewriting fixes this: on the next request WordPress regenerates URLs based on what it thinks the scheme is.

The WordPress HTTPS admin guide documents the symptom directly: "WordPress can recognize the HTTP_X_FORWARDED_PROTO header when running behind a reverse proxy to prevent infinite redirect loops." The fix is to tell WordPress that the original connection was HTTPS, even though the PHP process itself sees HTTP.

Open wp-config.php in your hosting panel's file manager (or via SFTP) and add this block above the /* That's all, stop editing! */ line and above your WP_HOME / WP_SITEURL constants:

if (
    isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
) {
    $_SERVER['HTTPS'] = 'on';
}

This runs before WordPress calls is_ssl(). is_ssl() checks $_SERVER['HTTPS'] for 'on' or '1'; by setting it based on the forwarded header, you tell WordPress what the client actually used.

Cloudflare Flexible specifically. Cloudflare's own blog is blunt: Flexible SSL "creates a secure (HTTPS) connection between the website visitor and CloudFlare and then an in-secure (HTTP) connection between CloudFlare and the origin server". For any site that handles login credentials, ecommerce, or form submissions, I strongly recommend switching to Full or Full (Strict) SSL mode and installing a real certificate on the origin. Flexible is a workaround for when you cannot get a certificate on the origin at all; it is not a production posture for a WordPress site.

Verification. Reload, check DevTools, and confirm no mixed content warnings appear. If you have SSH access, you can also verify the response headers from a separate machine:

curl -sI https://yoursite.nl/ | grep -i location

You should not see a Location: header pointing to http:// anywhere. If you do, something else is still forcing an HTTP redirect (an .htaccess rule, an nginx config, or a caching layer).

A word on FORCE_SSL_ADMIN

You may see FORCE_SSL_ADMIN suggested as a mixed content fix. It is not. FORCE_SSL_ADMIN forces HTTPS for logins and the admin dashboard only; it has no effect on front-end URLs or subresources. The constant has been admin-scoped since WordPress 2.6.0. WordPress 4.0 deprecated the separate FORCE_SSL_LOGIN constant, but FORCE_SSL_ADMIN's scope was not changed. Use it to lock down wp-admin, but do not expect it to clean up mixed content on your front end.

Verifying the fix end-to-end

After applying Fix 2 or Fix 3 (and Fix 4 if needed, Fix 5 if you are behind a proxy), run these three checks in order:

  1. DevTools console on a hard reload. Ctrl+Shift+R. Zero mixed content lines.
  2. DevTools Network tab with scheme:http filter. Zero rows.
  3. View page source in the browser. Right-click the page, choose "View Page Source", and use Ctrl+F (or Cmd+F) to search for http://yoursite.nl. Any match means something still references the insecure scheme. Go back through the fixes. If you have SSH access, you can automate this check:
curl -s https://yoursite.nl/ | grep -o 'http://[^"'"'"' >]*' | sort -u

Expected output. An empty list, or a list containing only external domains that genuinely do not support HTTPS (rare in 2026).

  1. Browser padlock. Close and reopen the browser tab. The padlock should be solid, with no warning overlay. Different browsers draw it slightly differently, but none of them should show a crossed-out padlock, an exclamation mark, or a "Not Secure" label.

When to escalate

If you have worked through all five fixes and the padlock is still broken, collect the following before asking for help:

  • The exact DevTools console output, verbatim, including the URL of every flagged resource.
  • The WordPress Address and Site Address from Settings > General (or wp option get siteurl and wp option get home if you have WP-CLI).
  • Your wp-config.php, with any database credentials redacted.
  • Whether you run behind Cloudflare, another CDN, or a load balancer, and if so which SSL mode.
  • The response headers on the homepage (use a free tool like securityheaders.com or, if you have SSH access, curl -sI https://yoursite.nl/).
  • Your active theme name and version, and a list of active plugins from Plugins > Installed Plugins.

A site with mixed content almost always has one of the five causes above. If none of them apply, the problem is usually a JavaScript-injected resource from a third-party script (chat widgets, old analytics, embedded players) that you can only find by opening the Network tab with "Preserve log" enabled and watching traffic across a full user session.

How to prevent it from happening again

  • Fix it at the database layer, once. Do not rely on Really Simple SSL or any runtime HTML-rewriting plugin as a permanent solution. Run the search-replace, remove the plugin, and move on.
  • Use home_url(), site_url(), and get_template_directory_uri() in custom code. Never hardcode http://yoursite.nl or https://yoursite.nl in a theme or plugin file. WordPress will generate the correct scheme for you.
  • If you run behind a reverse proxy, set the HTTP_X_FORWARDED_PROTO shim in wp-config.php on day one, not after the first redirect loop.
  • Use Full or Full (Strict) SSL on Cloudflare. Flexible SSL is a dead end for any real WordPress site.
  • When you migrate the site to a new domain, run wp search-replace again for the new domain, and verify with DevTools. Mixed content after a migration usually means stale URLs from a site you forgot about, like https://staging.yoursite.nl or https://yoursite.local.

For the related symptom where the redirect between HTTP and HTTPS goes into an infinite loop, see the Too Many Redirects in WordPress article, which covers the same Cloudflare Flexible SSL scenario from the redirect angle. For sessions that fail to persist across HTTP and HTTPS during an incomplete migration, see the cookies are blocked in WordPress article.

Want WordPress hosting that stays stable?

I handle updates, backups, and security, and keep performance steady—so outages and slowdowns don't keep coming back.

Explore WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.