Your site moved to HTTPS years ago, but the old http:// URLs are still in there: in blog posts, in image tags, in a footer someone hard-coded. Some of them cause browser warnings, some get blocked outright, and some just add a redirect hop. Here's how to find them and what to do with each kind.
Two kinds of mixed content
Active mixed content is anything that can change the page: scripts, stylesheets, iframes, fetch requests. Browsers block these on HTTPS pages, so a script loaded over http:// simply doesn't run. The result is a broken feature with no visible error.
Passive mixed content is images, audio and video. Modern browsers try to upgrade these to HTTPS automatically. If the HTTPS version doesn't exist, the image breaks.
Plain <a href="http://..."> links aren't mixed content (they're navigation), but on your own domain they still trigger an extra redirect for every click, and on other domains they may point to sites that no longer serve HTTP at all.
Find it in the browser
Open DevTools, then the Console. Mixed content shows up as warnings like:
Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure image 'http://example.com/img/logo.png'.
Good for one page; not practical for a whole site.
Find it with grep in your source
If your content is in files:
grep -rnoE 'http://[^"'"'"' )>]+' --include='*.html' --include='*.md' --include='*.php' . \
| grep -v 'http://www.w3.org' | sort | uniq
The w3.org filter removes XML namespace strings, which look like URLs but aren't requests.
Find it in a WordPress database
Old posts store absolute URLs in the content:
SELECT ID, post_title
FROM wp_posts
WHERE post_status = 'publish'
AND post_content LIKE '%src="http://%';
To replace your own domain safely, use WP-CLI, which handles serialized data correctly:
wp search-replace 'http://example.com' 'https://example.com' --dry-run
wp search-replace 'http://example.com' 'https://example.com'
Always run the dry run first and take a database backup.
Crawl the rendered pages
Source searches miss URLs built by templates, plugins and themes. A small crawler over the rendered HTML catches those:
import requests, re
from urllib.parse import urljoin
def insecure_resources(url):
html = requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"}).text
found = set()
for attr, val in re.findall(r'(src|href|srcset|data-src)=["\']([^"\']+)', html):
for part in val.split(","):
u = part.strip().split(" ")[0]
if u.startswith("http://"):
found.add((attr, u))
return found
for page in ["https://example.com/", "https://example.com/blog/"]:
for attr, u in insecure_resources(page):
print(page, attr, u)
Feed it the URLs from your sitemap to cover every page.
What to do with each finding
| Finding | Action |
|---|---|
| Your own domain over http | Change to https (or a root-relative path) |
| Third-party resource that supports https | Change to https |
| Third-party resource with no https | Host it yourself, replace it, or remove it |
| Plain link to an external http site | Check it still works; update to https if available |
Don't switch to protocol-relative URLs (//example.com/...). They were a workaround for the HTTP era and just add ambiguity now.
Add a safety net
The upgrade-insecure-requests directive tells browsers to fetch every http:// resource over HTTPS:
Content-Security-Policy: upgrade-insecure-requests
It hides the warnings, but if the HTTPS version of a resource doesn't exist, it still breaks. Use it as a backstop, not a fix.
Keep it fixed
New mixed content comes back with every pasted embed and copied snippet. A scheduled crawl that checks every link and image on the site catches regressions; linkbruno.com is built for that kind of ongoing check, alerting when a link or image breaks.
Related reading
- Check links in Markdown docs with a 60-line Python script
- Too many broken links? How to decide which to fix first
- Why links in old email newsletters break, and how to prevent it
- Trailing slashes: the small URL detail that creates redirects
- How to check mailto: and tel: links before they cost you leads
Top comments (0)