Mixed content WordPress oplossen is meestal geen mysterie. Je site draait op HTTPS, maar één of meer assets worden nog via HTTP geladen en de browser haalt daarom het slotje weg. Deze how-to loopt in vier stappen door: opsporen, WP-instellingen, database-replace en hardcoded thema-URLs. Geen plugin verplicht, wel commando’s die veilig met serialized data omgaan.
De symptomen zijn herkenbaar. Het slotje-icoontje verdwijnt of wordt doorgestreept. In Chrome krijg je “Not secure”. In de console verschijnt “was loaded over HTTPS, but requested an insecure element”. De inhoud werkt vaak nog, maar het vertrouwen richting bezoekers zakt. Voor webshops kan het zelfs de checkout blokkeren als de betalingsprovider strikte CSP-headers heeft.

Wat mixed content is en waarom het slotje verdwijnt
Een HTTPS-pagina verwacht dat alle onderliggende bronnen (afbeeldingen, scripts, stylesheets, iframes) óók via HTTPS worden opgehaald. Als één script via http:// binnenkomt, breekt de belofte van een volledig versleutelde verbinding. De browser reageert daarop met een waarschuwing.
Er zijn twee categorieën:
- Actieve mixed content (scripts, iframes, CSS, XHR). Wordt door moderne browsers actief geblokkeerd. Een script dat niet laadt, breekt vaak functionaliteit.
- Passieve mixed content (afbeeldingen, video’s, audio). Krijgt alleen een waarschuwing. De asset laadt nog wel, maar het slotje blijft weg.
Sinds Chrome 80 (feb 2020) upgradet Chrome mixed audio/video-content automatisch naar HTTPS; vanaf Chrome 81 (apr 2020) geldt hetzelfde voor images. Werkt waar dat kan. Werkt de bronserver ook op HTTPS: opgelost. Werkt de bronserver niet op HTTPS: de asset laadt niet en je krijgt alsnog een broken image. Vertrouw dus niet blind op auto-upgrade.
Voor de exacte specificatie en achtergrond is de web.dev-uitleg over mixed content de aangewezen bron. Voor de bredere HTTPS-configuratie op je site verwijs ik naar WordPress-beveiliging en HTTPS-config.
Mixed content WordPress oplossen: 4 stappen in overzicht
Voor je begint een korte preview van de workflow. Elke stap komt daarna uitgebreid aan bod.
- Opsporen. Chrome DevTools open, tab Console, filter op “mixed content”. Noteer welke URLs binnenkomen via HTTP.
- WordPress-instellingen. Site Address en WordPress Address op
https://. OptioneelFORCE_SSL_ADMINin wp-config.php. - Database-replace. Alle oude
http://voorbeeld.nlnaarhttps://voorbeeld.nlmet Better Search Replace of WP-CLI. Serialized-safe. - Hardcoded thema-URLs. Via SSH
grepdoor je child-theme, custom CSS en widget-content. Handmatig aanpassen wat de replace-tool niet raakt.
Extra afsluiting: cache flushen op elk niveau, en optioneel een upgrade-insecure-requests-header als vangnet.
Stap 1: opsporen in de browser-console
De snelste route: open je site in Chrome, druk F12, ga naar tab Console. Herlaad de pagina. In de console verschijnen meldingen als:
Mixed Content: The page at 'https://voorbeeld.nl' was loaded over HTTPS,
but requested an insecure element 'http://voorbeeld.nl/wp-content/...'Klik door de meldingen. Elke regel bevat de exacte URL die het probleem veroorzaakt. Noteer ze allemaal. Voor sites met veel content is een gratis online scanner handig: Why No Padlock (whynopadlock.com) crawlt je pagina en geeft een lijst URLs terug.
Voor een volledige site-scan draai ik lokaal een crawler zoals Screaming Frog SEO Spider in “internal + external”-modus met filter op “http://”. Dat vindt ook mixed content op pagina’s die je zelf niet dagelijks bezoekt.
Categoriseer wat je vindt in drie groepen: assets van je eigen domein (99 procent van de fixes), assets van externe bronnen (widget-embeds, oude analytics-code), en assets die alleen in specifieke templates verschijnen (mail-templates, PDF-generatoren).
Stap 2: WordPress-instellingen op HTTPS zetten
Ga naar Instellingen → Algemeen. Zet beide URL-velden om:
- WordPress-adres (URL):
https://voorbeeld.nl - Site-adres (URL):
https://voorbeeld.nl
Opslaan. WordPress logt je uit. Log opnieuw in via de HTTPS-URL. Als de admin nu werkt: goede eerste stap.
Voor extra zekerheid in de admin, voeg toe aan wp-config.php (boven de “That’s all, stop editing”-regel):
define('FORCE_SSL_ADMIN', true);Dat dwingt de admin-area en login-pagina altijd via HTTPS af, ook als iemand http://voorbeeld.nl/wp-admin/ probeert.
Waarschuwing: als deze instelling wordt gezet zonder werkend SSL-certificaat, kom je niet meer in de admin. Doe dit dus pas als je bevestigd hebt dat HTTPS werkt op de frontend.
Stap 3: database-URLs migreren met Better Search Replace of WP-CLI
Nu de kern. In je database staan honderden tot duizenden verwijzingen naar http://voorbeeld.nl. Die staan in post_content, in widget-instellingen, in ACF-velden, in serialized JSON van pagebuilders. Een simpele SQL-UPDATE ... REPLACE(...) breekt serialized data omdat de string-lengte-prefixen niet meer kloppen. Dat is de klassieke valkuil.
Route A: Better Search Replace (plugin). Installeer, ga naar Tools → Better Search Replace.
- Zoek naar:
http://voorbeeld.nl - Vervang met:
https://voorbeeld.nl - Selecteer alle tabellen (ctrl-A in het lijstje).
- Vink “Run as dry run” aan.
- Klik “Run Search/Replace”.
De dry-run toont hoeveel matches per tabel er zijn zonder wijzigingen door te voeren. Bekijk het rapport. Als de aantallen kloppen (meestal duizenden in wp_posts, honderden in wp_postmeta), zet dry-run uit en draai opnieuw.
Route B: WP-CLI (voorkeur bij grote sites). Via SSH:
wp search-replace 'http://voorbeeld.nl' 'https://voorbeeld.nl' --dry-run --all-tablesBekijk de output. Als het aantal wijzigingen redelijk lijkt:
wp search-replace 'http://voorbeeld.nl' 'https://voorbeeld.nl' --all-tablesWP-CLI behandelt serialized data correct. Voor multisite gebruik je --network erbij.
Wat vaak vergeten wordt: ook de variant zonder subdomain (http://www.voorbeeld.nl als je site op non-www draait, of andersom) apart doen. En de subdomein-varianten van CDN’s (http://cdn.voorbeeld.nl naar https://cdn.voorbeeld.nl).

Stap 4: hardcoded thema- en CSS-URLs oplossen
De database is nu schoon, maar er is content die niet in de database staat: je child-theme-bestanden, custom CSS-blokken die naar images verwijzen, en soms harde URLs in third-party plugin-configs.
Via SSH:
grep -r "http://voorbeeld.nl" wp-content/themes/child-theme/
grep -r "http://" wp-content/uploads/ --include="*.css"
grep -r "http://" wp-content/plugins/mijn-custom-plugin/Wat je vaak vindt:
- Header.php met een hardcoded logo-URL.
- Style.css met
background-image: url(http://voorbeeld.nl/wp-content/...). - Custom widgets waar een img-tag in staat.
- WooCommerce e-mail-templates met een hardcoded logo (Appearance → Customize → Emails, of in de template-overrides).
Voor elke vondst: verander http:// naar https:// of, nog beter, gebruik protocol-relative URLs (//voorbeeld.nl/...) of WordPress-functies (get_stylesheet_directory_uri()). Verbeteren boven herbouwen: één middag werk in plaats van een migratie.
Vergeet de custom CSS in de Customizer niet (Appearance → Customize → Additional CSS). Die staat wél in de database (in de option theme_mods_...), maar de search-replace vindt hem alleen als je exact op je oude domein-URL zoekt.

Extra: CDN, cache en Content Security Policy
Na de fix moet je alle caches legen om te zien of het echt opgelost is.
- WordPress-cache-plugin. WP Rocket, W3 Total Cache, LiteSpeed Cache: elk heeft een “Purge all”-knop.
- Object-cache. Bij Redis of Memcached: flush via WP-CLI (
wp cache flush) of het hostingpaneel. - Server-cache. Bij Hostinger LiteSpeed, Kinsta of WP Engine: knop in het klantpaneel.
- CDN. Cloudflare: Purge Everything. StackPath, KeyCDN: idem in dashboard.
- Browser. Hard-refresh (Ctrl+Shift+R of Cmd+Shift+R). Of anonieme modus openen.
Als vangnet kun je een Content Security Policy-header toevoegen die de browser vraagt om HTTP-content automatisch als HTTPS te proberen. In nginx:
add_header Content-Security-Policy "upgrade-insecure-requests";In Apache/.htaccess:
Header set Content-Security-Policy "upgrade-insecure-requests"Deze header is een aanvulling, geen vervanging. Als de bronserver geen HTTPS heeft, valt de asset alsnog uit.
Wanneer een webmaster het overneemt
Voor een standaard-site met één taal, één subdomein en geen exotische plugins is dit een uur werk. Voor sites waar het complexer wordt, loont het om iemand te laten kijken. Concrete situaties:
- Multisite waar je per site een replace moet doen.
- WooCommerce met veel product-images en e-mail-templates die net anders ingericht zijn.
- Een migratie waarbij het domein óók verandert (
http://oude-site.nlnaarhttps://nieuwe-site.nl): twee vervangingen tegelijk, meer risico. - Sites waar een eerdere replace half is uitgevoerd en de database inconsistent is.
Bij mij zit dit standaard onder het uurtarief van 95 euro voor serverwerk. Voor multisite of complexere WooCommerce zit het op 105 euro. Ik werk in de source-code voor wat er buiten de database zit en gebruik dry-runs voor alles wat de database raakt. Voor doorlopende hygiëne is onderhoud van je WordPress-installatie meestal goedkoper dan losse uren.
Andere stacks kennen hetzelfde patroon. Shopify Liquid-templates hebben zelden mixed content (Shopify serveert alles via HTTPS). Joomla vaker: menu-images en oude modules met hardcoded URLs. Drupal via Views en oudere versions van het File-module. Voor elk platform is de kern: opsporen, database (of config), templates, cache.



