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 URLsReferencesPages affected
Images in old posts (own domain, absolute URLs)1,840610
Images hot-linked from other hosts312140
Embeds (old video and slide players)4138
Theme: a font stylesheet and a social button script2all
_config.yml url: http://… (feeds, canonicals, OG tags)1 settingall
Number of insecure resource references by source Horizontal bars. Images on the site's own domain with absolute http URLs 1,840. Images hot-linked from other hosts 312. Old embeds 41. Theme resources loaded on every page 2. The site URL setting affecting every page's feeds, canonicals and social tags 1. Insecure references by source Own images (absolute http) 1840 Hot-linked images 312 Old embeds 41 Theme resources 2 Site url setting 1
Counted in the built output; the two theme references and the config setting affected every page despite their small counts.

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: ![](http://blog.example.com/assets/2014/diagram.png). 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.

Decision tree for fixing an insecure resource reference The root asks where the resource lives. On the site's own domain, make the URL root-relative. On another host that supports HTTPS, rewrite the URL to https. On a host without HTTPS or that no longer exists, download the resource from an archive into the repository if licensing allows. For an embed whose provider is gone, replace it with a link or a screenshot. Fixing each insecure reference Where does the resource live? Own domain Root-relative URL protocol-independent Other host with HTTPS Rewrite to https verify it loads Host without HTTPS Self-host a copy from an archive Dead embed provider Link or screenshot remove the iframe
upgrade-insecure-requests would have hidden the last two branches rather than fixing them.

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.

Browser behaviour for each kind of insecure reference on the HTTPS site A matrix of four kinds of reference. Images from hosts with HTTPS were auto-upgraded and displayed, so readers saw nothing wrong. Images from hosts without HTTPS were auto-upgraded, failed, and showed as broken images. Iframes and scripts over HTTP were blocked entirely, leaving empty boxes or missing features. The http site URL in feeds and canonicals caused no visible problem but sent search engines and feed readers to HTTP URLs. What browsers did with each insecure reference Reference Browser behaviour What readers saw Image on HTTPS-capable host auto-upgraded nothing wrong Image on HTTP-only host upgrade fails broken image Iframe or script over HTTP blocked empty box, missing feature http site URL in config not fetched by page wrong canonicals, feeds
The most visible failures were the smallest categories; the largest category was invisible to readers.

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

MeasureBeforeAfterTool
Insecure resource references in build2,1960CI scan
Pages with mixed content console warnings7900Playwright crawl of console
Broken embeds (empty boxes)410 (22 updated, 19 replaced)Manual review
Canonical and OG URLs on HTTPAll pagesNoneBuild output
External hosts the blog depends on for images379 (all HTTPS)Scan

Pitfalls & Rollback

  • Relying on upgrade-insecure-requests alone. 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.

Filed underFixes