Mixed content is a page loaded over HTTPS that requests some of its own resources over HTTP. The certificate is fine, the page is encrypted, and the browser still marks it as not fully secure, because part of what you are seeing arrived unprotected.
This is the step almost every site skips after installing a certificate, and it is why so many sites have a valid certificate and a broken padlock.
Why it matters more than the padlock
Browsers treat the two kinds differently, and the difference is the whole story.
Passive mixed content (images, video, audio) is loaded anyway, with the padlock downgraded to a warning. Annoying rather than dangerous.
Active mixed content: scripts, stylesheets, iframes, fonts, is blocked outright. A script arriving over HTTP could rewrite the entire page, so browsers refuse it.
That is why mixed content often shows up as a broken feature in place of a security notice. A slider that stopped working, a form that will not submit, a map that renders as a blank box. Those are blocked scripts, and the padlock is a secondary symptom.
Find them with the console
Open the page, open your browser's developer console, and reload. Mixed content warnings name every offending URL exactly.
That list tells you where the problem lives. A URL pointing at your own domain with http:// is stored content or theme code. A URL pointing at some other host means a third-party resource that either supports HTTPS or does not.
Check several pages, not just the homepage. Old landing pages and archived posts are where insecure URLs survive, precisely because nobody looks at them.
Fix stored URLs properly
On a content-managed site, most insecure URLs are in the database, years of posts and pages written when the site was HTTP.
Do not fix this with a plain SQL UPDATE. WordPress and similar systems store some settings as serialized data, where the length of each string is recorded alongside it. Replacing http:// with https:// changes the text without changing the recorded length, and those rows become unreadable: typically breaking theme settings and widget configuration in a way that is difficult to trace back to this moment.
Use a search-and-replace tool that understands serialized data. Take a database export first regardless. Managing MySQL databases with phpMyAdmin explains exporting.
Fix theme and plugin code
Hard-coded URLs in template files need editing directly. Do it in a child theme so the next update does not overwrite your change.
Where a resource is available over both protocols, the cleanest form is a relative URL. It follows whatever the page is using:
<!-- Fragile --> <script src="http://example.com/script.js"></script> <!-- Explicit and safe --> <script src="https://example.com/script.js"></script> <!-- Relative to your own site --> <img src="/images/logo.png">
For resources on your own domain, a path starting with / is best: it works over either protocol and survives a domain change.
Third-party resources that will not do HTTPS
If an external service only offers HTTP, there is no way to load it securely. Three options, in order of preference.
Check whether they now support HTTPS. Many do and simply never updated their documentation.
Host the file yourself if licensing allows. A font, a small script or an image can be copied to your own server and served over your own certificate.
Replace the service. If it cannot do HTTPS in the current decade, that tells you something about how it is maintained.
Do not use upgrade-insecure-requests as the fix
There is a header that tells browsers to silently retry insecure requests over HTTPS:
Content-Security-Policy: upgrade-insecure-requests
It makes the warnings disappear, which is exactly why it is tempting and why it is a poor primary fix. It only works when the resource is also available over HTTPS, if it is not, the request fails and the resource silently does not load.
Use it as a safety net after fixing the real URLs, not instead of fixing them.
Verify properly
Check in a private window with a cleared cache. Your browser has been caching these resources and will happily show you a working page while everyone else gets a broken one.
Then walk through the site rather than checking one page: homepage, a post, an archive, any form, and checkout if there is one. Mixed content hides in the pages you visit least.
Related failures
If the padlock shows a warning triangle, that is mixed content. If the browser shows a full certificate warning instead, that is a different problem: the certificate itself, and the SSL troubleshooting guide explains it.
And if plain HTTP still works alongside HTTPS, mixed content is only half the job. Redirect HTTP so nobody stays on the insecure version, forcing HTTPS.
Mixed content is one of four things a migration has to settle, and the others are less visible. For that, see How to Move a Site to HTTPS Without Losing Rankings.
Find every instance without opening every page
Checking pages by hand in the browser finds the ones you thought to check. The stored content is where the rest are.
grep -rn 'http://' ~/public_html --include='*.css' --include='*.js' --include='*.php' \ | grep -v 'http://www.w3.org' | head -20 wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%http://example.com%';" wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%http://example.com%' LIMIT 20;"
The namespace exclusion in the first command matters, since that address appears inside every SVG file and is not a resource being loaded.
Options and widget content are the usual hiding place, because they are not covered by a check that looks only at posts. Anything a plugin stored as a setting keeps its original address until something changes it deliberately.
The reference that browsers block outright
Not all insecure content behaves the same way, and the difference decides how visible the fault is.
An image or a stylesheet is typically loaded with a warning. A script, a stylesheet fetched by script, a frame, or a request made in the background is blocked entirely, and the feature that depended on it simply does not work.
curl -s https://example.com/ | grep -oE '<(script|iframe|link)[^>]*http://[^"]*' | head
This is why a page can look correct and still be broken. The visible parts loaded with a warning and the invisible one was refused, so a form does not submit or a map does not appear, and nothing on the page explains why.
Stop it coming back
Mixed content returns, because it is reintroduced by the same routes every time.
The three sources are an editor pasting an old address, a plugin that stores an absolute address in its settings, and content imported from a backup taken before the change.
Content-Security-Policy-Report-Only: default-src https:; report-uri /csp-report
Serving that header in report only mode gives you a record of insecure requests without changing what visitors experience. It is the difference between finding out from a report and finding out from a customer.
Add a check to whatever routine you already run on the site, so a new instance is caught within days rather than at the next audit. A yearly website maintenance checklist deals with the routine.