Fixing Mixed Content Warnings After Moving to HTTPS
A static site that has existed for a decade usually started on HTTP. Moving to HTTPS is easy — a certificate on the host and a redirect — and then the browser console lights up with "Mixed Content: The page at 'https://…' was loaded over HTTPS, but requested an insecure image 'http://…'". Some images still load because browsers upgrade them; a few do not, because their hosts never supported HTTPS; one old embed is blocked entirely and leaves an empty box. The pages are served securely, but the content inside them still points at the HTTP web.
This guide cleans up mixed content on a 2,600-post engineering blog built with Jekyll, after its move to HTTPS on GitHub Pages: finding every http:// reference in content, templates, data and generated output, fixing each at its source, and keeping new ones out. It belongs to Security Headers and Hardening for Static Sites; the HTTPS move itself is covered in Custom Domains and HTTPS for Jekyll on GitHub Pages.
Step 1: Find Every Insecure Reference in the Build
The built site is the place to look, because it includes what templates, plugins and data add:
bundle exec jekyll build
grep -rhoE '(src|href|srcset|data-src|poster|action)="http://[^"]+"|url\(http://[^)]+\)' _site \
| sed -E 's/.*http:\/\/([^/"]+).*/\1/' | sort | uniq -c | sort -rn | head -30
Only references that load resources matter for mixed content — src, srcset, poster, CSS url(), action for forms. Plain <a href="http://…"> links to other sites are not mixed content (navigating to an HTTP page is allowed), though they are worth upgrading where the target supports HTTPS.
Source of http:// resource URLs | References | Pages affected |
|---|---|---|
| Images in old posts (own domain, absolute URLs) | 1,840 | 610 |
| Images hot-linked from other hosts | 312 | 140 |
| Embeds (old video and slide players) | 41 | 38 |
| Theme: a font stylesheet and a social button script | 2 | all |
_config.yml url: http://… (feeds, canonicals, OG tags) | 1 setting | all |
Fix 1: The Site URL and the Theme
The highest-impact fixes were the smallest. _config.yml had url: http://blog.example.com, which jekyll-feed, jekyll-seo-tag and the sitemap used for absolute URLs — every canonical link, Open Graph image and feed item pointed at HTTP. Changing it to https:// fixed all of them at once.
The theme's head.html loaded a web font stylesheet over HTTP (blocked as active content, so the font had silently fallen back for every HTTPS visitor) and a social sharing script, also HTTP and blocked. The font was self-hosted; the sharing script was removed and replaced with plain share links that need no JavaScript.
Fix 2: The Site's Own Images
1,840 image references in old posts used absolute URLs to the site's own domain, from an old editor that inserted them that way: . Making them root-relative fixes them regardless of protocol or domain:
grep -rlE 'https?://blog\.example\.com/' _posts | xargs sed -i -E 's#https?://blog\.example\.com/#/#g'
A review of the diff confirmed nothing else matched the pattern. Relative URLs also made the content portable to a staging host.
Fix 3: Hot-Linked Images
312 images were loaded from other sites, mostly over HTTP. A script checked each host:
for host in $(grep -rhoE 'src="http://[^/"]+' _site | cut -d/ -f3 | sort -u); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "https://$host/")
echo "$code $host"
done
- Host supports HTTPS (61% of references): URL rewritten to
https://. - Host has no HTTPS or no longer exists (39%): the image was found in the Wayback Machine or the author's archive, downloaded into the repository, and linked locally — 74 images recovered, 48 lost and replaced with a note.
Hot-linking also meant the blog depended on 37 other sites staying up; downloading the images removed that dependency.
Fix 4: Old Embeds
41 iframes and scripts embedded videos and slide decks from services that had changed URL schemes or shut down. Iframes over HTTP are blocked as active content, so these had been empty boxes on HTTPS. Twenty-two had current HTTPS embed URLs and were updated, using a facade pattern for performance as in Lazy-Loading YouTube Embeds on Static Sites. Nineteen were from defunct services and were replaced with a link to an archived copy or a static screenshot with a caption.
What Browsers Did With Each Kind
Understanding what readers actually saw helped prioritise. Browsers treat the categories differently, and the console warnings did not reflect how visible each problem was.
The 1,840 own-domain images were the largest group but caused no visible breakage, because browsers upgraded them to the site's own HTTPS host. The 41 embeds and two theme resources were few but each one broke something on every affected page. Fixing in order of visibility — theme and config first, then embeds, then dead hot-linked images, then the bulk rewrite — meant readers saw improvements on day one rather than at the end.
Step 2: A Safety Net in the Browser
With the content fixed, the site sends a Content Security Policy that includes upgrade-insecure-requests, so any reference that slips through is requested over HTTPS rather than loaded insecurely or blocked. GitHub Pages cannot set headers, so the directive goes in a meta tag in the layout:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
It is a safety net, not a fix: the CI check below is what keeps the source clean.
Step 3: Stop New Mixed Content in CI
The scan from Step 1 runs on every pull request against the built _site, failing on any http:// in a resource attribute or CSS url(), with a small allow-list for XML namespace URIs (http://www.w3.org/2000/svg) and similar identifiers that are not fetched:
if grep -rnoE '(src|srcset|poster|data-src|action)="http://|url\(http://' _site --include='*.html' --include='*.css' \
| grep -vE 'w3\.org|xmlns' ; then echo "Insecure resource URLs found"; exit 1; fi
It caught three new hot-linked images from guest posts in the following two months.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Insecure resource references in build | 2,196 | 0 | CI scan |
| Pages with mixed content console warnings | 790 | 0 | Playwright crawl of console |
| Broken embeds (empty boxes) | 41 | 0 (22 updated, 19 replaced) | Manual review |
| Canonical and OG URLs on HTTP | All pages | None | Build output |
| External hosts the blog depends on for images | 37 | 9 (all HTTPS) | Scan |
Pitfalls & Rollback
- Relying on
upgrade-insecure-requestsalone. It hides references to hosts without HTTPS. - Fixing posts but not the site URL. Feeds, canonicals and social tags keep using HTTP.
- Ignoring CSS.
url(http://…)in stylesheets is mixed content too. - Treating links as mixed content. Only resources the page loads count; links are optional upgrades.
- Rollback: the content rewrites are commits that can be reverted; the meta tag can be removed independently.
Conclusion
Mixed content after moving a static site to HTTPS comes from absolute URLs in old content, hot-linked resources, dead embeds, theme includes and an http:// site URL in configuration. Scanning the built output instead of the source finds all of them; fixing each at its source — relative URLs, HTTPS rewrites, self-hosted copies and replaced embeds — cleared 2,196 references, and a CI scan keeps new ones out.
FAQ
What is mixed content?
An HTTPS page that loads a resource over plain HTTP. Browsers block active mixed content, such as scripts, stylesheets and iframes, outright. Passive mixed content, such as images, audio and video, is automatically upgraded to HTTPS by current browsers, and blocked if the upgraded request fails. Either way, the page loses its secure padlock state or breaks.
Does upgrade-insecure-requests fix mixed content?
It tells browsers to request every http URL on the page over HTTPS instead, which fixes references to hosts that support HTTPS without changing the content. It does not help when the host has no HTTPS, and it hides the problem in the source. Use it as a safety net while fixing the references, not as the fix.
Where do http references hide on a static site?
In old Markdown content with absolute URLs and image links, front matter and data files, theme templates and includes, CSS url values and imports, third-party embed codes pasted years ago, and generated output such as RSS feeds and sitemaps that use a site URL configured with http.
How do I keep new mixed content from appearing?
Scan the built HTML, CSS and feeds for http URLs in CI and fail on any outside an allow-list, and send a Content-Security-Policy with upgrade-insecure-requests or block-all-mixed-content style directives so browsers protect readers if one slips through. Report-only CSP reports also surface mixed content in the field.
Related
- Parent: Security Headers and Hardening for Static Sites — hardening a static site.
- Custom Domains and HTTPS for Jekyll on GitHub Pages — the HTTPS move that preceded this.
- Enabling HSTS and Preload Safely — the next step once content is clean.
- Writing a Content Security Policy for a Static Site — a full policy beyond the upgrade directive.
- Validating HTML Output in CI — other checks on the built output.
Filed underFixes