If your WordPress site “has SSL” but the browser still throws a warning, you’re not alone. The padlock is a liar—well, not exactly. It’s doing what it was designed to do: tattling when any resource on an HTTPS page is fetched over HTTP.
This is the part where teams panic, install three more plugins, and accidentally make it worse. Don’t. Mixed content is a systems problem: the database, themes, plugins, CDNs, proxies, caches, and headers all get a vote. You fix it by narrowing the sources, changing the right layer, and preventing relapse.
What mixed content actually is (and why browsers care)
Mixed content happens when a page loaded over HTTPS pulls in any subresource over plain HTTP. Subresources include images, CSS, JavaScript, fonts, iframes, video, XHR/fetch calls, and even background images referenced inside CSS.
Browsers treat this as a security downgrade. TLS secures the top-level document, but if you load a script over HTTP, an attacker sitting on the network can swap it. Now your “secure” page can be turned into a credential-harvesting kiosk.
Active vs passive mixed content
- Active mixed content (scripts, iframes, XHR) can execute code or change the page behavior. Browsers typically block it by default.
- Passive mixed content (images, audio, video) usually can’t execute code directly. Browsers may allow it but warn. “May allow” is not a strategy.
Why “I installed SSL” isn’t enough
SSL termination just means the edge can speak HTTPS. WordPress still might:
- Store absolute HTTP URLs in the database.
- Generate HTTP links when it thinks it’s on HTTP (reverse proxy mismatch).
- Load third-party assets over HTTP (old theme, vendor snippets, embedded content).
- Serve redirects inconsistently (some paths 301 to HTTPS, others don’t).
- Cache old HTML containing HTTP URLs (page cache, CDN cache).
One more thing: browser warnings aren’t always about obvious http://. They can come from protocol-relative URLs (//example.com), CSS url(), inline scripts, and hardcoded plugin endpoints. Mixed content is a scavenger hunt, except the scavenger hides in your footer widget.
Interesting facts and a little history
- “Mixed content” predates modern browsers. Early HTTPS sites often embedded HTTP images to save CPU—TLS used to be expensive.
- Google’s “HTTPS as a ranking signal” (2014) turned the padlock from a nice-to-have into an org-level KPI.
- Let’s Encrypt (public launch 2015) made certificates free and automated, which increased HTTPS adoption—and exposed decades of HTTP hardcoding.
- HTTP/2 (standardized 2015) was effectively HTTPS-only in major browsers, pushing sites to migrate just to get performance features.
- Chrome started labeling HTTP pages “Not Secure” (2018) especially on forms, which forced even reluctant businesses to clean up mixed content during HTTPS rollouts.
- HSTS can “lock” a domain into HTTPS. Great for security, brutal when you still have HTTP-only subdomains or third-party assets.
- Protocol-relative URLs (
//) were once a migration trick. Today they mostly cause confusion and should be retired on modern HTTPS-first sites. - WordPress stores URLs everywhere. Not just posts: options, widgets, builder data, serialized arrays, and sometimes theme mods.
- Some CDNs reintroduce mixed content. You can terminate TLS at the CDN but still fetch origin assets over HTTP or rewrite HTML inconsistently.
Fast diagnosis playbook: find the offender fast
This is the triage order I use when someone pings “padlock is broken” five minutes before a launch.
1) Reproduce with one page and look at the browser console
Pick a single problematic page, open DevTools → Console and Network. Mixed content errors usually include the exact offending URL.
Decision: If the resource is your domain, fix WordPress/config/database. If it’s third-party, decide whether to replace it, proxy it, or remove it.
2) Confirm what WordPress thinks its own URL is
If siteurl/home are HTTP, WordPress will happily print HTTP links forever.
Decision: If mismatch exists, correct in DB or via WP-CLI, then purge caches.
3) Validate TLS and redirect behavior from the edge
Check if HTTP redirects to HTTPS consistently, and whether your reverse proxy sets the right headers.
Decision: If WordPress is behind a load balancer/CDN, ensure it sees HTTPS=on equivalents (typically X-Forwarded-Proto: https).
4) Search for hardcoded http:// in rendered HTML
Don’t guess. Fetch the page and grep. If http:// shows up, you’ve got a rewrite/database/theme issue.
Decision: If it’s in HTML from posts/options, do a safe search-replace. If it’s in theme/plugin code, fix code or override.
5) If it’s “only sometimes,” suspect caches or conditional rendering
Page cache, object cache, CDN, and builder caches can preserve old URLs. Also: logged-in vs anonymous output can differ.
Decision: Purge the right cache layers, then verify with a fresh request bypassing cache.
Where mixed content comes from in WordPress (real failure modes)
Database-stored absolute URLs
WordPress content is a landfill of URLs. Posts and pages store absolute links. So do widgets. So do builder plugins. The worst part: some plugins store data as serialized PHP arrays, so naive search/replace breaks string lengths and corrupts data.
Modern WP-CLI handles this correctly. Most “search replace” plugins handle it correctly too, but running them in production without a backup is how you earn new gray hair.
Theme or plugin hardcoding
Common patterns:
- Enqueued scripts/styles with
http://hardcoded. - Google Fonts or analytics snippets copied from an ancient blog post.
- Inline CSS with
background-image: url(http://...). - Old social widgets and tracking pixels.
Reverse proxy / CDN confusion: WordPress thinks it’s HTTP
If TLS is terminated at a load balancer (AWS ALB, NGINX proxy, CDN), the backend might receive plain HTTP. Unless you pass the right headers and configure WordPress to trust them, it generates HTTP asset URLs.
This is also where you see loops: HTTP→HTTPS redirects happen at the proxy, but WordPress still redirects back, or generates mixed schemes in canonical links.
CDN or cache serving stale HTML
You fixed the DB, but the CDN is still serving the old HTML with HTTP URLs. Or a page cache plugin has a static HTML file from last week. Or your object cache has stale options.
Third-party content: the one you can’t fix from here
If a third-party script is only served over HTTP, it’s a dead dependency. Replace it. If you can’t replace it, you can sometimes proxy it through your domain over HTTPS, but then you own the security and caching behavior. Don’t do this lightly.
Joke #1: Mixed content is like wearing a seatbelt while leaving the car door open—technically you tried, practically you didn’t.
Practical tasks with commands: detect, decide, fix
These tasks assume you can SSH into the host or container running WordPress. If you can’t, you can still do most checks from your laptop, but ops reality is: the fix often lives server-side.
Rule: Each task ends with a decision. If you don’t decide, you’re just collecting logs for your scrapbook.
Task 1: Check the rendered HTML for obvious HTTP resources
cr0x@server:~$ curl -sS https://example.com/ | grep -Eo 'http://[^"]+' | head
http://example.com/wp-content/uploads/2022/10/hero.jpg
http://fonts.googleapis.com/css?family=Open+Sans:400,700
What it means: The page HTML includes absolute HTTP URLs.
Decision: If it’s your domain (example.com), fix DB/theme. If it’s third-party (fonts.googleapis.com), update snippet to HTTPS or remove/replace.
Task 2: Use the browser-grade view with headers to confirm redirects
cr0x@server:~$ curl -I http://example.com/
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Server: nginx
What it means: HTTP redirects to HTTPS at the edge.
Decision: Good. If you don’t get a 301/308 to HTTPS, fix your web server/CDN redirect policy first. Mixed content fixes are pointless if users can still land on HTTP.
Task 3: Confirm the canonical URL WordPress outputs
cr0x@server:~$ curl -sS https://example.com/ | grep -iE 'rel="canonical"|og:url' | head -n 5
<link rel="canonical" href="http://example.com/" />
<meta property="og:url" content="http://example.com/" />
What it means: WordPress (or an SEO plugin) believes the site URL is HTTP.
Decision: Fix home and siteurl in WP options and verify reverse proxy headers.
Task 4: Check WordPress URL settings via WP-CLI
cr0x@server:~$ cd /var/www/html
cr0x@server:~$ wp option get home
http://example.com
cr0x@server:~$ wp option get siteurl
http://example.com
What it means: Your base URLs are HTTP, which drives internal link generation.
Decision: Update both to HTTPS (next task), then purge caches.
Task 5: Update home and siteurl safely
cr0x@server:~$ wp option update home 'https://example.com'
Success: Updated 'home' option.
cr0x@server:~$ wp option update siteurl 'https://example.com'
Success: Updated 'siteurl' option.
What it means: WordPress will now generate HTTPS URLs by default.
Decision: Re-test the page. If you still see HTTP in content, you need search/replace in posts/options.
Task 6: Identify HTTP URLs across the database (dry-run first)
cr0x@server:~$ wp search-replace 'http://example.com' 'https://example.com' --all-tables --dry-run
+----------------------+--------------+--------------+------+
| Table | Column | Replacements | Type |
+----------------------+--------------+--------------+------+
| wp_posts | post_content | 128 | SQL |
| wp_postmeta | meta_value | 42 | PHP |
| wp_options | option_value | 19 | PHP |
+----------------------+--------------+--------------+------+
Success: Made 189 replacements.
What it means: There are 189 occurrences of HTTP URLs, including serialized data (marked PHP).
Decision: If replacements look sane, run the same command without --dry-run. If the count is unexpectedly huge, stop and take a DB backup first.
Task 7: Perform the search/replace for real
cr0x@server:~$ wp search-replace 'http://example.com' 'https://example.com' --all-tables
+----------------------+--------------+--------------+------+
| Table | Column | Replacements | Type |
+----------------------+--------------+--------------+------+
| wp_posts | post_content | 128 | SQL |
| wp_postmeta | meta_value | 42 | PHP |
| wp_options | option_value | 19 | PHP |
+----------------------+--------------+--------------+------+
Success: Made 189 replacements.
What it means: Database content now references HTTPS for your domain.
Decision: Purge cache layers; then re-run Task 1. If third-party HTTP remains, fix those individually.
Task 8: Find theme/plugin hardcoded HTTP in code
cr0x@server:~$ grep -RIn --exclude-dir=node_modules --exclude-dir=.git "http://" wp-content/themes wp-content/plugins | head
wp-content/themes/acme/header.php:44: <script src="http://cdn.vendor.example/widget.js"></script>
wp-content/plugins/old-analytics/old-analytics.php:12:$src = 'http://stats.vendor.example/pixel.js';
What it means: You have hardcoded HTTP in code. Search/replace in DB won’t touch this.
Decision: Update to HTTPS or remove the dependency. If the vendor can’t do HTTPS, replace the vendor. Don’t “just ignore the warning.”
Task 9: Confirm what the backend sees (reverse proxy check)
cr0x@server:~$ wp eval 'var_dump($_SERVER["HTTPS"] ?? null, $_SERVER["HTTP_X_FORWARDED_PROTO"] ?? null);'
NULL
string(4) "http"
What it means: WordPress thinks it’s on HTTP behind the proxy (forwarded proto is http).
Decision: Fix the proxy to set X-Forwarded-Proto: https on TLS requests, and configure WordPress to trust it (often via wp-config.php or server config). Without this, mixed content can reappear even after DB cleanup.
Task 10: Inspect response headers for security and scheme hints
cr0x@server:~$ curl -sSI https://example.com/ | grep -iE 'strict-transport-security|content-security-policy|location|x-forwarded-proto'
Strict-Transport-Security: max-age=0
What it means: HSTS is disabled (max-age=0). Not directly mixed content, but it affects how aggressively browsers stay on HTTPS.
Decision: Don’t turn on long HSTS until mixed content is truly gone and you’re sure every subdomain you care about can do HTTPS.
Task 11: Check NGINX config for proxy headers (if you run it)
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "X-Forwarded-Proto" | head
128: proxy_set_header X-Forwarded-Proto $scheme;
What it means: NGINX will pass the scheme it received. If NGINX terminates TLS, $scheme should be https.
Decision: If TLS terminates elsewhere (CDN/ELB), then NGINX may see HTTP and pass http. In that case, set it explicitly based on the incoming forwarded headers, or terminate TLS at NGINX.
Task 12: Find mixed content in CSS files (background images are sneaky)
cr0x@server:~$ grep -RIn "url(http://" wp-content | head
wp-content/themes/acme/assets/css/main.css:233:background-image: url(http://example.com/wp-content/uploads/2021/03/bg.png);
What it means: Static assets reference HTTP inside CSS. Browsers will flag this.
Decision: Fix the CSS (use relative paths or HTTPS) and rebuild/minify if needed. Then purge caches/CDN.
Task 13: Verify uploads are served over HTTPS and not redirected weirdly
cr0x@server:~$ curl -I https://example.com/wp-content/uploads/2022/10/hero.jpg
HTTP/2 200
content-type: image/jpeg
cache-control: public, max-age=31536000
What it means: Uploads are accessible via HTTPS directly.
Decision: If you see redirects to HTTP or a different host, fix rewrite rules, CDN origin config, or offload plugin settings.
Task 14: Check WordPress for “force SSL” settings and admin scheme
cr0x@server:~$ wp config get FORCE_SSL_ADMIN --type=constant
true
What it means: Admin is forced to SSL. Good, but it doesn’t guarantee frontend correctness.
Decision: Keep it, but don’t mistake it for a mixed content fix. Frontend assets and content still need cleaning.
Task 15: Spot-check that no HTTP is left in the page after fixes
cr0x@server:~$ curl -sS https://example.com/ | grep -Eo 'http://[^"]+' | wc -l
0
What it means: The HTML for that page no longer contains obvious HTTP URLs.
Decision: If the browser still warns, it’s likely coming from runtime requests (JS, third-party calls) or another page template. Use DevTools Network to catch it.
Task 16: If you run a CDN, confirm it isn’t rewriting or caching old content
cr0x@server:~$ curl -sSI https://example.com/ | grep -iE 'cf-cache-status|x-cache|age|via'
Age: 8421
Via: 1.1 varnish
What it means: You’re hitting a cache layer with aged content. It may still contain old HTTP links.
Decision: Purge the cache for affected paths, or invalidate everything if you changed sitewide URLs. Then re-test with Cache-Control: no-cache or query-string bypass (depending on your CDN rules).
Three corporate mini-stories (how this goes wrong)
Mini-story 1: The incident caused by a wrong assumption
Company A moved a WordPress marketing site behind a shiny new load balancer. TLS was terminated at the LB; traffic to the origin was HTTP on a private network. Everyone nodded and said, “It’s fine, internal traffic is trusted.” They were half right, which is the worst kind of right.
The web team verified that https:// loaded and saw a padlock on the homepage. Launch day came. Suddenly paid traffic started landing on campaign pages that threw “Not Secure” warnings in some browsers. Conversion dropped. The marketing team blamed the new theme. The theme team blamed the CDN. The SRE on call blamed gravity.
The root issue was simple: WordPress thought requests were HTTP because the forwarded proto header was wrong. The LB sent X-Forwarded-Proto: http due to a misconfigured listener rule. The homepage happened to be cached with correct HTTPS links from a prior request path, but several landing pages rendered fresh HTML and generated HTTP asset links.
Fix: correct the LB header behavior, add backend logic to trust the header only from known proxy IPs, purge cache, then run a database search/replace to clean the old content. The “wrong assumption” was that “TLS at the edge means the app knows it’s HTTPS.” It doesn’t. Software isn’t psychic.
Mini-story 2: The optimization that backfired
Company B wanted faster pages. Someone enabled “HTML rewrite” at the CDN to minify and “normalize” content. It sounded harmless. It even improved Lighthouse scores for a week.
Then a plugin update introduced protocol-relative URLs for a script include: //vendor.example/script.js. The CDN rewrite logic tried to be helpful and “standardize” links. It rewrote some protocol-relative URLs to http:// when the origin fetch was over HTTP (because the CDN-to-origin connection used HTTP for performance reasons—yes, really).
Result: browsers began blocking active mixed content, but only for users who hit cached pages processed by that rewrite path. DevTools showed the script coming from HTTP, but searching the WordPress database didn’t find it. The team wasted hours doing search/replace on content that was never the source.
The fix wasn’t a plugin rollback. It was turning off the CDN HTML rewrite feature (or configuring it to preserve scheme), moving CDN-to-origin to HTTPS, and pinning critical third-party resources to explicit HTTPS. Performance optimization that backfired, classic. The internet always collects its debt with interest.
Mini-story 3: The boring but correct practice that saved the day
Company C had a bland runbook for “site URL migrations” that nobody loved. It required: DB backup, WP-CLI dry-run search/replace, verification via curl/grep, cache purge, then a canary test page in staging and production.
During a rebrand, they moved from an old domain to a new one and enabled HTTPS everywhere. Mixed content should have happened—there were years of embedded images, inline HTML blocks, and a page builder that stored JSON blobs in postmeta.
But the runbook forced them to do the unpleasant parts: a serialized-safe WP-CLI replacement, a scan for http:// across theme/plugin directories, and a verification step that fetched the top 20 templates and grepped for HTTP. It also mandated a CDN purge after changes, not before.
Launch day was uneventful. Nobody praised the runbook. That’s how you know it worked. The boring practice didn’t just fix mixed content; it prevented it from showing up as a random, reputation-denting browser warning during a high-visibility moment.
Common mistakes: symptom → root cause → fix
- Symptom: Homepage shows padlock, but some pages show “Not Secure”
- Root cause: Cached homepage vs uncached templates; mixed content in specific page builder blocks or postmeta.
- Fix: Use DevTools on a failing page, then run WP-CLI search/replace across all tables, plus scan for hardcoded HTTP in theme/plugin code.
- Symptom: Browser console says “blocked active mixed content” for a JS file
- Root cause: Hardcoded
http://script include in a theme, plugin, or injected tag manager snippet. - Fix: Remove or update the snippet to HTTPS; if third-party doesn’t support HTTPS, replace the vendor. Don’t proxy arbitrary JS unless you accept ownership.
- Symptom: WordPress admin is HTTPS, but frontend outputs HTTP assets
- Root cause:
FORCE_SSL_ADMINset, buthome/siteurlstill HTTP; or reverse proxy header mismatch. - Fix: Update
homeandsiteurlto HTTPS; ensureX-Forwarded-Protois correct; purge caches. - Symptom: Mixed content appears only for some users or regions
- Root cause: CDN edge caching stale HTML; regional POPs have different cache states; A/B testing injecting HTTP URLs.
- Fix: Purge CDN caches; verify cache keys; audit injection systems; re-test from multiple regions with consistent headers.
- Symptom: After “fixing” with a plugin, site layout breaks or content disappears
- Root cause: Naive search/replace corrupted serialized data or builder JSON blobs.
- Fix: Restore backup; use WP-CLI
search-replacewhich handles serialization; re-run carefully with dry-run first. - Symptom: Images load, but browser still warns about mixed content
- Root cause: An iframe, script, font, or XHR is still HTTP. Images are just the usual suspect, not the only one.
- Fix: Check DevTools Network filtered by “blocked” or “mixed content”; scan page source and CSS for
http://. - Symptom: You enabled HSTS and now parts of the site are broken
- Root cause: Some subresources (subdomains, third-party content) still require HTTP; HSTS forces HTTPS and reveals gaps brutally.
- Fix: Roll back HSTS (short max-age), fix all dependencies, then re-enable gradually. Don’t preload until you are certain.
- Symptom: WooCommerce checkout fails or payment iframe won’t load
- Root cause: Payment provider assets requested over HTTP, or callback endpoints mis-schemed behind proxy.
- Fix: Ensure provider URLs are HTTPS; confirm proxy headers; validate the checkout page in DevTools and provider configuration.
Checklists / step-by-step plan (do it once, do it right)
Checklist A: Single-site WordPress, direct TLS on the web server
- Confirm HTTP → HTTPS redirect works for
/and a few deep paths (category, post, asset). - Confirm certificate is valid and chain is correct (browser check + curl headers).
- Set
homeandsiteurlto HTTPS via WP-CLI. - Run WP-CLI
search-replace(dry-run, then real) for your domain. - Grep theme/plugin directories for
http://; fix hardcoded assets. - Purge page cache plugin and regenerate minified assets.
- Fetch top templates and grep for
http://in rendered HTML. - Verify in browser DevTools: no mixed content in Console; no blocked requests in Network.
Checklist B: WordPress behind reverse proxy / load balancer / CDN
- Confirm edge redirects HTTP → HTTPS consistently.
- Confirm edge sends correct forwarded headers (
X-Forwarded-Proto: httpswhen client uses HTTPS). - Configure backend web server and/or WordPress to trust proxy headers only from known proxy IP ranges.
- Set
homeandsiteurlto HTTPS. - Run serialization-safe DB replacement.
- Purge CDN caches after content changes (not before).
- Verify the origin isn’t generating HTTP in canonical/meta tags for uncached pages.
- Only then consider enabling HSTS with a short max-age, then extend.
Checklist C: Migration scenario (domain or scheme changes)
- Backup DB and uploads. No backup, no changes. That’s not dogma, it’s survival.
- Stage the change in a staging environment with a copy of production data.
- Run WP-CLI dry-run search/replace; review replacement counts by table/column.
- Run the real replacement; verify top pages and critical flows (login, checkout, forms).
- Cut over DNS / edge config; verify redirects and canonical tags.
- Purge caches and warm the CDN with a small crawl of important pages.
- Monitor error logs and client-side console errors via RUM if available.
Prevention: stop mixed content from coming back
Set the right invariants
- Everything is HTTPS (including uploads, API calls, fonts, analytics, embeds).
- One canonical host (www vs apex, pick one and redirect the other).
- One canonical scheme (HTTPS, always).
Use Content Security Policy (CSP) as a guardrail, not a bandage
CSP can help you catch regressions by reporting blocked mixed content or disallowed sources. But CSP doesn’t magically fix broken URLs stored in your database. It just makes the failure louder and more deterministic.
If you deploy CSP, start with report-only mode, watch what breaks, then tighten. Treat it like a change-management process, not a one-liner.
HSTS: powerful, dangerous when you’re sloppy
HSTS tells browsers: “Always use HTTPS for this domain.” It’s good. It also means any lingering HTTP-only dependency becomes a hard failure. Roll it out in phases: short max-age, verify, then increase. Avoid preloading until your house is clean and stays clean.
Operational quote
Hope is not a strategy.
— James Cameron
Joke #2: The browser padlock is basically your security auditor, except it works weekends and never accepts donuts.
FAQ
Why do I still get mixed content after changing WordPress Address and Site Address?
Because those settings change what WordPress generates going forward, not what you already stored. Old posts, widgets, builder data, and theme options can still contain absolute HTTP URLs. Run a serialization-safe search/replace and scan theme/plugin code.
Can I “fix mixed content” by using a plugin that forces HTTPS?
Sometimes it hides the symptoms by rewriting output. It rarely fixes the root causes. Use it only as a temporary mitigation while you clean the database and code. Otherwise you’ll ship a permanent hack that fails during caching, minification, or plugin updates.
What’s the safest way to replace HTTP with HTTPS in the database?
WP-CLI wp search-replace with a dry-run first. It handles serialized data correctly. Always take a DB backup before the real run, especially on sites using page builders.
Why does mixed content show up only in Chrome, not in Firefox (or vice versa)?
Browsers differ in what they block vs warn, and they differ in caching behavior and DevTools reporting. Don’t argue with the browser; use DevTools Network/Console to identify the exact URL and fix it at the source.
I’m behind Cloudflare (or another CDN). Why does WordPress think it’s HTTP?
Because the origin connection may be HTTP, and forwarded headers may not be set or trusted. Ensure the CDN sends X-Forwarded-Proto: https and configure your origin/WordPress to treat the request as HTTPS when appropriate.
Is it okay to leave “passive” mixed content like images?
No. It trains users to ignore security signals and can still leak user behavior through request metadata. Also, browsers increasingly tighten rules. Fix it now while it’s a warning, not a broken page.
Will enabling HSTS fix mixed content?
No. HSTS forces HTTPS for top-level navigation to your domain. It doesn’t rewrite third-party URLs, and it won’t fix HTTP links embedded in HTML, CSS, or scripts. It can make failures more obvious, which is helpful after you’ve cleaned up.
Why does WooCommerce show mixed content specifically on checkout?
Checkout pages often include payment provider scripts, iframes, and API calls. One HTTP endpoint is enough to trigger warnings or blocks. Check DevTools for third-party URLs and ensure your proxy headers and site URL settings are correct.
How do I know I fixed it everywhere, not just one page?
Test representative templates: homepage, blog post, category, search, login, cart/checkout, and a few landing pages built with your builder. Programmatically fetch and grep for http:// in HTML, and use browser DevTools to catch runtime requests.
Next steps you can ship today
- Pick one failing page and identify the exact HTTP resource in DevTools.
- Verify
homeandsiteurlare HTTPS; fix if not. - Run WP-CLI
search-replace(dry-run, then real) for your domain. - Scan theme/plugin code and CSS for hardcoded
http://and remove it. - Purge all cache layers (page cache + CDN) after changes.
- Only after the site is clean, roll out HSTS gradually to prevent backsliding.
If you do those in order, mixed content stops being a spooky browser mood swing and turns into a straightforward engineering cleanup. Which is what it always was.