Skip to content
  • FG-28
  • 13 min read
  • Last checked November 8, 2026

How to Fix Mixed Content and a “Not Secure” Warning on an HTTPS WordPress Site

Your certificate works but the padlock is missing or parts of the page fail to load. Find every http:// reference and fix it at the source, including builder data.

Run the health check

Your certificate is valid and the address starts with https://, yet the browser shows “Not secure”, a warning icon instead of the usual secure indicator, or “Your connection to this site is not fully secure”. Some images, fonts, sliders or forms may be missing. In the browser console you will find lines that start with “Mixed Content: The page at ‘https://example.com/’ was loaded over HTTPS, but requested an insecure…”.

This is mixed content: a secure page that pulls in at least one file, script or form target over plain HTTP. The connection to your page is encrypted, but those extra requests are not, so the browser either upgrades them, blocks them or downgrades the security indicator. Site owners usually meet this right after switching a site to HTTPS or migrating it, because old http:// addresses are stored all over the database.

What is happening

An HTTPS page promises that everything on it came from the server untouched. A single HTTP request breaks that promise, because anyone on the network path could read or change it. Browsers split mixed content into two groups and treat them differently.

Type Examples What current browsers do
Upgradable (formerly “passive” or “display”) img, audio, video Chrome and Firefox (from version 127) rewrite the request to HTTPS. If the file is not available over HTTPS, it does not load.
Blockable (formerly “active”) scripts, stylesheets, iframes, fonts, fetch and XHR requests, objects Blocked outright. No upgrade is attempted.
Insecure form target <form action="http://..."> The secure indicator is downgraded, and Chrome warns the visitor before submitting.

That table explains the symptoms people describe. A blocked stylesheet makes the layout fall apart. A blocked script breaks a slider, a menu or a checkout. An image whose host has no HTTPS version simply disappears. A newsletter form posting to an HTTP address keeps the secure indicator from showing. Visitors can also allow insecure content for one site in their browser settings, which can make your own testing misleading, so always test in a fresh private window.

