Fixing Mixed Content Warnings — Finishing the HTTPS Migration You Started
You got the certificate, set up the redirect, and your site loads over HTTPS. Then the padlock refuses to cooperate — a warning triangle, a "Not fully secure" label, or a page where the stylesheet simply didn't load. That's mixed content: an HTTPS page pulling some of its resources over plain HTTP. It's the most common way an HTTPS migration ends up 95% done — and the last 5% is the part your visitors actually see.
The fix is rarely hard. It's mostly an inventory problem: finding every hardcoded http:// reference in your templates, your database, and your third-party embeds, and keeping new ones from creeping back in.
Active vs passive: browsers don't treat all mixed content equally
Browsers split mixed content into two classes, and the difference determines whether your page degrades or breaks outright:
- Active (blockable) mixed content — scripts, stylesheets, iframes, XHR/fetch requests, fonts. These can read or rewrite the page, so an attacker who intercepts one plain-HTTP script owns your entire HTTPS page. Every modern browser blocks these outright. If your CSS or JS is referenced over
http://, it does not load, period — which is why a "small" mixed content issue can render your site unstyled or non-functional. - Passive (optionally-blockable) mixed content — images, audio, video. These can't script the page, so browsers have historically loaded them while downgrading the padlock. That grace period is ending: Chrome now attempts to auto-upgrade passive mixed content to HTTPS and blocks it if the upgrade fails, rather than loading it insecurely. In other words, an
http://image on an HTTPS page either works over HTTPS anyway or shows up broken.
The practical takeaway in mid-2021: treat all mixed content as broken content. The days of "it's just an image, the padlock looks a bit sad" are effectively over.
Finding it: DevTools first, then crawl
For a single page, the browser tells you everything. Open DevTools, load the page, and check the Console — mixed content produces explicit warnings:
Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure script 'http://cdn.example.com/app.js'.
This request has been blocked; the content must be served over HTTPS.
The Security panel (Chrome) lists every non-secure origin the page touched. That handles the pages you think to check — but mixed content hides in the pages you don't: old blog posts, category archives, that landing page marketing built in 2018.
For whole-site coverage, crawl it. A blunt but effective approach with wget and grep:
wget --spider -r -l 5 --no-parent https://example.com 2>&1 \
| grep -B 2 'http://' | less
Or go to the source and search your codebase and database directly:
# templates, config, hand-written HTML
grep -rn 'src="http://' --include='*.php' --include='*.html' .
grep -rn 'href="http://' --include='*.css' .
On a CMS, the database is usually the bigger offender — years of post content with absolute http:// image URLs pasted in. WordPress users have search-replace tooling for exactly this; whatever your stack, remember that serialized data (widget configs, page-builder blobs) can break if you replace strings naively, so use a tool that understands it or test on a copy first.
Fixing the usual suspects
Once you have the inventory, the fixes are mechanical:
- Hardcoded URLs in templates. Change
http://tohttps://— or better, make them root-relative (/assets/app.css) so the scheme is inherited and this class of bug can't recur. - Database content. Run a search-replace from
http://yourdomain.comtohttps://yourdomain.com. Old post content is where mixed content goes to hide. - Third-party embeds and widgets. That analytics snippet, chat widget, or ad tag from 2016 may still point at an
http://endpoint. Most vendors have HTTPS endpoints now — update the snippet. If a vendor genuinely can't serve HTTPS in 2021, that's a strong signal to replace the vendor. - Your CDN and asset host. Confirm the CDN hostname serves HTTPS with a valid certificate, and that your CMS/framework is configured with an
https://base URL so generated markup comes out right.
The protocol-relative trap
You'll find old advice recommending protocol-relative URLs — //cdn.example.com/app.js — so the resource inherits the page's scheme. It technically prevents mixed content, but it's an anachronism with real pitfalls: on any HTTP context (local dev, an email client rendering your markup, an old HTTP page you forgot) it silently requests the insecure version, and it makes your intent invisible in a grep. There is no remaining upside. Just write https:// — every CDN worth using serves it, and HTTPS resources work fine even on HTTP pages.
The safety net: upgrade-insecure-requests
Content-Security-Policy has a directive built for exactly this migration:
add_header Content-Security-Policy "upgrade-insecure-requests" always;
With that header (or the equivalent <meta> tag), the browser rewrites every http:// subresource request on your pages to https:// before it leaves the machine — first-party and third-party alike. It's the difference between hunting down 400 old image URLs tonight and shipping a one-line header now.
Two honest caveats:
- It's a band-aid, not a fix. The insecure URLs are still in your content, invisible until someone views the page in a client that doesn't support the directive — or until you restructure and the header gets lost. Ship the header and fix the sources.
- Upgraded requests can fail. If a resource host doesn't actually serve HTTPS, the upgraded request breaks where the insecure one would have "worked." That's the correct outcome — but test before you deploy.
For ongoing visibility, CSP reporting can tell you about violations from real users' browsers:
add_header Content-Security-Policy-Report-Only
"default-src https:; report-uri /csp-report" always;
Run it in report-only mode for a couple of weeks and you'll get a map of every insecure request your real traffic triggers — including the pages your crawl missed.
Mixed content is a regression, not an event
Here's the part most migration checklists skip: mixed content isn't something you fix once. Every new blog post, every marketing landing page, every third-party snippet someone pastes in is a fresh chance to reintroduce it. The teams that stay clean treat it like any other regression class — a header that must stay present, pages that must keep rendering, checks that run continuously rather than the week of the migration.
That's where monitoring earns its keep. CompleteStatus checks your pages on a schedule from the outside — the way a visitor's browser sees them — so a stylesheet that stops loading or a security header that quietly disappears after a config change becomes an alert instead of a support ticket. Keyword assertions catch pages that stop rendering correctly, and security checks watch the headers you set today so they're still set next quarter. See what's included on the features page, or create a free account and point a check at the page you just fixed — future-you will appreciate knowing the moment it breaks again.