022 —WordPress
Dode WordPress-site reanimeren: een 90-minuten playbook
Een Nederlandse bureau-eigenaar stuurde een Loom om 23:41: wit scherm, geen logs, geen SSH. Negentig minuten later draaide de checkout weer. De volgorde.
Een Nederlandse bureau-eigenaar stuurde ons vorige dinsdag om 23:41 een Loom. Twaalf seconden van hem die wp-admin ververst en de browser ziet hangen. De site, een WooCommerce-shop die ongeveer €4k per dag draait, was “gisteren nog prima”. Vandaag: wit scherm, geen logs, geen SSH, alleen FTP en een phpMyAdmin-link uit 2019. Hij had in een ander tabblad een klantmail openstaan met de vraag waarom de checkout kapot was.
Dit is het playbook dat we met hem doorliepen. Negentig minuten van wit scherm naar werkende checkout. De meeste “dode” WordPress-sites zijn niet echt dood; ze zijn op twee of drie plekken verkeerd geconfigureerd, en de fixes zijn deterministisch. De truc zit in de volgorde waarin je werkt.
De eerste tien minuten: vaststellen waar je naar kijkt
Voordat je iets aanraakt, noteer wat je van buitenaf kunt zien. Open de homepage. Open /wp-admin. Open /wp-login.php. Open /wp-cron.php in de browser. Noteer per pagina het exacte foutbeeld. Een wit scherm is niet hetzelfde als een 500, en een 500 is niet hetzelfde als een redirect loop, en een redirect loop is niet hetzelfde als “database connection error”. Elk wijst naar een ander orgaan.
De vier foutbeelden die je tegenkomt, ruwweg op volgorde van frequentie:
- 500 Internal Server Error op elke URL. Bijna altijd een PHP-versie mismatch of een fatal in een plugin.
- Error establishing a database connection. De credentials in wp-config.php kloppen niet, de database ligt eruit, of de host heeft de socket hernoemd.
- White screen of death. Een PHP fatal met
display_errorsuit. De echte foutmelding staat in de log. - Redirect loop op /wp-admin. Een mismatch tussen siteurl en home in wp_options, of een achterhaalde .htaccess.
Trek /wp-config.php over FTP binnen en zet bovenaan, direct onder <?php, drie regels:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Ververs de homepage één keer. WordPress schrijft nu naar /wp-content/debug.log. Trek dat bestand op. In negentig procent van de gevallen noemt de eerste fatal daarin het bestand en de regel die de site heeft gesloopt. De overige tien procent is de log leeg, wat op zichzelf ook een signaal is: de fout treedt op voordat WordPress boot. Dan zit het in .htaccess, de PHP-versie, of de database.
Het bestandssysteem triagen
Zodra je het foutbeeld kent, is het bestandssysteem de goedkoopste plek om te kijken. Drie dingen om in deze volgorde te controleren.
PHP-versie tegenover de codebase
De meeste hosts hebben hun standaard-PHP de afgelopen achttien maanden stilletjes geupgraded naar 8.2 of 8.3. WordPress core kan daar prima mee om. Plugins uit 2017 niet. Open het hostingpaneel en controleer de actieve PHP-versie. Kijk vervolgens naar de meest recent gewijzigde plugin in /wp-content/plugins/. Als de site sinds 2019 niet is aangeraakt en PHP net naar 8.2 is gegaan, zullen de fatals in debug.log dingen noemen als Cannot use "self" when no class scope is active of de verwijderde each()-functie. De migratienotes op php.net zijn de canonieke lijst van wat 7.x-code breekt.
De fix is om in het hostingpaneel één major version terug te zetten, te bevestigen dat de site weer laadt, en daarna een echte upgrade in te plannen. Probeer de plugin niet live om 23:41 te patchen.
Plugins en themes chirurgisch uitschakelen
Als debug.log een plugin noemt, deactiveer hem dan niet via wp-admin (daar kom je toch niet). Hernoem zijn map via FTP:
mv /wp-content/plugins/broken-plugin \
/wp-content/plugins/broken-plugin.off
WordPress behandelt een ontbrekende map als een gedeactiveerde plugin en zal niet klagen. Dezelfde truc werkt op het actieve theme: hernoem het, en WordPress valt terug op het default theme (twentytwentyfour, twentytwentythree) dat nog geïnstalleerd staat. Als er geen default geïnstalleerd is, drop er dan eerst eentje via FTP in voordat je hernoemt.
Bestandsrechten
Als de host het account heeft gemigreerd of vanuit een back-up heeft hersteld, kunnen de permissies verkeerd terugkomen. De canonieke veilige set:
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod 600 wp-config.php
Als je alleen FTP hebt, kan je client (Transmit, Cyberduck, FileZilla) deze recursief toepassen via het rechtsklikmenu. 600 op wp-config.php is geen paranoia; het voorkomt dat andere tenants op shared hosting je DB-credentials kunnen uitlezen.
De database, het onderdeel waar iedereen bang voor is
Als de homepage nu laadt maar /wp-admin oneindig blijft redirecten, of als de site online is maar elke link naar het verkeerde domein wijst, zit het probleem in wp_options. Twee rijen sturen vrijwel alles aan: siteurl en home.
Open de database (phpMyAdmin als dat alles is wat je hebt, een fatsoenlijke MySQL editor als het kan) en draai:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Als die wijzen naar http://staging.oldhost.com en je live-domein is https://shop.example.nl, dan heb je je redirect loop. Doe niet zomaar een UPDATE op die twee rijen en loop weg. WordPress slaat de oude URL op in geserialiseerde PHP, verspreid over wp_postmeta, wp_options, en eventuele plugin-tabellen die menustructuren of widget-configs hebben gecachet. Een naïeve find-and-replace breekt de lengteprefixen van de serialisatie en corrumpeert de data ongemerkt.
De juiste tool is wp-cli, als je SSH hebt:
wp search-replace 'http://staging.oldhost.com' 'https://shop.example.nl' \
--all-tables --skip-columns=guid --dry-run
Haal --dry-run weg zodra het aantal hits realistisch oogt. Heb je geen SSH, gebruik dan Search Replace DB van interconnect/it: zet hem bij voorkeur in een map buiten de webroot, draai hem één keer, verwijder hem. Laat dat script nooit op een live server staan.
De vlag --skip-columns=guid is belangrijk. De guid-kolom in wp_posts is een permanente identifier en wordt niet gebruikt voor URL-routing, ook al ziet hij eruit als een URL. Hem herschrijven verwart RSS-lezers en breekt feed-deduplicatie voor iedereen die geabonneerd is.
Als de database zelf het probleem is
Als wp_options er prima uitziet maar je nog steeds “Error establishing a database connection” krijgt, controleer dan drie dingen, in volgorde. Eerst: de credentials in wp-config.php tegenover wat het hostingpaneel vandaag aangeeft. Hosts roteren DB-wachtwoorden bij planwijzigingen vaker dan ze toegeven. Twee: de waarde van DB_HOST. Sommige hosts zijn van localhost overgestapt op een socketpad zoals localhost:/var/run/mysqld/mysqld10.sock of een interne hostname zoals db-internal.host.net. Drie: draai een snelle reparatie:
define('WP_ALLOW_REPAIR', true);
Zet die regel in wp-config.php en bezoek /wp-admin/maint/repair.php. Klik op “Repair and Optimize Database”. Haal de regel weg als je klaar bent. Het endpoint is publiek toegankelijk zolang de vlag aanstaat, wat tien minuten prima is, maar geen week.
.htaccess en de rewrite-stack
Als de homepage laadt maar elke subpagina een 404 geeft, ontbreekt .htaccess, is hij afgekapt, of is hij vervangen tijdens een hostingmigratie. WordPress regenereert hem zodra je permalinks opslaat, maar daar heb je geen wp-admin voor nodig. Plak dit in de webroot:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
Draai je op nginx, dan doet dit blok niets, en staat het equivalent in de server-config (die je op shared hosting meestal niet kunt aanpassen). Op nginx-hosts vraag je support om te bevestigen dat de directive try_files $uri $uri/ /index.php?$args; aanwezig is. Die ene regel is op nginx het equivalent van het hele blok hierboven.
Nu je toch in .htaccess zit, voeg twee dingen toe die op een productie-WordPress altijd thuishoren:
<Files wp-config.php>
Require all denied
</Files>
<Files xmlrpc.php>
Require all denied
</Files>
Het eerste blok voorkomt dat iemand wp-config.php kan downloaden als PHP ooit stopt met parsen (precies het foutbeeld dat je DB-wachtwoord lekt). Het tweede sluit xmlrpc.php af, al een decennium een doelwit voor brute-force amplification en in de praktijk door bijna niemand meer gebruikt. De Apache mod_authz_core docs behandelen de moderne Require-syntax als je nog op Deny from all-regels uit het 2.2-tijdperk zit.
Live krijgen en de perimeter dichtzetten
Zodra de homepage, wp-admin, en een steekproef van een product- of post-URL allemaal 200 retourneren, ben je nog niet klaar. De site leeft; hij is nog niet veilig. Twintig minuten werk voorkomt het volgende telefoontje.
Reset het admin-wachtwoord vanuit de database, want je weet niet wie het nog kent:
UPDATE wp_users
SET user_pass = MD5('temp-pwd-rotate-now')
WHERE user_login = 'admin';
WordPress hasht bij de eerste login automatisch om naar phpass. Dwing elke andere gebruiker om opnieuw in te loggen door de auth salts in wp-config.php te wijzigen. Het salt-endpoint van WordPress geeft je een vers blok; plak het over de bestaande acht define()-regels. Elke sessietoken in de database is daarmee ongeldig.
Daarna een audit van de pluginlijst. Alles wat in achttien maanden niet is geupdatet, is een kandidaat om weg te halen. De WPScan vulnerability database vertelt je welke van de actieve plugins bekende CVE’s hebben tegen de geïnstalleerde versie. Draai hem één keer, schrijf de drie ergste op, pak ze deze week aan. Het saaie patroon op elke dode WordPress-site die we hebben gezien: een oud contactformulier, een oude caching-plugin, een oude SEO-plugin. Het patroon is saai, en juist daardoor werkt het.
Maak ten slotte een snapshot van de werkende staat. Tar de webroot en dump de database, voordat je iets anders verandert:
tar -czf wp-site-$(date +%F).tar.gz public_html/
mysqldump -u user -p dbname > wp-db-$(date +%F).sql
Als je alleen FTP hebt, trek de hele boom naar lokale schijf en exporteer de database via phpMyAdmin naar .sql.gz. Wat je ook doet: je wilt een werkende baseline vóór de volgende wijziging.
Wat we hier uiteindelijk voor hebben gebouwd
We liepen deze precieze negentig-minuten-loop vaak genoeg tegen om er het grootste deel van ons consultingwerk aan legacy site-onderhoud van te maken. Het trage stuk was nooit de fix; het was de FTP-edit-reload-grep-debug.log-cyclus, plus dat moment van angst als je een rij in wp_options UPDATEt zonder undo. Tijdens het bouwen van Pier liepen we precies hier tegenaan, en wat we er uiteindelijk van maakten was een gedockte FTP- en MySQL-werkomgeving waarin elke bestandsbewaring en elke SQL-schrijfactie in een version history belandt die je met één klik kunt terugrollen. Hetzelfde playbook, minder mailtjes om 02:00.
Het kleinste wat je vandaag kunt doen: open de wp-config.php van één klantsite en plak het WP_DEBUG_LOG-blok hierboven erin, uitgecommentarieerd. Als het telefoontje komt, haal je drie regels uit de comments in plaats van in paniek wp-config.php te zitten lezen.
— Vragen —
Wat als WP_DEBUG_LOG een leeg bestand laat zien?
Een lege debug.log betekent meestal dat de fout optreedt voordat WordPress boot. Kijk dan eerst naar .htaccess, de PHP-versie, of de database-credentials in wp-config.php.
Kan ik niet gewoon siteurl en home in wp_options met SQL UPDATEn?
Voor die twee rijen specifiek: ja. Voor URL's in posts, postmeta, of widget-configs: nee. Die kolommen bevatten geserialiseerde PHP en vereisen een search-replace die de lengteprefixen herschrijft.
Is het veilig om xmlrpc.php geblokkeerd te laten?
Voor de meeste sites wel. De Jetpack-app en een paar verouderde pingdiensten gebruiken hem nog. Als pingbacks falen of de WP-mobiele app weigert te verbinden, is dit blok de oorzaak.