WordPress
WordPress naar Astro: negen dagen vast in Yoast-redirects
Negen dagen na de migratie van WordPress 6.4 naar Astro en Payload gaven 4.200 long-tail URL's een 404. De Yoast redirect manager loog over zijn eigen data.

Het was 11:47 op een dinsdagochtend in Den Haag. De marketing lead bij een Nederlandse uitgever met 26 mensen ververste Google Search Console voor de vijfde keer dat uur. De organische clicks zakten al sinds de relaunch op die zaterdagavond, tien dagen eerder. Niet zachtjes. Dinsdagochtend stonden 4.200 pagina's op 404, en de hoofdredacteur kwam de trap af met een uitdraai in haar hand.
Ze waren gemigreerd van WordPress 6.4 naar Astro op de front-end en Payload CMS op de back-end. De cutover ging schoon. Alle 12.600 artikel-URL's losten op het nieuwe domein op. Het team had zondag nog gevierd. Op dinsdag zaten ze negen dagen diep in een spiraal van redirect-debuggen, zonder duidelijke oorzaak en met een CFO die vroeg wanneer SEO terugkwam.
Op dag negen werden wij erbij gehaald. Wat volgt is hoe de bug uiteindelijk gevonden werd, en waarom geen enkel automatisch migratiescript dit had opgevangen.
Het migratieplan dat schoon oogde
De uitgever draaide op long-tail content. Twaalfduizend vijfhonderd artikelen, de meeste ouder dan vijf jaar, een flink deel rankte op niche queries die echte advertentie-omzet binnenbrachten. De nieuwe stack was een verstandige stap. Astro on the edge voor snelheid, Payload als redactioneel CMS, met een Postgres-database en hosting op Vercel. Het oude WordPress was een onderhoudslast geworden: een database die sinds 2014 alleen maar groeide, een stapel plugins van drie verschillende bureaus, en een jaarlijkse hostingfactuur die elke verlenging hoger uitviel.
In het migratieplan zat één onderdeel waar iedereen het over eens was: de 301 redirect map was de belangrijkste oplevering. Twaalfduizend zeshonderd oude URL's in ?p=-stijl en /2019/categorie/slug-stijl moesten op de nieuwe permalink-structuur uitkomen. Daarbovenop had de uitgever vijf jaar lang wildcard-regels opgebouwd in de Yoast SEO Premium redirect manager. Patronen als /oude-rubriek/* die herschreven naar /archief/oude-rubriek/$1. Bij elkaar zo'n 240 wildcard-regels.
De interne developer die de migratie deed, koos de logische route. Hij draaide de WP-CLI export, maakte van de redirect map een JSON-bestand, voerde dat in een Vercel vercel.json redirect-blok, en zette het live.
Het eerste signaal dat er iets misging
Zaterdag en zondag: organisch zoekverkeer zag er redelijk normaal uit. Search Console heeft een paar dagen vertraging. Maandagmiddag was het aantal 404's gestegen naar 1.100. Dinsdagochtend stond de teller op 4.200.
Het patroon in de 404-lijst was het interessante deel. Elke URL kwam uit artikelen die vóór mei 2019 waren gepubliceerd. Nieuwere artikelen redirectten netjes. Oudere artikelen redirectten ofwel naar een letterlijke $1-string (de niet-vervangen wildcard placeholder), ofwel nergens heen.
De developer had de redirect map al gecontroleerd. Hij oogde compleet: 12.600 entries plus 240 wildcards, allemaal aanwezig, allemaal valide. Hij had drie dagen besteed aan het opnieuw draaien van de export, het vergelijken van diff hashes, zelfs aan het herbouwen van de WordPress staging-omgeving uit een verse back-up. De export was deterministisch. De output zag er gezond uit.
Dat was schijn.
De smoking gun in de SQL
Toen wij op dag negen binnenkwamen, was het eerste wat we deden: de ruwe geserialiseerde PHP-blob openen waarin de wildcard-regels zaten. Niet de JSON-output waar de developer naar zat te staren. De daadwerkelijke option_value uit de database. We haalden hem op met één regel.
wp db query \
"SELECT option_value FROM wp_options \
WHERE option_name = 'wpseo-premium-redirects-base'" \
--skip-column-names > redirects-raw.txt
wc -c redirects-raw.txtHet bestand was 65.535 bytes. Precies. Dat getal is een waarschuwingsbel.
Als jouw dump van een geserialiseerde PHP-option exact 65.535 bytes is, dan zit je niet naar je data te kijken. Je kijkt naar het kolomtype. MySQL's TEXT heeft een cap op 216-1 bytes (65.535). Echte WordPress option-data hoort in LONGTEXT (4 GiB). Lopen die twee door elkaar, dan wint de kleinste en gebeurt het afkappen geluidloos.
We deserialiseerden de blob in een PHP REPL. unserialize() gaf false terug. De foutmelding wees naar een lengtemismatch rond byte 65.400: een geserialiseerde string was aangekondigd als s:412:"...", maar de string zelf was midden in een karakter afgekapt. De afsluitende accolade van de buitenste array kwam nooit. PHP zag de corruptie, gooide de handen omhoog, en gaf false terug.
WordPress' eigen get_option() gaf precies diezelfde stukke blob terug aan alles wat downstream meelas, dus de redirect manager UI negeerde stilletjes elke wildcard-regel die na het afkappunt gedefinieerd stond. De eerste 8.400 platte redirects overleefden, want die zaten eerder in de geserialiseerde array. De 240 wildcards stonden achteraan in de structuur, en de long-tail pre-2019 archiefregels stonden helemaal achteraan. Dat waren de 4.200 pagina's die op 404 stonden.
Waarom de option API het verborg
Dit is het stuk dat ons zes uur kostte om uit te puzzelen, omdat de bug ouder was dan iedereen in het huidige dev-team.
In 2019 had een vorig bureau er een eigen plugin op gebouwd die de Yoast redirect-data synchroniseerde naar een rapportagetabel die wp_seo_redirects_sync heette. Ze bouwden dat omdat het marketingteam redirect-performance wilde joinen met Matomo-data, en joinen tegen een geserialiseerde blob binnen wp_options is een drama. Dus maakten ze een platte tabel. Ze definieerden de rule_payload-kolom als TEXT.
Drie jaar lang hield die tabel alles vast. De geserialiseerde blob zat ergens rond de 38 kilobyte. Ruimte zat. Toen importeerde de redactie ergens in 2022 een grote batch verouderde URL's van een zustertitel, en groeide de blob over de 64 KB heen. De volgende sync kapte af. De rapportagetabel hield de afgekapte kopie. Niemand merkte het, want niemand las de rapportagetabel direct uit. De redirect manager UI bleef uit wp_options lezen, waar de data wel intact was.
Hier komt het stuk dat ze de das om deed. Het migratiescript van de interne developer, dat hij in 2026 schreef om alles naar Astro te verhuizen, haalde de redirect map uit wp_seo_redirects_sync, niet uit wp_options. Waarom? Omdat de rapportagetabel al genormaliseerde kolommen had. Source, destination, rule type, regex. Het was de voor de hand liggende databron. Hij was ook drie jaar verouderd en stilletjes stuk.
De wp_options-blob was prima. De data was er altijd geweest. De export pipeline was de bug.
De reparatie, in vier zetten
We hadden geen tijd om de hele migratie opnieuw te doen. De CFO wilde de SEO-clicks binnen de week terug. Dit deden we.
Eén. We trokken de canonieke redirect map rechtstreeks uit wp_options, en omzeilden de stukke sync-tabel volledig.
wp eval '
$raw = get_option("wpseo-premium-redirects-base");
$rules = is_array($raw) ? $raw : [];
echo json_encode($rules, JSON_PRETTY_PRINT);
' > redirects-canonical.jsonTwee. We valideerden elke regel in het bestand. Elke regel waarvan de source-URL geen 200 gaf op het nog draaiende WordPress (op een staging-domein) werd gemarkeerd. De gemarkeerde set vertelde ons wat echt was en wat een spook was uit jaren van plugin-wisselingen.
// validate-redirects.mjs
import fs from "node:fs/promises";
const rules = JSON.parse(await fs.readFile("redirects-canonical.json", "utf8"));
const base = "https://staging.publisher.nl";
const results = [];
for (const r of rules) {
const res = await fetch(base + r.origin, { redirect: "manual" });
results.push({ ...r, livestatus: res.status });
}
await fs.writeFile("redirects-verified.json", JSON.stringify(results, null, 2));
const live = results.filter(r => r.livestatus === 200).length;
console.log(`${results.length} rules; ${live} live`);Drie. We trokken twaalf maanden aan access logs uit de oude hostingomgeving, groepeerden op URL, en filterden de top 25.000 paden op request count. Alles in die lijst dat niet in redirects-verified.json stond, kreeg een handmatige mapping. Dit is het stuk werk zonder glamour waarmee je tail-verkeer terughaalt. Een shortcut bestaat niet.
Vier. We splitsten de redirect map tussen Vercel's edge config (de hete 5.000 op verkeer) en een Postgres-lookup in Payload (de long tail). Edge config heeft een eigen size cap. Doen alsof die er niet is, is precies hoe migraties als deze stranden.
Wat we in ons migratie-playbook hebben aangepast
Drie concrete gewoontes zijn uit deze klus voortgekomen. We gebruiken ze nu bij elke CMS-migratie.
Controleer het kolomtype, niet het aantal rijen
Voordat we een geserialiseerde blob uit een WordPress-database exporteren, draaien we één regel ter controle op de kolomdefinitie.
wp db query "SHOW COLUMNS FROM wp_options LIKE 'option_value'"Als het antwoord geen longtext is, stoppen we en gaan we graven. Hetzelfde doen we op elke custom tabel waar pluginstate in leeft. De MySQL storage requirements-tabel is een bookmark waard. TINYTEXT stopt op 255 bytes, TEXT op 64 KiB, MEDIUMTEXT op 16 MiB, LONGTEXT op 4 GiB. Sites die door veel handen zijn gegaan, hebben bijna altijd ergens één verkeerd type zitten.
Valideer de serialisatie, niet de bytecount
Een geserialiseerde PHP-blob heeft een vaste grammatica. Je kunt hem streng parsen. Geeft unserialize() false terug, dan valt er niets meer te bespreken. De data is stuk. We draaien tegenwoordig unserialize op elke option groter dan 8 KiB tijdens de auditfase, voordat er ook maar één regel migratiecode draait.
Verifieer de canonieke keten van begin tot eind, niet alleen de telling
De interne developer had 12.600 entries geteld in zijn outputbestand. De telling klopte. De inhoud niet. Wij nemen nu een steekproef van 200 willekeurige URL's uit twaalf maanden access logs en doen er een cURL tegenaan op de nieuwe site. Geeft een van die URL's iets anders terug dan 200 of één enkele 301, dan is de migratie niet af. Rijen tellen is geen testen.
De prijs van de stilstand
De uitgever verloor negen dagen organisch verkeer op 4.200 rankende pagina's. Search Console liet zien dat de impressions op de getroffen set binnen achtenveertig uur na de cutover instortten. Het meeste verkeer kwam binnen vier weken na de fix terug. De langste tail had ongeveer acht weken nodig. Sommige queries herstelden zich nooit helemaal. Google leest een lange 404-streep als signaal, en dat signaal blijft hangen.
Toen wij de nieuwe redirect-laag bouwden voor deze uitgever, was het ding waar we tegenaan liepen dat geen enkele export-tool de volledige pluginstate over drie jaar bureau-overdrachten betrouwbaar heen en weer kon trekken. Uiteindelijk losten we het op door de canonieke data uit twee onafhankelijke bronnen te halen (de option API en de access logs) en die te reconciliëren voordat er één redirect-regel live ging. Dat is het soort werk dat we vaker doen rond legacy migraties: geen magie, gewoon het zorgvuldige loodgieterswerk waar niemand graag voor betaalt totdat ze negen dagen kwijt zijn.
Heb je een WordPress-site die je gaat verhuizen, draai dan één commando voordat je iets anders doet. Open een database client en vraag: SHOW COLUMNS FROM wp_options LIKE 'option_value'. Antwoordt hij longtext, dan zit je waarschijnlijk goed. Antwoordt hij iets anders, stop. De volgende negen dagen van je project zijn net goedkoper geworden.
Kern
Is je dump van een geserialiseerde WordPress-option exact 65.535 bytes, dan is de bug het kolomtype, niet de data. Echte option-data hoort in LONGTEXT.
FAQ
Waarom was de Yoast redirect-blob exact 65.535 bytes?
Omdat hij ergens in de export-keten in een MySQL TEXT-kolom werd bewaard, en die loopt vast op 2^16-1 bytes. De data was compleet in wp_options (LONGTEXT) maar afgekapt in een custom sync-tabel die TEXT gebruikte.
Hoe weet ik welke WordPress-kolommen LONGTEXT zijn en welke TEXT?
Draai wp db query "SHOW COLUMNS FROM wp_options LIKE 'option_value'" of kijk in een custom plugintabel met SHOW CREATE TABLE. Staat er bij een kolom die geserialiseerde data bevat iets anders dan LONGTEXT, behandel hem dan als verdacht.
Kun je WordPress-redirects naar Astro migreren zonder Yoast Premium?
Ja. De redirect-regels staan in wp_options als geserialiseerde array. Trek ze eruit met wp eval, zet ze om naar JSON, en laad ze in Vercel edge config of het CMS van je keuze. Yoast is handig, maar niet vereist.
Wat is de veiligste manier om een migratie te verifiëren voordat hij live gaat?
Pak een steekproef van een paar honderd URL's uit twaalf maanden server access logs en doe er een curl tegenaan op de nieuwe site. Alles wat meer dan één 301 teruggeeft of een 404, is een bug. Rijen tellen in het exportbestand is geen testen.