Je hebt een SSL-certificaat geïnstalleerd, Instellingen > Algemeen bijgewerkt naar https:// en verwachtte een groen slotje. In plaats daarvan toont de browser een kapot of open slotje, of een label "Niet veilig" naast de URL, terwijl het certificaat zelf prima in orde is. Dat is mixed content: de pagina wordt geserveerd over HTTPS, maar minstens één resource binnen de pagina wordt nog geladen over plain HTTP.
Wat mixed content echt betekent
Mixed content is een HTTPS-pagina die een subresource over HTTP binnenhaalt. Het certificaat op het hoofddocument is prima. De browser vertelt je dat een deel van wat je net geladen hebt ongecodeerd binnenkwam, waardoor de pagina als geheel niet meer als privé vertrouwd kan worden.
Browsers splitsen mixed content in twee bakken:
- Upgradable content:
<img>,<audio>,<video>en CSS background images. Moderne browsers herschrijven deze stilletjes naar HTTPS en laden de upgraded versie als die bestaat. De pagina toont nog steeds "Niet veilig" omdat de bron-HTML de HTTP-URL bevat, ook al lukte de fetch zelf wel. - Blockable content:
<script>,<link>-stylesheets,<iframe>,fetch()enXMLHttpRequesten@font-face. Browsers weigeren deze zonder meer te laden. Een HTTP-script op een HTTPS-pagina draait gewoon niet.
De uitrol bij Chrome ging in fases, niet in één klap. Chrome 80 in februari 2020 begon met het autoupgraden en blokkeren van mixed audio en video. Image-autoupgrade stond oorspronkelijk gepland voor Chrome 81 maar werd uitgesteld tot Chrome 84 in augustus 2020. Vanaf eind 2020 werden dus ook mixed images geblokkeerd als de HTTPS-versie niet bestond. Firefox en Safari landden rond hetzelfde venster op vergelijkbaar gedrag.
Drie dingen die dit níet zijn:
- Het is geen kapot certificaat. Een kapot certificaat geeft een vol rood tussenscherm ("Je verbinding is niet privé"), niet een werkende pagina met een doorgestreept slotje. Als je de pagina ziet laden en alleen het slotje is verkeerd, dan is het certificaat prima. Bron: WP Engine's mixed content guide.
- Het is niet iets wat een SSL-plugin volledig oplost. Plugins als Really Simple SSL bufferen PHP-output en herschrijven HTTP-URL's in de gerenderde HTML op het moment van het verzoek. Ze kunnen geen URL's veranderen die hardcoded in theme-PHP-bestanden staan en raken de database niet aan. Ze kunnen ook niet ingrijpen op resources die een plugin op runtime via JavaScript toevoegt.
- Het is niet iets wat Cloudflare magisch aan de edge afhandelt. Cloudflare's Automatic HTTPS Rewrites herschrijven sommige HTTP-subresources on the fly, maar Cloudflare's eigen docs zeggen dat deze rewrite alles mist wat dynamisch via JavaScript wordt toegevoegd en alles op externe domeinen die geen HTTPS ondersteunen. Op Flexible SSL is het probleem erger: de edge-naar-origin hop is zelf nog HTTP, dus WordPress blijft gewoon HTTP-URL's genereren.
Mixed content is een content-probleem, geen transport-probleem. De HTTP-URL's staan in je WordPress-database opgeslagen, gebakken in theme- en pluginbestanden of door JavaScript op runtime ingevoegd. Tot die bronnen gecorrigeerd zijn, blijft het slotje kapot.
Zo vind je de schuldige resources met DevTools
Voordat je iets oplost, moet je weten wat er exact fout gaat. Ga niet blind URL's herschrijven.
- Open de betreffende pagina in Chrome of Firefox en druk F12 om DevTools te openen.
- Ga naar het tabblad Console.
- Herlaad de pagina met Ctrl+Shift+R (of Cmd+Shift+R op macOS) om een schone fetch af te dwingen.
- Lees de console. Mixed content-fouten zijn onmiskenbaar:
Mixed Content: The page at 'https://yoursite.nl/' was loaded over HTTPS,
but requested an insecure stylesheet 'http://yoursite.nl/wp-content/themes/old/style.css'.
This request has been blocked; the content must be served over HTTPS.
Het woord "blocked" zegt dat het om een script, stylesheet, iframe of font gaat. Het woord "loaded but will be upgraded" wijst op een image of media-element.
- Voor een volledige lijst ga je naar het tabblad Network, herlaad je opnieuw en typ je in het filterveld
scheme:http. Elke rij die verschijnt is een subresource die de pagina nog over HTTP opvraagt. Klik met rechts op een rij en kies Copy > Copy URL om de URL te pakken. - Maak een korte lijst. In de praktijk vind je één of twee unieke hosts (vaak alleen
http://yoursite.nlzelf) die terugkeren bij veel resources, geen wildgroei aan verschillende boosdoeners.
Verwachte uitkomst. Voor een gezonde HTTPS-pagina heeft de console geen enkele mixed content-regel en geeft het Network-filter scheme:http nul rijen. Dat is je verificatiedoel.
Is je console leeg maar oogt het slotje nog verkeerd, kijk dan naar JavaScript-injected resources. Open het Network-tabblad, wis het filter en zoek naar requests die ná de initiële load verschijnen (sorteer op initiator of tijd). Third-party scripts die image-tags of iframes uit een HTTP-string opbouwen zie je pas op runtime.
Fix 1: wp-config.php scheme-alignment (10 seconden, deels)
Zorg eerst dat WordPress zelf weet dat het onder HTTPS draait. De constanten WP_HOME en WP_SITEURL in wp-config.php zijn geen permanente fix, maar wel de snelste manier om te testen of de core-URL's het probleem zijn.
Open wp-config.php in de bestandsbeheerder van je hostingpaneel (of download het bestand via SFTP) en voeg deze regels toe boven de markering /* That's all, stop editing! */:
define( 'WP_HOME', 'https://yoursite.nl' );
define( 'WP_SITEURL', 'https://yoursite.nl' );
Wat deze eigenlijk doen. De officiële wp-config documentatie is expliciet: WP_SITEURL "will not change the database stored value" en "the URL will revert to the old database value if this line is ever removed from wp-config." Deze constanten overschrijven de opties home en siteurl in wp_options op runtime; ze schrijven níet naar de database. Haal de regels weg en de HTTP-URL's zijn meteen terug.
Daarom is dit een halve fix. Je moet de opgeslagen waardes nog steeds herschrijven (Fix 2 of Fix 3), maar door de constanten eerst te zetten weet je of HTTPS op deze site überhaupt werkt, en ruim je meteen de core-gegenereerde URL's op (menu's, wp-login.php-redirects, canonical links).
Controle. Herlaad de front-end en bekijk de paginabron. De tag <link rel="canonical">, de admin-URL in de wp-admin-balk en eventuele <base>-tags moeten allemaal met https:// beginnen. De DevTools-console zou minder mixed content-regels moeten tonen. Toont hij meer, dan heb je het domein verkeerd benoemd of zit er een reverse-proxy-probleem in de weg (zie Fix 5).
Fix 2: WP-CLI search-replace (de echte fix)
Geen SSH-toegang? Ga naar Fix 3: Plugin-alternatief, dat dezelfde operatie vanuit je WordPress-dashboard doet.
Dit is de canonieke fix. Je vertelt WordPress om elke letterlijke http://yoursite.nl in de database te vervangen door https://yoursite.nl, inclusief serialized data. WP-CLI's search-replace-commando stelt het expliciet: "Search/replace intelligently handles PHP serialized data, and does not change primary key values."
Voorwaarden. SSH-toegang tot de server met WP-CLI geïnstalleerd. Heb je geen SSH maar biedt je hoster WP-CLI via een webterminal (gebruikelijk op managed WordPress hosting), dan werkt dat ook. Maak eerst een back-up van de database via de back-uptool van je hostingpaneel of via phpMyAdmin-export. Deze operatie herschrijft rijen over alle tabellen heen; een fout kost je veel.
Draai eerst een dry run zodat je precies ziet wat er zou veranderen:
wp search-replace 'http://yoursite.nl' 'https://yoursite.nl' \
--dry-run \
--skip-columns=guid \
--all-tables
Verwachte uitkomst.
+------------------+-----------------------+--------------+------+
| Table | Column | Replacements | Type |
+------------------+-----------------------+--------------+------+
| wp_options | option_value | 2 | PHP |
| wp_posts | post_content | 147 | SQL |
| wp_postmeta | meta_value | 63 | PHP |
| wp_comments | comment_content | 0 | SQL |
+------------------+-----------------------+--------------+------+
Success: 212 replacements to be made.
De getallen variëren, maar wp_posts.post_content en wp_postmeta.meta_value zijn vrijwel altijd de twee grootste rijen. Zie je overal nul vervangingen, dan staan je opgeslagen URL's al op https:// en zit het probleem ergens anders (themabestanden, hardcoded plugin-assets of externe CDN's).
Draai hem dan echt door de vlag --dry-run weg te laten:
wp search-replace 'http://yoursite.nl' 'https://yoursite.nl' \
--skip-columns=guid \
--all-tables
Waarom --skip-columns=guid. GUID's zijn permanente unieke identifiers voor posts. Ze zien eruit als URL's, maar WordPress gebruikt ze nooit om te routeren. Ze aanpassen kan feedreaders en analytics in de war sturen die op GUID dedupliceren. Laat ze met rust.
Waarom --all-tables. Zonder deze vlag raakt WP-CLI alleen tabellen met de WordPress-prefix aan. Multisite-installs en plugins die hun eigen tabellen maken (WooCommerce, BuddyPress, Yoast) vallen buiten die default-scope. --all-tables pakt ze wel mee.
Controle. Herlaad de front-end, open de DevTools-console en herlaad met Ctrl+Shift+R. De mixed content-waarschuwingen voor yoursite.nl-resources zouden weg moeten zijn. Wat overblijft, zit of in themabestanden hardcoded of verwijst naar een extern domein. Ga verder met Fix 3.
Fix 3: Plugin-alternatief (als je geen SSH hebt)
Is WP-CLI niet beschikbaar, gebruik dan de Better Search Replace-plugin om dezelfde operatie vanuit het dashboard te doen. Het is dezelfde search-replace-logica met dezelfde serialisatie-veilige afhandeling, alleen verpakt in een UI.
- Installeer en activeer Better Search Replace.
- Ga naar Tools > Better Search Replace.
- Vul bij Search for
http://yoursite.nlin. Bij Replace with vul jehttps://yoursite.nlin. Geen trailing slash. - Selecteer elke tabel in de lijst (gebruik Ctrl+A of klik ze stuk voor stuk aan).
- Laat "Replace GUIDs" uit staan. Zelfde reden als hierboven.
- Vink "Run as dry run" aan en klik op Run Search/Replace.
- Lees het rapport. Controleer of het klopt met wat je verwacht (honderden wijzigingen in posts en postmeta, nul of bijna nul in comments).
- Vink "Run as dry run" uit en draai hem nog een keer.
Controle. Zelfde als Fix 2. Herlaad de front-end en kijk in DevTools.
Really Simple SSL is een legitieme plugin, maar geen vervanging voor Fix 2 of Fix 3. Hij buffert PHP-output en herschrijft URL's in gerenderde HTML op runtime, wat betekent dat elk verzoek een runtime-kost draagt en dat de database gewoon HTTP-URL's blijft bevatten. Ik adviseer Better Search Replace als eenmalige operatie boven Really Simple SSL als permanente pleister.
Fix 4: Hardcoded assets in theme- en pluginbestanden
Na Fixes 1 tot en met 3 is de database schoon. Laat DevTools nog steeds mixed content zien voor resources op yoursite.nl (niet een extern domein), dan staan de URL's hardcoded in PHP-bestanden. Dat gebeurt als een developer een letterlijke http://yoursite.nl/...-URL in een themabestand heeft gezet voor een afbeelding of stylesheet, in plaats van home_url() of get_template_directory_uri() te gebruiken.
Doorzoek het actieve thema en eventuele custom plugins op hardcoded HTTP-URL's. De makkelijkste manier zonder SSH is via Weergave > Themabestandseditor in wp-admin: doorzoek elk templatebestand (header.php, footer.php, functions.php) op http://yoursite.nl. Je kunt ook de themamap downloaden via de bestandsbeheerder van je hostingpaneel en lokaal doorzoeken met een teksteditor.
Heb je SSH-toegang:
grep -rn 'http://yoursite.nl' wp-content/themes/your-active-theme/
grep -rn 'http://yoursite.nl' wp-content/plugins/your-custom-plugin/
Verwachte uitkomst.
wp-content/themes/your-active-theme/header.php:42: href="http://yoursite.nl/favicon.ico"
wp-content/themes/your-active-theme/footer.php:18: src="http://yoursite.nl/assets/logo.png"
wp-content/themes/your-active-theme/single.php:89: href="http://yoursite.nl/css/print.css"
Fix elke regel door of http:// te vervangen door https://, of beter, door een WordPress-functie te gebruiken die op runtime het juiste scheme genereert. Vervang bijvoorbeeld een hardcoded favicon-URL door home_url( '/favicon.ico' ) in een esc_url()-wrapper, en een hardcoded thema-asset-URL door get_template_directory_uri() . '/assets/logo.png' in een esc_url()-wrapper. Beide functies geven het juiste scheme (HTTP of HTTPS) op basis van het huidige verzoek.
De tweede vorm is portable: hij werkt ook nog als je de site later naar een staging-domein of een andere host verhuist.
Pas geen thema aan dat je niet zelf hebt gebouwd. Staan de hardcoded URL's in een thema uit een marketplace, maak dan een child theme of overschrijf de template in je eigen code. Directe edits in een parent theme worden bij de eerstvolgende update weggegooid.
Controle. Herlaad en herlaad nog een keer met DevTools open. De specifieke resources die je gevonden hebt moeten nu over HTTPS laden. Faalt het nog steeds, dan bestaat de HTTPS-versie oprecht niet (het bestand is weg, of het domein dat hem serveert doet geen HTTPS) en moet je de asset zelf hosten of een andere kiezen.
Fix 5: Reverse proxy-uitzondering (Cloudflare Flexible, AWS ALB, load balancers)
Draai je achter Cloudflare Flexible SSL, een AWS Application Load Balancer of een andere reverse proxy die HTTPS termineert en daarna HTTP tegen de origin spreekt, dan ziet WordPress een HTTP-verbinding en genereert HTTP-URL's. Geen hoeveelheid database-herschrijven lost dit op: bij het volgende verzoek genereert WordPress de URL's gewoon opnieuw, op basis van wat hij dénkt dat het scheme is.
De WordPress HTTPS admin-gids documenteert het symptoom direct: "WordPress can recognize the HTTP_X_FORWARDED_PROTO header when running behind a reverse proxy to prevent infinite redirect loops." De fix is WordPress vertellen dat de originele verbinding HTTPS was, ook al ziet het PHP-proces zelf HTTP.
Open wp-config.php in de bestandsbeheerder van je hostingpaneel (of via SFTP) en voeg dit blok toe boven de regel /* That's all, stop editing! */ en boven je WP_HOME- en WP_SITEURL-constanten:
if (
isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
&& $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
) {
$_SERVER['HTTPS'] = 'on';
}
Dit draait voordat WordPress is_ssl() aanroept. is_ssl() controleert $_SERVER['HTTPS'] op 'on' of '1'; door die waarde te zetten op basis van de forwarded header, vertel je WordPress wat de client echt gebruikte.
Specifiek over Cloudflare Flexible. Cloudflare's eigen blog is er duidelijk over: Flexible SSL "creates a secure (HTTPS) connection between the website visitor and CloudFlare and then an in-secure (HTTP) connection between CloudFlare and the origin server". Voor elke site met logincredentials, webshop of formulierverzendingen raad ik je aan om te schakelen naar Full of Full (Strict) SSL-modus en een echt certificaat op de origin te installeren. Flexible is een workaround voor wanneer je op de origin helemaal geen certificaat kunt krijgen; het is geen productiehouding voor een WordPress-site.
Controle. Herlaad, kijk in DevTools en controleer dat er geen mixed content-waarschuwingen meer verschijnen. Heb je SSH-toegang, dan kun je ook de response-headers checken vanaf een andere machine:
curl -sI https://yoursite.nl/ | grep -i location
Je zou nergens een Location:-header moeten zien die naar http:// wijst. Zie je die wel, dan forceert iets anders nog een HTTP-redirect (een .htaccess-regel, een nginx-config of een cache-laag).
Kanttekening bij FORCE_SSL_ADMIN
Je ziet FORCE_SSL_ADMIN soms voorgesteld als mixed content-fix. Dat is het niet. FORCE_SSL_ADMIN forceert HTTPS alleen voor logins en het admin-dashboard; hij heeft geen effect op front-end-URL's of subresources. De constante is admin-scoped sinds WordPress 2.6.0. WordPress 4.0 zette de aparte FORCE_SSL_LOGIN-constante op deprecated, maar de scope van FORCE_SSL_ADMIN zelf is daarbij niet veranderd. Zet hem gerust in om wp-admin dicht te timmeren, maar verwacht niet dat hij mixed content op je front-end opruimt.
Einde-tot-einde verificatie
Draai na Fix 2 of Fix 3 (en Fix 4 als dat nodig was, Fix 5 als je achter een proxy zit) deze drie checks op volgorde:
- DevTools-console op een hard reload. Ctrl+Shift+R. Nul mixed content-regels.
- DevTools Network-tab met filter
scheme:http. Nul rijen. - Bekijk de paginabron in de browser. Klik rechts op de pagina, kies "Paginabron bekijken" en zoek met Ctrl+F (of Cmd+F) naar
http://yoursite.nl. Elke match betekent dat iets nog steeds naar het onveilige scheme verwijst. Loop de fixes opnieuw langs. Heb je SSH-toegang, dan kun je deze check automatiseren:
curl -s https://yoursite.nl/ | grep -o 'http://[^"'"'"' >]*' | sort -u
Verwachte uitkomst. Een lege lijst, of een lijst met alleen externe domeinen die oprecht geen HTTPS ondersteunen (zeldzaam in 2026).
- Browser-slotje. Sluit het tabblad en open hem opnieuw. Het slotje moet massief en zonder waarschuwing zijn. Verschillende browsers tekenen hem net iets anders, maar geen enkele toont een doorgestreept slotje, een uitroepteken of een "Niet veilig"-label.
Wanneer om hulp vragen
Heb je alle vijf fixes doorgelopen en staat het slotje nog steeds verkeerd, verzamel dan dit voordat je hulp vraagt:
- De exacte DevTools-console-output, letterlijk, inclusief de URL van elke gemelde resource.
- Het WordPress-adres en het siteadres uit Instellingen > Algemeen (of
wp option get siteurlenwp option get homeals je WP-CLI hebt). - Je
wp-config.php, met alle database-credentials verwijderd. - Of je achter Cloudflare, een andere CDN of een load balancer draait, en zo ja: in welke SSL-modus.
- De response-headers van de homepage (gebruik een gratis tool als securityheaders.com of, als je SSH hebt,
curl -sI https://yoursite.nl/). - Je actieve thema met versienummer en een lijst van actieve plugins uit Plugins > Geïnstalleerde plugins.
Een site met mixed content heeft vrijwel altijd een van de vijf oorzaken hierboven. Als geen enkele past, zit het probleem meestal in een JavaScript-injected resource van een third-party script (chatwidgets, oude analytics, embedded players) die je alleen vindt door het Network-tabblad open te zetten met "Preserve log" aan en het verkeer over een volledige gebruikerssessie te volgen.
Hoe voorkom je dat dit weer gebeurt
- Los het op bij de databaselaag, één keer goed. Leun niet op Really Simple SSL of een andere runtime HTML-rewriting-plugin als permanente oplossing. Draai de search-replace, verwijder de plugin en ga verder.
- Gebruik
home_url(),site_url()enget_template_directory_uri()in custom code. Hardcode nooithttp://yoursite.nlofhttps://yoursite.nlin een thema- of pluginbestand. WordPress genereert het juiste scheme wel voor je. - Draai je achter een reverse proxy, zet dan de shim voor
HTTP_X_FORWARDED_PROTOinwp-config.phpvanaf dag één, niet pas na de eerste redirect-lus. - Gebruik Full of Full (Strict) SSL op Cloudflare. Flexible SSL is een doodlopend spoor voor elke serieuze WordPress-site.
- Als je de site naar een nieuw domein migreert, draai
wp search-replaceopnieuw voor het nieuwe domein en controleer met DevTools. Mixed content na een migratie betekent meestal oude URL's van een site die je vergeten was, zoalshttps://staging.yoursite.nlofhttps://yoursite.local.
Voor het verwante symptoom waarbij de redirect tussen HTTP en HTTPS in een oneindige lus gaat, zie het artikel Too Many Redirects in WordPress, dat hetzelfde scenario rond Cloudflare Flexible SSL vanuit de redirect-hoek behandelt. Voor sessies die niet blijven hangen tussen HTTP en HTTPS tijdens een halfgemaakte migratie, zie het artikel cookies zijn geblokkeerd in WordPress.