Docs

Fix mixed content

Fix mixed content

Mixed content is an https:// page that loads some of its parts over plain http://: an image, a script, a stylesheet, a font or an embedded frame. The page itself is encrypted, but those parts are not, so anyone on the network could see or swap them. Browsers react on their own: they block scripts, stylesheets and frames outright (a slider stops working, a form loses its styling, a map disappears), try to upgrade images and video to https:// and drop them if that fails, and show the page without the padlock.

It almost always comes from addresses written into content or code before the site moved to HTTPS. The site checklist lists how many files it found and gives a couple of examples.

Where it comes from

Source Typical example
content in the database images inserted into posts years ago as http://example.com/wp-content/uploads/…
theme or template a logo or script with the full http:// address written in the code
page builder Elementor, Divi or WPBakery storing absolute URLs and generated CSS
embeds and third-party code an old widget, map, font or counter included as http://…
the application a framework behind a proxy that thinks the request came over HTTP and builds http:// links

Links (<a href="http://…">) are not mixed content: they do not load anything into the page.

Check it yourself

The quickest way is the browser. Open the page, press F12 (or Cmd+Option+I on a Mac), switch to the Console tab and reload. Every problem shows up as a line like:

Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure image 'http://example.com/logo.png'.

To list them from the command line (replace example.com with your page):

curl -s https://example.com | grep -oiE '<(img|script|iframe|source|video|audio|embed)[^>]+src="http://[^"]+"|<link[^>]+href="http://[^"]+"' | sort -u

Fixed: the console shows no Mixed Content lines, and the command prints nothing. Check a few different page types (home, a post, a product); the problem is often only in older content.

How to fix

The real fix is to change the http:// addresses to https:// where they are stored. First make sure each file actually exists on https:// (open the address with https:// in the browser). Then pick what you have access to.

WordPress

  1. In Settings → General, both WordPress Address (URL) and Site Address (URL) must start with https://.
  2. Replace old addresses in the database. Take a backup first. Use a tool that understands WordPress's serialized data; a plain SQL REPLACE corrupts widgets and page builder settings.

With WP-CLI, do a dry run first, then the real one:

wp db export before-https.sql
wp search-replace 'http://example.com' 'https://example.com' --all-tables --dry-run
wp search-replace 'http://example.com' 'https://example.com' --all-tables

Without the command line, use the free Better Search Replace plugin (Tools → Better Search Replace): search for http://example.com, replace with https://example.com, select all tables and run it as a dry run first.

  1. Elementor: Elementor → Tools → Replace URL does the same for Elementor data. Then regenerate its CSS on the General tab of the same screen. Other builders keep generated CSS too; clear it in their settings.
  2. Theme and widgets: search your child theme files, Appearance → Customize → Additional CSS and text or HTML widgets for http://.
  3. Clear every cache (WP Rocket, LiteSpeed Cache, your host's cache, Cloudflare).

As a quick stopgap, the free Really Simple SSL plugin (now Really Simple Security) has a mixed content fixer that rewrites http:// to https:// in the page as it is sent. It hides the symptom; the addresses stay in the database, so do the replacement anyway.

Static HTML

Search all files for addresses that load something over HTTP:

grep -rnE '(src|href|action)="http://|url\(.?http://' --include='*.html' --include='*.css' --include='*.js' .

Skip ordinary links (<a href="http://…">); they are not mixed content. Change the rest to https://. For files on your own domain a root-relative path (/images/logo.png) is even better: it works on any domain and protocol. If a third-party file is not available over HTTPS, download it and serve it yourself, or replace it.

Laravel

Behind a load balancer, Cloudflare or another proxy, Laravel receives the request over HTTP and url(), asset() and route() build http:// addresses. Trust the proxy in bootstrap/app.php, so Laravel reads X-Forwarded-Proto:

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})

Replace '*' with your proxy's IP addresses if they are fixed. Also set APP_URL=https://example.com in .env, since queued jobs and e-mails use it. Then look for hard-coded http:// in your Blade views and stored content.

Cloudflare

Go to SSL/TLS → Edge Certificates and turn on Automatic HTTPS Rewrites. Cloudflare then rewrites http:// addresses in the HTML it passes on to https:// for resources it knows work over HTTPS, including your own domain.

It only helps on proxied traffic (orange cloud) and cannot fix a resource that has no HTTPS version. The addresses stay in your content, so fix them at the source as well.

nginx

As a stopgap while you clean up the content, the upgrade-insecure-requests directive tells the browser to fetch every http:// resource of the page over https:// instead. In the HTTPS server block (/etc/nginx/sites-available/example.com on Debian and Ubuntu, /etc/nginx/conf.d/example.com.conf on Rocky, Alma and CentOS):

add_header Content-Security-Policy "upgrade-insecure-requests" always;
sudo nginx -t && sudo systemctl reload nginx

If you already send a Content-Security-Policy header, add upgrade-insecure-requests; to it instead of sending a second one.

Apache and LiteSpeed

As a stopgap, the same upgrade-insecure-requests directive, in the site's virtual host or in .htaccess in the site's root (it works on LiteSpeed too):

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "upgrade-insecure-requests"
</IfModule>

With server access, reload afterwards: sudo apachectl configtest && sudo systemctl reload apache2 (httpd on Rocky, Alma and CentOS).

Note: upgrade-insecure-requests and Cloudflare's rewrites make the browser load the files over HTTPS, but the http:// addresses are still in the page, so the checklist keeps listing them until they are changed at the source. A file that does not exist on https:// stops loading instead of loading insecurely.

Hosted platforms

Shopify, Wix, Squarespace and Webflow serve their own files over HTTPS. Mixed content there comes from what was added by hand: a custom code or embed block, a script pasted into the header, an image inserted by its address, or theme code (in Shopify: Online Store → Themes → ⋯ → Edit code). Find it with the browser console and change the address in that block to https://.

Confirm the fix

Reload the page with the browser console open and run the curl command from Check it yourself again, then open the target in PulseKeeper and click Check now on the Checklist tab.