Doel
Aan het einde van dit artikel heb je een extern domein als klant1.nl gekoppeld aan een WordPress multisite-subsite die oorspronkelijk in het netwerk op klant1.netwerk.nl is aangemaakt. Bezoekers die naar https://klant1.nl gaan krijgen de subsite over HTTPS, de WordPress-admin werkt zowel op het gemapte als op het originele URL zonder login redirect loops, en interne content-links wijzen naar het canonieke gemapte domein.
Native multisite domain mapping zit in WordPress core sinds versie 4.5, uitgebracht op 12 april 2016. De oude WPMU Domain Mapping-plugin is achterhaald: op 5 oktober 2025 is hij op verzoek van de auteur gesloten en niet meer beschikbaar in de plugin directory. Installeer hem dus niet op een moderne WordPress-site.
Voorwaarden
Dit artikel overruled de standaard-doelgroep voor de categorie wordpress. Een domein koppelen raakt DNS, nginx-config en TLS-certificaatuitgifte, dus je hebt shell-toegang tot je server nodig. Zit je op managed hosting zonder SSH? Volg dan de eigen domain-mapping-instructies van je host: de meeste managed platforms (Kinsta, WP Engine, Pressable en vergelijkbare) regelen het nginx- en SSL-deel achter hun dashboard en laten jou alleen de Network Admin en wp-config.php aanraken.
Je hebt nodig:
- Een werkend WordPress multisite-netwerk. Dit artikel gaat ervan uit dat je al een draaiend netwerk hebt (WordPress 6.x met
MULTISITEenSUBDOMAIN_INSTALLal inwp-config.php). Heb je dat nog niet? Begin dan eerst met de WordPress multisite setup guide. - Een subsite die al in het netwerk is aangemaakt. Maak de subsite aan via
Network Admin → Sites → Add Newmet een tijdelijk URL alsklant1.netwerk.nl, nog voordat je gaat mappen. Zo weet je zeker dat er al een rij inwp_blogsstaat, de per-site databasetabellen bestaan en de admin-user gekoppeld is. - Root- of sudo-toegang op een Debian 12, Ubuntu 22.04 of Ubuntu 24.04 server met nginx 1.25.1 of nieuwer en PHP-FPM 8.3. De paden in dit artikel volgen de Debian-family-conventies (
/etc/nginx/sites-available/,/etc/letsencrypt/live/,www-data). - Controle over de DNS van het externe domein. Je moet een
A-record (ofAAAAvoor IPv6) kunnen toevoegen bij de DNS-provider voorklant1.nl. certbotgeïnstalleerd met de nginx-plugin (pakkettencertbotenpython3-certbot-nginxop Debian en Ubuntu). Certbot automatiseert de Let's Encrypt ACME challenge en zet de certificaatbestanden in/etc/letsencrypt/live/<domein>/.- WP-CLI op de server. Die heb je in de laatste stap nodig om de URL search-replace veilig over geserialiseerde data te draaien.
- Een volledige backup van de database en van
wp-content. De koppeling bewerktwp_blogs,wp_optionsvan de subsite, enwp-config.php. Een rollback-pad kost niets en levert alles op.
Wat native domain mapping wel en niet doet
Voor WordPress 4.5 had je de WPMU Domain Mapping-plugin nodig om een subsite aan een custom domein te koppelen. WordPress 4.5 heeft de feature in core gemerged: de Network Admin-dashboard accepteert nu elk domein in het veld Site Address (URL) van een subsite, en de routinglaag van WordPress lost binnenkomende verzoeken voor dat domein op naar de juiste subsite.
Wat WordPress overneemt beperkt zich tot de routingbeslissing. Alles buiten zijn eigen scope regel je nog steeds zelf:
- DNS. WordPress komt niet aan je DNS. Jij zet het
A-record bij je provider. - De webserver. WordPress genereert geen nginx- of Apache-config. Jij zet het server block neer.
- TLS-certificaten. WordPress geeft geen certificaten uit. Jij draait certbot.
- Cookies. Het standaard-cookie-gedrag van WordPress klapt om zodra één sessie over meerdere top-level-domeinen moet lopen. Jij zet
COOKIE_DOMAINexpliciet inwp-config.php.
De officiële WordPress multisite domain mapping-documentatie noemt deze vier voorwaarden, maar is dun over hoe je ze daadwerkelijk invult. Dat is wat dit artikel doet.
Twee dingen die native domain mapping niet nodig heeft:
- Geen plugin. Elk artikel of forum-reply dat zegt dat je WPMU Domain Mapping moet installeren, is verouderd. De plugin is gesloten, al acht jaar niet bijgewerkt, en de redirect-logica botst op moderne WordPress met de domeinresolutie van core zelf.
- Geen
sunrise.php. Desunrise.php-drop-in heb je alleen nodig als je meerdere domeinen naar dezelfde subsite wilt laten resolveren (vanity-aliassen, regionale varianten, canonical-redirects in PHP). Voor het één-domein-per-subsite-geval dat dit artikel dekt, heb je hem niet nodig.SUNRISEaanzetten voegt complexiteit toe zonder dat je er iets voor terugkrijgt.
Stap 1: DNS-records voor het nieuwe domein
Maak bij je DNS-provider een A-record aan voor klant1.nl dat naar het publieke IPv4-adres van de server wijst. Zit het netwerk al achter een CDN of reverse proxy (Cloudflare, een load balancer)? Wijs het record dan naar hetzelfde target dat het primaire netwerkdomein al gebruikt.
Type Naam Waarde TTL
A @ 203.0.113.10 300
A www 203.0.113.10 300
AAAA @ 2001:db8::10 300
Hou de TTL kort (300 seconden) tijdens de cutover, zodat je een foutje snel kunt corrigeren. Eenmaal geverifieerd kun je hem naar 3600 of hoger zetten.
Verwachte output: Na DNS-propagatie (meestal 1 tot 10 minuten bij TTL 300) geeft dig +short klant1.nl het server-IP terug.
# Check de DNS-propagatie vanaf de server zelf
dig +short klant1.nl
Geeft dig niets terug of nog het oude target? Wacht dan een minuut en probeer het opnieuw. Ga niet door naar stap 2 voordat DNS correct resolvet. Als je nginx configureert en een certificaat aanvraagt terwijl DNS nog niet naar de server wijst, faalt de HTTP-01-challenge van certbot: Let's Encrypt controleert door vanuit het publieke internet http://klant1.nl/.well-known/acme-challenge/... op te halen.
Stap 2: Nginx server block voor het gemapte domein
Elk gemapt domein heeft zijn eigen nginx server block nodig, omdat elk domein zijn eigen TLS-certificaat krijgt. Wildcard-certificaten dekken maar één niveau subdomeinen onder één apex af (*.netwerk.nl dekt klant1.netwerk.nl, maar niet klant1.nl), dus een netwerk met meerdere top-level-domeinen draait bijna altijd op aparte certificaten per gemapt domein, die tijdens het opbouwen van de verbinding via Server Name Indication (SNI) geselecteerd worden.
Maak /etc/nginx/sites-available/klant1.nl met de volgende inhoud. De root wijst naar dezelfde wp-content-ouder als de rest van het netwerk, want in een WordPress multisite delen alle subsites één fysiek filesystem.
# Poort 80: eerst de ACME-challenge beantwoorden, daarna redirect
server {
listen 80;
listen [::]:80;
server_name klant1.nl www.klant1.nl;
# Laat certbot de HTTP-01-challenge beantwoorden voor we redirecten
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://klant1.nl$request_uri;
}
}
# Poort 443: de eigenlijke gemapte subsite
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name klant1.nl www.klant1.nl;
# Wijs naar de netwerkroot. WordPress bepaalt de subsite op basis
# van de Host-header, niet op basis van het filesystem-pad.
root /var/www/netwerk.nl/public;
index index.php;
# TLS-certificaat: certbot vult dit in de volgende stap in
ssl_certificate /etc/letsencrypt/live/klant1.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/klant1.nl/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Permalinkrouting: identiek aan het server block van het hoofdnetwerk
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP-FPM pass
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# Blokkeer directe toegang tot gevoelige bestanden
location ~ /\.ht { deny all; }
location = /wp-config.php { deny all; }
}
Voeg de certificaatregels nog niet toe als de bestanden er nog niet staan: certbot doet dat in stap 3. Commentarieer de vier ssl_*-regels voor nu uit en laat een kaal server block op poort 80 staan, zodat certbot het eerste certificaat kan bootstrappen. Link het bestand in sites-enabled en herlaad nginx:
sudo ln -s /etc/nginx/sites-available/klant1.nl \
/etc/nginx/sites-enabled/klant1.nl
sudo nginx -t
sudo systemctl reload nginx
Verwachte output: nginx -t rapporteert syntax is ok en test is successful. systemctl reload nginx geeft geen output. curl -I http://klant1.nl/ vanaf elk apparaat met publieke internettoegang geeft óf een 301 naar HTTPS terug (zodra het certificaat er staat), óf het poort-80-blok serveert een WordPress-pagina zonder geldige routing (omdat WordPress het gemapte domein nog niet kent).
Stap 3: TLS-certificaat uitgeven met certbot
Met het poort-80-blok live en DNS die goed resolvet, kun je een certificaat uitgeven voor het gemapte domein:
sudo certbot --nginx -d klant1.nl -d www.klant1.nl
Certbot gebruikt de HTTP-01-challenge, zet een tijdelijk bestand neer onder /.well-known/acme-challenge/, vraagt Let's Encrypt om dat over HTTP te verifiëren, en bij succes past hij je nginx-config aan om HTTPS met het nieuwe certificaat aan te zetten. Hij installeert ook een auto-renewal-cron (of systemd-timer) die twee keer per dag draait.
Verwachte output: Certbot print het pad naar het uitgegeven certificaat en herlaadt nginx automatisch.
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/klant1.nl/fullchain.pem
Key is saved at: /etc/letsencrypt/live/klant1.nl/privkey.pem
This certificate expires on 2026-07-18.
Het per-domein rate limit van Let's Encrypt ligt op 50 nieuwe certificaten per zeven dagen per geregistreerd domein, wat bij normale multisite-netwerken zelden een probleem is, maar wel goed om te weten als je tientallen klantdomeinen tegelijk migreert. Test met de staging-omgeving (--staging) zodat je geen echte uitgiftes verbruikt.
Stap 4: Het gemapte URL zetten in Network Admin
Log in op de network admin op https://netwerk.nl/wp-admin/network/. Ga naar Sites → All Sites, zoek de subsite die nu op klant1.netwerk.nl staat en klik op Edit.
Op het tabblad Info wijzig je Site Address (URL) van https://klant1.netwerk.nl naar https://klant1.nl. Opslaan.
WP-CLI doet hetzelfde in één commando als je liever script:
# Zoek de blog_id van de subsite
wp site list --field=url --field=blog_id
# Wijzig Site Address (URL) en domein voor blog_id 2
wp site meta update 2 url https://klant1.nl/
wp db query "UPDATE wp_blogs SET domain = 'klant1.nl', path = '/' \
WHERE blog_id = 2"
Na deze wijziging herkent de routing-laag van WordPress klant1.nl als onderdeel van die subsite. Maar de bestaande content van de subsite bevat nog hardgecodeerde verwijzingen naar de oude URL, en het cookie-domein staat nog verkeerd. Dat lossen stap 5 en 6 op.
Stap 5: Cookie-domein fixen om login-loops te voorkomen
Het standaard-gedrag van WordPress is om de auth-cookie op het primaire netwerkdomein te zetten (in dit voorbeeld netwerk.nl). Logt een bezoeker in op een gemapt domein als klant1.nl? Dan zet WordPress de cookie voor dat verzoek goed, maar bij het volgende verzoek probeert hij hem tegen het netwerkdomein te valideren, en lijkt de login in stilte te mislukken. Dat is de klassieke multisite redirect loop op gemapte domeinen.
De oplossing: zeg tegen WordPress dat hij de cookie moet scopen op de binnenkomende host, wat op een gemapte subsite het gemapte domein is. Voeg deze regel aan wp-config.php toe, boven de /* That's all, stop editing! Happy publishing. */-marker, direct na de multisite-defines:
// Laat het cookie-domein meelopen met de host uit het verzoek,
// zodat logins werken op elk gemapt domein in het netwerk.
define( 'COOKIE_DOMAIN', $_SERVER['HTTP_HOST'] );
De officiële WordPress multisite domain mapping-documentatie adviseert precies deze configuratie om dezelfde reden: "If login errors or cookie-blocking issues occur, add define( 'COOKIE_DOMAIN', $_SERVER['HTTP_HOST'] ); after existing network code." Een alternatief dat je in oudere artikelen ziet is COOKIE_DOMAIN op een lege string zetten, wat ook werkt maar iets minder strikt is (de browser leidt het cookie-domein dan af uit de response-host, functioneel komt het dicht bij elkaar). Gebruik de $_SERVER['HTTP_HOST']-variant; dat is wat de WordPress-core-documentatie laat zien.
Verwachte output: Herlaad de subsite-admin op https://klant1.nl/wp-admin/, log uit en log opnieuw in. De login lukt zonder redirect loop.
Stap 6: URL search-replace op de subsite draaien
De database van de subsite bevat het oude URL nog ingebakken in option-waarden (siteurl, home), postcontent (image-URLs, interne links), en geserialiseerde widget- en theme-instellingen. Een naïeve UPDATE ... SET content = REPLACE(...) sloopt PHP-geserialiseerde data door de lengteprefixen te corrumperen. Gebruik dus altijd een serialisatie-bewuste tool. WP-CLI's search-replace is de veiligste optie en de enige die de WordPress developer-documentatie aanbeveelt.
Draai hem gescoopt op de doelsubsite met --url:
# Eerst een dry run: zien welke tabellen geraakt worden
wp search-replace 'https://klant1.netwerk.nl' 'https://klant1.nl' \
--url=https://klant1.nl \
--all-tables-with-prefix \
--skip-columns=guid \
--dry-run
# Daarna echt uitvoeren
wp search-replace 'https://klant1.netwerk.nl' 'https://klant1.nl' \
--url=https://klant1.nl \
--all-tables-with-prefix \
--skip-columns=guid
Een paar opmerkingen bij de flags:
--url=https://klant1.nlscoopt WP-CLI op de subsite die je aan het mappen bent, niet op de primaire netwerksite. Zonder deze flag draait WP-CLI op blog ID 1 en mist hij de eigen tabellen van de subsite.--all-tables-with-prefixneemt dewp_2_*-tabellen van de subsite mee (waar 2 de blog_id van de subsite is).--skip-columns=guidbewaart deguid-kolom op posts. WordPress gebruiktguidals permanente identifier die feed-lezers en externe systemen gebruiken; hem veranderen breekt abonnementen en hoort nooit onderdeel te zijn van een URL-replace.--dry-runop de eerste doorgang print het aantal vervangingen per tabel zonder iets weg te schrijven. Altijd eerst draaien.
Verwachte output: De dry run rapporteert iets als Success: 147 replacements to be made over 4 tot 8 tabellen. De echte uitvoering rapporteert dezelfde aantallen als replacements made.
Controleren of het gemapte domein end-to-end werkt
Loop deze korte checklist door voordat je de koppeling als klaar beschouwt:
- DNS:
dig +short klant1.nlgeeft het server-IP terug. - HTTPS:
curl -I https://klant1.nl/geeft een200 OKmet geldig certificaat en de WordPress-response-headers (link: <https://klant1.nl/wp-json/>; rel="https://api.w.org/"). - Redirect:
curl -I http://klant1.nl/geeft een301naarhttps://klant1.nl/. - Frontend:
https://klant1.nl/in een browser rendert de homepage van de subsite met assets vanafhttps://klant1.nl/wp-content/...(niethttps://klant1.netwerk.nl/wp-content/...). - Admin:
https://klant1.nl/wp-admin/laadt na login het subsite-dashboard. Uitloggen en weer inloggen triggert geen redirect loop. - Oude URL:
https://klant1.netwerk.nl/werkt óf nog als alias (WordPress accepteert beide domeinen voor de subsite totdat je één van de twee expliciet redirect), óf hij redirect naar het gemapte domein, afhankelijk van je nginx-config.
Slagen alle zes? Dan is de koppeling klaar. Herhaal de hele flow voor elke volgende subsite met een eigen domein.
Subdomain versus subdirectory: wat verschilt er
Domain mapping werkt op beide netwerktypes hetzelfde wat betreft de UI-wijziging in Network Admin en de COOKIE_DOMAIN-fix. De verschillen zitten in DNS en nginx.
Subdomain-netwerk (SUBDOMAIN_INSTALL = true):
- Voor de koppeling staan subsites op
klant1.netwerk.nl. - Wildcard-DNS (
*.netwerk.nl) en een wildcard-certificaat op het primaire netwerk zijn er meestal al om on-demand subsites aan te kunnen maken. - Het gemapte domein (
klant1.nl) staat volledig los van die wildcard. Zijn server block en certificaat hebben niets met*.netwerk.nlte maken. - Dit is de makkelijkste situatie om vanuit te mappen.
Subdirectory-netwerk (SUBDOMAIN_INSTALL = false):
- Voor de koppeling staan subsites op
netwerk.nl/klant1/. - Er is geen wildcard-DNS nodig, subsites hebben immers geen eigen hostnaam. DNS is dus simpeler.
- Het server block voor het hoofdnetwerk bevat het
WPMU rewrite blockdat/klant1/-paden naar de juiste subsite routeert. Het server block van het gemapte domein heeft die rewrite niet nodig: zodra WordPress het gemapte domein ziet, kan hij rechtstreeks naar de subsite zonder de path-prefix-routing. - De core-routing van WordPress werkt nog steeds, maar de content die je van
netwerk.nl/klant1/naarklant1.nlmigreert bevat een pad-component in het originele URL dat de search-replace in stap 6 mee moet nemen. Dewp search-replace-aanroep hierboven doet dat automatisch zolang je het volledige URL met pad meegeeft ('https://netwerk.nl/klant1'naar'https://klant1.nl').
Veel gemaakte aanname: "domain mapping werkt bij subdomain- en subdirectory-netwerken hetzelfde." Dat klopt op het niveau van Network Admin, maar de nginx-routing eronder verschilt wezenlijk, en de zoekstrings die je aan WP-CLI meegeeft zijn ook anders. Test een subdirectory-naar-extern-domein-koppeling dus eerst op een staging-omgeving voordat je hem in productie uitrolt.
Troubleshooting: redirect loops en verloren admin-toegang
De login lukt, maar je komt direct weer op het loginscherm terug. Klassiek COOKIE_DOMAIN-probleem. Check of de define in wp-config.php staat, check dat er geen oude COOKIE_DOMAIN met een hardgecodeerd domein van een vorige install is blijven staan, wis de browsercookies voor zowel het gemapte als het netwerkdomein, en log opnieuw in. Blijft het probleem? Check dan of je caching-laag (Varnish, Cloudflare, een object cache) de host-header meeneemt in de cache key; anders kan een gecachte login-respons voor het ene domein op het andere landen en raakt de browser in de war.
Te veel redirects (ERR_TOO_MANY_REDIRECTS). Meestal redirect het nginx-server-block naar zichzelf. Check of er een return 301 https://...-regel in het poort-443-blok staat in plaats van in het poort-80-blok. Check ook dat de siteurl- en home-opties van WordPress (na stap 6 op het gemapte HTTPS-URL) niet botsen met een HTTP-naar-HTTPS-redirect in nginx.
Network Admin redirect naar het gemapte domein in plaats van naar het netwerkdomein. Dit gebeurt wanneer DOMAIN_CURRENT_SITE in wp-config.php per ongeluk op het gemapte domein is gezet. DOMAIN_CURRENT_SITE moet altijd het primaire netwerkdomein zijn (het domein dat je gebruikte toen het netwerk werd opgezet), nooit een gemapt subsite-domein. Zet hem terug op het netwerkdomein en de Network Admin-login werkt weer.
Het gemapte domein serveert de hoofdsite in plaats van de subsite. WordPress heeft het gemapte domein niet naar een blog ID kunnen mappen. Check direct in wp_blogs: de domain-kolom voor de rij van de subsite moet exact klant1.nl zijn (geen protocol, geen trailing slash, geen www.). Afwijkingen daar zijn verreweg de meest gemelde oorzaak van "de mapping doet niets".
Images en stylesheets laden nog vanaf het oude URL. De search-replace in stap 6 heeft niet alle tabellen meegepakt. Draai wp search-replace --url=https://klant1.nl --dry-run opnieuw om te zien welke tabellen het oude URL nog bevatten, en verbreed de replace waar nodig (sommige plugintabellen staan buiten de wp_2_*-prefix en hebben een eigen --all-tables-pass nodig).
Admin-dashboard-links gaan na de mapping naar het verkeerde domein. Core-ticket #47630 beschrijft deze klasse issues: sommige admin-bar-links gebruiken network_admin_url() dat altijd naar het netwerkdomein wijst, wat correct gedrag is maar verwarrend als je verwacht dat ze op het gemapte domein blijven. De network admin leeft op het primaire netwerkdomein en is niet te mappen; een klant die zijn subsite wil beheren gebruikt gewoon https://klant1.nl/wp-admin/ (niet de Network Admin).
De complete eindconfiguratie
Voor de duidelijkheid: hier staat alles bij elkaar na alle zes de stappen, zodat je het niet uit de losse code-blokken hoeft te reconstrueren.
/etc/nginx/sites-available/klant1.nl:
server {
listen 80;
listen [::]:80;
server_name klant1.nl www.klant1.nl;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://klant1.nl$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name klant1.nl www.klant1.nl;
root /var/www/netwerk.nl/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/klant1.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/klant1.nl/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.ht { deny all; }
location = /wp-config.php { deny all; }
}
Het multisite-blok van wp-config.php (alleen de regels die met domain mapping te maken hebben):
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', true );
define( 'DOMAIN_CURRENT_SITE', 'netwerk.nl' ); // altijd het primaire netwerk
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );
// Laat de auth-cookie meelopen met het gemapte domein, zodat logins
// werken op elk subsite-URL in het netwerk.
define( 'COOKIE_DOMAIN', $_SERVER['HTTP_HOST'] );
Databasetoestand voor de gemapte subsite:
-- wp_blogs-rij voor de gemapte subsite
SELECT blog_id, domain, path FROM wp_blogs WHERE blog_id = 2;
-- Verwacht: 2 | klant1.nl | /
-- siteurl- en home-opties op de subsite
SELECT option_name, option_value FROM wp_2_options
WHERE option_name IN ('siteurl', 'home');
-- Verwacht: beide op https://klant1.nl
Voor elk volgend gemapt domein herhaal je stap 1 tot en met 6 met het nieuwe domein erin. Het wp-config.php-blok blijft hetzelfde, één COOKIE_DOMAIN-define is genoeg voor het hele netwerk. Alleen het nginx-server-block en de per-subsite databaserecords verschillen per gemapt domein.