You moved your site to HTTPS, but the padlock shows a warning, some images or scripts are missing, and the console says Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure resource 'http://example.com/...'. This request has been blocked; the content must be served over HTTPS. To fix mixed content, find every http:// URL the page loads and change it to https:// at the source. This guide shows how, including the database clean-up WordPress sites need.
Quick fix
On most sites, especially WordPress, the insecure URLs are hard-coded in the database from before the HTTPS switch. The fix:
- Open DevTools, go to the Console, and filter by
Mixed Contentto list every offending URL. - Back up the database.
- Run a serialisation-safe search-and-replace from
http://tohttps://for your own domain. - Clear every cache (plugin, server, CDN) and reload.
wp db export before-https.sql
wp search-replace 'http://example.com' 'https://example.com' --all-tables --precise --dry-run
wp search-replace 'http://example.com' 'https://example.com' --all-tables --precise
wp cache flush
Run the --dry-run first and check the replacement counts before running it for real.
What mixed content is
A page served over HTTPS that loads sub-resources over plain HTTP is “mixed”. Browsers treat the two kinds differently:
- Active mixed content (scripts, stylesheets, iframes,
fetch/XHR, fonts) is blocked outright, because an attacker could use it to rewrite the whole page. That’s why layouts break or features stop working. - Passive mixed content (images, audio, video) is automatically upgraded to HTTPS by modern browsers. If the HTTPS version doesn’t exist, it fails to load. Either way, the padlock is no longer clean.
Finding the offenders
- Console. Every mixed request is logged with its full URL. Reload with the console open.
- Security panel. Chrome’s Security panel in DevTools summarises whether the page has insecure origins and lists them.
- View source and stylesheets. Search for
http://in the HTML, and also in CSSurl()values,srcsetattributes, inline styles and JSON embedded in the page. - Crawl the site. Mixed content is per page. Check templates you rarely visit, such as archives, search and 404 pages.
curl -s https://example.com/ | grep -oE '(src|href|srcset|content)="http://[^"]+' | sort -u
Cause 1: hard-coded http:// URLs in the database
WordPress stores absolute URLs in post content, widget settings, theme options and page-builder data. Changing the Site Address setting doesn’t touch any of it.
Don’t use a plain SQL REPLACE(). WordPress stores many options as PHP serialised strings that include each string’s length, such as s:22:"http://example.com/img". Changing the length without updating that number corrupts the value, and widgets or theme settings silently reset. wp search-replace unserialises, replaces and re-serialises safely. That’s what --precise enforces for every row.
Also check for URLs stored with escaped slashes, which block editor attributes and page builders such as Elementor use in JSON:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --precise --dry-run
Elementor also has its own Replace URL tool under its settings, which regenerates its CSS files afterwards. Finally, make sure the core URLs are correct, either in Settings or in wp-config.php:
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
Cause 2: theme and plugin assets
If the warnings are about CSS, JS or font files under /wp-content/themes/ or /wp-content/plugins/, the code is building URLs with a hard-coded scheme. Search the theme for http:// and replace it with the WordPress helpers, which respect the current scheme:
// Bad
echo '<script src="http://example.com/wp-content/themes/mytheme/app.js"></script>';
// Good
wp_enqueue_script('app', get_theme_file_uri('app.js'), [], '1.0', true);
echo '<img src="' . esc_url(get_theme_file_uri('img/logo.svg')) . '" alt="">';
For non-WordPress sites the idea is the same. Check environment variables such as NEXT_PUBLIC_SITE_URL, CMS settings, and absolute URLs in Markdown or JSON content.
Cause 3: HTTPS terminated at a proxy or CDN
This is the one experienced developers miss. If Cloudflare, a load balancer or a reverse proxy handles HTTPS and talks to your server over HTTP, the application thinks the request is insecure. WordPress’s is_ssl() then returns false, and it generates http:// URLs for scripts and styles even though the database is clean. Tell WordPress to trust the forwarded protocol, at the top of wp-config.php:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
Only do this when your server is reachable solely through the proxy, since the header can otherwise be spoofed. The same mismatch causes redirect loops. See fixing too many redirects with WordPress and Cloudflare. Ideally, use Cloudflare’s Full (strict) SSL mode so the origin connection is encrypted too.
Cause 4: iframes, embeds and third-party resources
Old embed codes, such as video players, maps and booking widgets, may use http:// iframe or script URLs. Iframes are active content and get blocked, so the embed just disappears. Re-copy the current embed code from the provider. Almost every provider supports HTTPS now. If a third party genuinely has no HTTPS endpoint, remove it or proxy it through your own HTTPS server. Also check <form action="http://...">: browsers warn before submitting a secure page’s form to an insecure URL.
Protocol-relative URLs (//cdn.example.com/lib.js) technically work, but they’re obsolete. Write https:// explicitly.
Safety nets, not fixes
Two tools hide mixed content warnings without fixing the source. They’re worth turning on, but only after the clean-up.
upgrade-insecure-requeststells the browser to fetch everyhttp://sub-resource over HTTPS. If a resource doesn’t exist on HTTPS, it still fails, just more quietly. Old browsers ignore it, and your source still contains insecure URLs that leak into feeds, emails and the API.- Cloudflare Automatic HTTPS Rewrites rewrites links in HTML that passes through Cloudflare, but only for domains it knows support HTTPS. It doesn’t touch URLs built by JavaScript at runtime, or content served when you bypass Cloudflare.
add_header Content-Security-Policy "upgrade-insecure-requests" always;
HSTS: roll out carefully
Once everything works over HTTPS, Strict-Transport-Security tells browsers never to use HTTP for your domain. It stops downgrade attacks, but it’s sticky. Browsers remember it for max-age seconds, and you can’t take it back early.
- Start with a short
max-agesuch as 300 seconds, confirm nothing breaks, then increase it in steps. - Add
includeSubDomainsonly when every subdomain, including old staging and mail hosts, serves valid HTTPS. - Submit to the HSTS preload list only when you’re certain. Removal is slow.
add_header Strict-Transport-Security "max-age=300" always;
# later: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Step-by-step diagnosis
- Load the page in a private window with DevTools open and record every mixed content message.
- Group the URLs. Your domain in uploads points to the database. Theme or plugin paths point to code. Third-party domains point to embeds.
- If your own assets come out as
http://even after the database fix, test for the proxy problem by logging$_SERVER['HTTPS']on the origin. - Purge page caches, object caches and the CDN, then retest the homepage, a post, an archive and a 404 page.
Prevention
- Never hard-code a scheme or domain. Use helpers like
get_theme_file_uri()or a single site-URL config value. - Redirect all HTTP to HTTPS at the server or CDN, and keep the origin on HTTPS too.
- Add a CI or monitoring check that fails when a page contains
src="http://. - Mixed content is one item on a longer list. My WordPress security hardening checklist covers the rest.
Key takeaways
- Find offenders in the Console and Security panel, then fix them at the source.
- Use
wp search-replace, never raw SQL, so serialised data survives. - HTTPS terminated at a proxy makes WordPress generate
http://URLs. TrustX-Forwarded-Proto. upgrade-insecure-requestsand Automatic HTTPS Rewrites are safety nets, not fixes.- Roll out HSTS gradually, with a short
max-agefirst.
Frequently asked questions
What causes mixed content errors?
An HTTPS page loads a script, stylesheet, image, iframe or other resource through an http:// URL. The usual sources are old absolute URLs in the database, hard-coded theme URLs, outdated embed codes, or an app that doesn’t know it’s behind HTTPS.
How do I fix mixed content in WordPress?
Back up the database, then run wp search-replace 'http://example.com' 'https://example.com' --all-tables --precise. Update theme code to use WordPress URL helpers and clear all caches.
Does upgrade-insecure-requests fix mixed content?
It upgrades insecure sub-resource requests in supporting browsers, but it fails for resources that aren’t available over HTTPS and leaves the bad URLs in your source. Use it as a safety net after fixing the URLs.
Is it safe to search and replace URLs with SQL?
Not for WordPress. Plain SQL replacements break PHP serialised data, because string lengths no longer match. Use WP-CLI’s search-replace or a tool that handles serialisation.
If your site still won’t show a clean padlock after a migration, I can audit it and fix it at the source. Get in touch. If cross-origin requests are failing too, see how to fix CORS errors.