The http:// addresses come from a handful of places:

  • Post and page content in wp_posts, such as images inserted before the switch.
  • Options and theme settings in wp_options, such as a logo URL in the Customizer or widget text. These are often PHP serialized arrays.
  • Page builder data in wp_postmeta, usually stored as JSON in which slashes are escaped, so the address appears as http:\/\/example.com.
  • Theme and plugin files and CSS, for example a background-image: url(http://...) rule or a hardcoded script tag.
  • External embeds and scripts, such as an old widget or map code that loads from an HTTP address.
  • Server detection: behind a proxy or load balancer, WordPress may not know the visitor is on HTTPS and builds every asset URL with http://.

Before you start

You need an administrator login to WordPress and, ideally, SSH access so you can run wp-cli. A database search and replace changes many rows at once and cannot be undone except from a backup, so take one first. With wp-cli, wp db export BACKUP_FILE.sql run in the site folder does this; replace BACKUP_FILE with a name such as the date, and store the file outside the public web folder afterward. Good output ends with “Success: Exported to”.

How to run the commands in this guide. Server commands run in an SSH terminal on the web server, from the site folder, which is the folder containing wp-config.php. Depending on your host, wp-cli is called wp or wp-cli; use whichever exists. A few commands run on your own computer: on macOS and Linux, curl is in Terminal; on Windows, use WSL or curl.exe in PowerShell. Browser checks happen in devtools, opened with F12 or Ctrl+Shift+I (Cmd+Option+I on a Mac). If you cannot use SSH, a search and replace plugin that understands serialized data is the fallback, with the same backup rule.

Step 1: List the insecure requests in devtools

Do not guess. The browser already knows exactly which requests are insecure.

  1. Open the affected page in a private window, open devtools and select the Console tab.
  2. Reload the page. Mixed content messages are listed with the page URL and the insecure resource URL.
  3. Switch to the Network tab, reload, and type mixed-content:all into the filter box to list only mixed requests. In Chrome you can also use mixed-content:blocked and mixed-content:displayed.
  4. Open the Privacy and security panel (called the Security panel before Chrome 134). Its overview states whether the page and its resources are secure and links to the offending requests.

A typical blocked script message reads like this:

Mixed Content: The page at 'https://example.com/shop/' was loaded over HTTPS, but requested an insecure script 'http://example.com/wp-content/plugins/PLUGIN_SLUG/slider.js'. This request has been blocked; the content must be served over HTTPS.

This is browser console output, not a command. PLUGIN_SLUG stands for whichever plugin folder appears in your message. Read two things from each line: the resource type (script, stylesheet, image, iframe, font) and the host. If the host is your own domain, the fix is in your database or files. If the host is a third party, you need that provider’s HTTPS address. A message saying the request was automatically upgraded is low priority; one saying it was blocked is what breaks the page.

Check more than the home page. Test a blog post, a shop or product page, the contact page and any landing page built with your page builder, because each pulls data from different places.

Step 2: Confirm WordPress knows it is on HTTPS

If every theme and plugin file loads over http://, the cause is probably not stored data. It is WordPress building URLs with the wrong scheme. Check the two core settings first, on the server:

wp option get home
wp option get siteurl

Both should print an address starting with https://. If either starts with http://, update it in Settings, General, or with wp option update home 'https://example.com' and the same for siteurl, replacing example.com with your domain. If these are defined as WP_HOME and WP_SITEURL in wp-config.php, the file wins, so edit it there.

If both are already HTTPS but assets still load over HTTP, the site is probably behind a reverse proxy, load balancer or CDN that talks to the server over plain HTTP, so is_ssl() returns false. Check whether the proxy sends an X-Forwarded-Proto: https header, and tell WordPress to trust it. Our guide to ERR_TOO_MANY_REDIRECTS covers that proxy fix in detail, because the same misdetection causes redirect loops.

Step 3: Measure the stored http:// URLs before changing anything

Now find out how many stored references exist. wp-cli can perform a dry run that changes nothing and reports counts per table and column:

wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --precise --skip-columns=guid --dry-run --report-changed-only

Run this on the server in the site folder. Replace example.com in both strings with your exact domain, including www. if your site uses it. Do not add a trailing slash, so that every path below the domain matches. The flags:

  • --dry-run runs the whole search and reports what would change, without writing anything.
  • --all-tables-with-prefix includes every table that starts with your table prefix (set in wp-config.php, often wp_), not only the core tables. Plugin tables such as form or builder tables are included this way.
  • --precise forces wp-cli to load each matching value into PHP and process it safely. It is slower but handles serialized data reliably.
  • --skip-columns=guid leaves the post GUID column alone. GUIDs are permanent identifiers used by feed readers and are not loaded by browsers, so they do not cause mixed content.
  • --report-changed-only hides tables with zero matches so the report is readable.

Good output is a table listing tables, columns and replacement counts, ending with a line like “Success: 412 replacements to be made.” Zero replacements means the plain form is not your source, so check the escaped form below. An error such as “Error: Error establishing a database connection” means wp-cli cannot reach the database from this folder; make sure you are in the folder that contains wp-config.php.

The escaped JSON form

Most page builders store layouts as JSON, and JSON encoders often escape forward slashes. In the database the address looks like http:\/\/example.com, which the plain search above will not match. Measure that form separately:

wp search-replace 'http:\/\/example.com' 'https:\/\/example.com' --all-tables-with-prefix --precise --skip-columns=guid --dry-run --report-changed-only

Run this on the server. Keep the single quotes so your shell passes the backslashes through unchanged. example.com is your domain again. Good output shows matches mainly in the wp_postmeta table in the meta_value column, which is where builders such as Elementor and Breakdance keep their data. If you see counts here, this is why your builder pages kept old URLs after a plain replace.

Step 4: Run the replacement for real

With a backup in hand and the dry run looking sensible, run the same two commands without --dry-run:

wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --precise --skip-columns=guid --report-changed-only
wp search-replace 'http:\/\/example.com' 'https:\/\/example.com' --all-tables-with-prefix --precise --skip-columns=guid --report-changed-only
wp cache flush

Run these on the server, replacing example.com with your domain. The flags mean the same as in Step 3. wp cache flush empties the WordPress object cache so old values are not served from memory. Good output ends with “Success: Made N replacements” for each command, where N matches the dry run. If your site used both www and the bare domain over the years, run the pair again for the other variant.

Why use wp-cli instead of a raw SQL REPLACE()? PHP serialized data stores the length of each string, for example s:23:"http://example.com/logo";. Adding the “s” in “https” changes the length. A raw SQL replace leaves the old length in place, PHP can no longer read the value, and the setting silently resets. wp-cli unserializes the value, replaces the text and serializes it again with correct lengths.

Rebuild generated files

Page builders and optimization plugins write CSS and combined files to disk. Those files still contain the old URLs until regenerated. Use your builder’s tools screen (Elementor has Regenerate CSS and Data under its Tools menu; other builders have an equivalent cache or CSS regenerate button), then purge any page cache, optimization plugin cache and CDN cache.

Step 5: Fix hardcoded URLs in theme files, CSS and code

If devtools still lists your own domain after the database is clean, the URL is in a file. Search the theme and plugin folders on the server:

grep -rn --include='*.php' --include='*.css' --include='*.js' 'http://example.com' wp-content/themes wp-content/plugins wp-content/mu-plugins

Replace example.com with your domain. -r searches folders recursively, -n prints the line number, and each --include limits the search to that file type. Good output is nothing at all. Bad output looks like wp-content/themes/THEME_NAME/style.css:212: background-image: url(http://example.com/wp-content/uploads/hero.jpg);, giving you the file and line to fix, where THEME_NAME is your theme folder. Change these to https://, or to a relative path such as /wp-content/uploads/hero.jpg. Edit a child theme rather than the parent, or the next theme update will undo your fix. Also search for http:// generally in custom snippets, the Customizer’s Additional CSS (stored in the database, so Step 4 covers it) and any header and footer scripts plugin.

A CSS background image is a common surprise, because it is easy to miss in a page source view. In the browser, it shows in the Console like any other image request.

Step 6: Replace or remove insecure external resources

For third-party hosts, try the HTTPS version of the same address in a new tab. Most providers serve HTTPS today, so you simply change the scheme in the embed code. If the provider has no HTTPS version, the resource cannot be loaded securely at all. Download and self-host it if the license allows, replace it with a current embed, or remove it. Older protocol-relative URLs that start with // are not themselves mixed content on an HTTPS page, but writing https:// explicitly is clearer.

Step 7: Use upgrade-insecure-requests as a stopgap only

If you need the warnings gone today while you fix the source, the Content-Security-Policy directive upgrade-insecure-requests tells browsers to rewrite every HTTP subresource request on your pages to HTTPS before sending it. Add it as a header on the server. For Apache, in .htaccess or the virtual host (requires mod_headers):

Header always set Content-Security-Policy "upgrade-insecure-requests"

For Nginx, inside the HTTPS server block:

add_header Content-Security-Policy "upgrade-insecure-requests" always;

Both are files you edit on the server; neither has placeholders. always makes the header appear on error pages too. Reload Nginx after editing with sudo nginx -t then sudo systemctl reload nginx. If you already send a Content-Security-Policy header, add the directive to it instead of sending a second header, because multiple policies combine and can block things you did not intend.

Check that it is live from your computer:

curl -sI https://example.com/ | grep -i content-security-policy

-s hides the progress bar and -I requests headers only. Good output prints the header line. No output means the header is not being sent; check that you edited the file for the right site and reloaded the server.

Treat this as a bandage. It only helps browsers that support it, does nothing for RSS readers, emails or apps that consume your content, and a resource with no HTTPS version will still fail. It also hides new mistakes. Fix the source and keep the header only as a safety net.

Advanced: Hunting stubborn mixed content

When the usual sources are clean and one request remains, query the database directly to find exactly where a string lives. On the server:

wp db search 'http://example.com' --all-tables-with-prefix --stats

Replace example.com with your domain. wp db search scans every column of each table, --all-tables-with-prefix widens the scope as before, and --stats adds a summary of matches per table. Good output is “Success: Found 0 matches”. Otherwise each match line shows the table, column and row ID, which you can inspect, for example with wp post get POST_ID where POST_ID is the number shown.

Some URLs are generated at runtime and never stored. A plugin might build an address from a hardcoded constant, or a theme might use get_template_directory_uri() while the proxy problem from Step 2 is unresolved. In those cases the HTML source contains http://, yet the database search finds nothing. Use the Network panel’s Initiator column to see which script or line requested the file, and a plugin such as Query Monitor to see which plugin enqueued a script.

WordPress 5.7 and newer can switch a site to HTTPS from Tools, Site Health. When the site address changes from http:// to https:// on the same domain (through that button or in Settings, General) on a site with existing content, WordPress also rewrites old http:// links to your own site in post content, excerpts, text widgets and Additional CSS as pages are displayed, without changing the database. That can make a site look clean while the stored data is still old, which matters the moment you move it or use the content elsewhere. Running the search and replace in this guide fixes the stored data for good.

Finally, sites embedded in other systems, such as an iframe inside another HTTPS site, or a page loaded through a translation proxy, report mixed content against the outer page. Check which page the console message names before chasing it.

When to contact your host

Open a ticket if WordPress keeps building http:// asset URLs even though home and siteurl use HTTPS, which points to a proxy or server variable problem, or if you cannot run wp-cli. Send:

  • The page URL and the exact console messages, including the insecure resource URLs.
  • The output of wp option get home and wp option get siteurl.
  • The headers from curl -sI https://example.com/.
  • Whether a CDN or proxy sits in front of the site, and the UTC time you tested.
  • What changed recently, such as an HTTPS switch, a migration or a new plugin.

If the site is broken for visitors right now, start at our Emergency Room. For a layout that is broken for reasons other than blocked stylesheets, see our guide to CSS not loading.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides
FG-04 SSL

How to Fix ERR_CERT_DATE_INVALID (Expired SSL Certificate)

Your SSL certificate expired, or the visitor’s clock is wrong. Confirm which with one command, renew the certificate, and fix the DNS, CAA or blocked-challenge problem that stopped it renewing.

Read the guide
9 min read

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close