Migration
PrestaShop naar WooCommerce: 60k SKU's zonder 404's
Een PrestaShop 1.7-winkel met 60.000 SKU's moest in acht weken naar WooCommerce, zonder kapotte URL's. Dit is het redirect-playbook dat we bouwden, en wat het bijna brak.

De admin van PrestaShop 1.7 laadde nog, maar het officiële supportvenster was achttien maanden eerder gesloten en de hostingprovider had de klant een datum gegeven. Acht weken om de catalogus, de checkout en 60.237 geïndexeerde URL's naar WooCommerce te verhuizen. De briefing was één zin: "geen 404's, geen rankingverlies, geen verrassingen."
Dat is de briefing die telt bij elke migratie van PrestaShop naar WooCommerce. De productdata verhuizen gaat prima. De orders verhuizen gaat prima. URL's zijn waar deze projecten misgaan, want het friendly URL-patroon van PrestaShop en de permalink-structuur van WooCommerce hebben vrijwel niets gemeen, en Google heeft jarenlange equity opgebouwd in URL's die eruitzien als:
/12345-product-name.html
/category-name/12345-product-name.html
/content/7-about-usWooCommerce wil standaard:
/product/product-name/
/product-category/category-name/
/about-us/Vermenigvuldig dat over 60.000 productpagina's, 1.400 categoriepagina's, 320 CMS-pagina's, fabrikantroutes, attribuutcombinaties en paginering, en je hebt ongeveer 70.000 redirects te schrijven, te testen en live te zetten zonder dat er iets breekt in productie. Zo deden we het voor deze klant.
De inventarisatie komt vóór de migratie
Week één was geen migratie. Het was een audit. We trokken drie datasets parallel binnen voordat we ook maar één productregel aanraakten:
- Elke URL die Google de afgelopen 18 maanden had geïndexeerd, via de Pages-export van Search Console en de URL Inspection API.
- Elke URL met inkomende links, via een Ahrefs-export plus een verse Screaming Frog-crawl van de live site.
- Elke URL met een echte hit in de laatste 90 dagen, uit de Cloudflare-logs die de klant had bewaard.
Drie exports, drie CSV's, gededupliceerd tot één master-inventaris van 71.842 unieke URL's die de nieuwe site moest honoreren. Dat is het getal dat in het contract hoort, niet "60.000 producten". Als je een PrestaShop-migratie scoopt op SKU-aantal, mis je een derde van de redirects.
De reden dat de inventarisatie de database verslaat, is dat PrestaShop meer URL-varianten genereert dan zijn producttabel suggereert. Attribuut-gefilterde categoriepagina's, fabrikantroutes, layered-nav-permalinks en de oude ?id_product=-querystrings moeten allemaal in kaart. Crawl de live site, crawl daarna de logs. Vertrouw de database niet.
De URL-map ís de migratie
De belangrijkste deliverable was één CSV met twee kolommen: old_url en new_url. Elke regel in dat bestand is een contract met Google. We bouwden 'm in drie passes.
Pass één: productslugs
PrestaShop product-URL's zetten het numerieke product-ID vóór de slug. WooCommerce niet. Het goede nieuws is dat de slug zelf (het leesbare deel) meestal identiek is of dichtbij genoeg om op naam te matchen. Het slechte nieuws is dat PrestaShop dubbele slugs toestaat in verschillende categorieën, en WooCommerce niet. We vonden 412 slug-collisions in de catalogus.
De resolutieregel die we met de klant afspraken: hou de variant met het hoogste verkeer op de canonieke slug, en plak bij de verliezer de fabrikant achteraan. De mapping-query tegen de staging WooCommerce-database zag er ongeveer zo uit:
SELECT
ps.id_product,
ps.link_rewrite AS old_slug,
wc.post_name AS new_slug,
CONCAT('/', ps.id_product, '-', ps.link_rewrite, '.html') AS old_url,
CONCAT('/product/', wc.post_name, '/') AS new_url
FROM prestashop_product_lang ps
JOIN wp_posts wc
ON wc.post_title = ps.name
AND wc.post_type = 'product'
WHERE ps.id_lang = 1;Dat leverde 59.891 zekere matches op. De resterende 346 gingen naar een spreadsheet die een mens regel voor regel doorliep. Daar is geen shortcut voor.
Pass twee: categoriebomen
PrestaShop-categorieën dragen ID's in de URL als "Categorieën in URL" aanstaat, en de categorieboom zelf is vaak gedenormaliseerd vergeleken met wat merchandising in WooCommerce wil. We namen de live boom, mapten 'm op de nieuwe boom die de klant voor de relaunch had getekend, en produceerden een one-to-many map waarbij één oude categorie soms naar twee of drie nieuwe redirect.
Als dat gebeurt, kies de nieuwe bestemming met het hoogste verkeer uit analytics en accepteer dat de secundaire categorieën hun directe redirect verliezen. Slim proberen te zijn met multi-destination-logica (geo-aware, query-string-aware, cookie-aware) is hoe migraties vier weken uitlopen.
Pass drie: de long tail
CMS-pagina's, fabrikantenpagina's, leverancierspagina's, de /best-sales- en /new-products-controllers, zoekresultaat-URL's die per ongeluk geïndexeerd raakten, de oude /blog/-boom van een vorig platform. Elk daarvan had een expliciete regel nodig. De saaie waarheid van een schone migratie is dat de laatste 5% van de URL's 30% van de tijd opvreet.
Redirect-implementatie
Met 71.842 mapping-regels zet je redirects niet in een WordPress-plugin. We testten de bekende Redirection-plugin eerst op staging, en die slikte het niet boven 40.000 regels. Elke request hitte de database voor een lookup tegen een geïndexeerde maar zeer grote tabel, en TTFB ging van 180ms naar 720ms.
De juiste plek voor deze redirects is de webserver. De klant draaide nginx vóór PHP-FPM, wat map-directives tot de natuurlijke keuze maakte:
map_hash_max_size 262144;
map_hash_bucket_size 256;
map $request_uri $new_url {
default "";
"/12345-classic-walnut-chair.html" "/product/classic-walnut-chair/";
"/home/12345-classic-walnut-chair.html" "/product/classic-walnut-chair/";
# 71,840 more lines
}
server {
listen 443 ssl http2;
server_name example.com;
location / {
if ($new_url) {
return 301 $new_url;
}
try_files $uri $uri/ /index.php?$args;
}
}nginx laadt die map bij opstart in het geheugen. Lookups zijn O(1). De extra latentie voor een redirect op schaal is een paar milliseconden. Het hele map-bestand woog 7,4 MB. De reload van de nginx-config duurde ongeveer 900ms op de productiemachine, wat we twee keer op staging hadden gerepeteerd voordat we het echt deden.
Als je redirect-tabel groter is dan 10.000 regels, haal 'm uit WordPress en zet 'm in de webserver. Plugins zijn prima voor de editoriale long tail. Bulk-redirects horen boven PHP.
Apache en LiteSpeed
Voor Apache is het equivalent een RewriteMap met een gehasht DBM-bestand:
RewriteEngine On
RewriteMap redirects "dbm:/etc/apache2/redirects.dbm"
RewriteCond ${redirects:$1|NOT_FOUND} !NOT_FOUND
RewriteRule ^(.+)$ ${redirects:$1} [R=301,L]Je bouwt het .dbm-bestand op uit dezelfde CSV met httxt2dbm. LiteSpeed hergebruikt de Apache-syntax. Hoe dan ook, het principe is identiek: lookup-tabellen in de server, niet in PHP.
De verificatieloop
Je kunt 71.842 redirects niet op vertrouwen live zetten. Het verificatieharnas dat we schreven deed elke nacht drie dingen in de twee weken voor cutover:
- Trek de master-inventaris uit de bron van waarheid, een Postgres-tabel die het migratieteam beheerde.
- Hit elke oude URL tegen de staging-omgeving met een HEAD-request, met als verwachting een 301 naar de gemapte target.
- Hit elke nieuwe URL met een GET, met als verwachting een 200 en een niet-lege
<title>.
Het is een saai script. Het is ook het enige dat de edge cases vangt. De eerste run vond 1.847 missers. Ongeveer de helft waren trailing-slash mismatches, een kwart waren URL-encoded tekens in productnamen (umlauts, ampersands, de losse à), en de rest waren echte mapping-bugs die we handmatig fixten. Bij de vierde nachtelijke run zaten we onder de 50 missers, allemaal bekend en getrieerd.
Hier is de kern van de checker, geschreven als een klein Node-script dat elke operations lead kan lezen:
import fs from "node:fs";
import { parse } from "csv-parse/sync";
const rows = parse(fs.readFileSync("redirects.csv"), { columns: true });
const base = "https://staging.example.com";
const failures = [];
for (const { old_url, new_url } of rows) {
const res = await fetch(base + old_url, { redirect: "manual" });
const location = res.headers.get("location");
if (res.status !== 301 || !location?.endsWith(new_url)) {
failures.push({
old_url,
expected: new_url,
got: location,
status: res.status,
});
}
}
console.log(`${failures.length} failures of ${rows.length}`);
fs.writeFileSync("failures.json", JSON.stringify(failures, null, 2));Draai het door een queue met 50 workers en 71.842 URL's zijn in minder dan twintig minuten gecheckt.
Cutover-dag
Cutover was op een dinsdag om 03:00 CET. De volgorde:
- Bevries de PrestaShop-admin. Geen nieuwe orders, geen catalogus-edits.
- Draai een laatste delta-sync van orders en klanten naar de WooCommerce-database.
- Swap DNS naar de nieuwe server. De TTL stond een hele week op 300 seconden.
- Kijk hoe het
access.logvan nginx op de oude machine in ongeveer acht minuten leegloopt. - Bevestig dat de redirect-map geladen was op de nieuwe machine, draai een 100-URL spotcheck, en zet daarna de checkout weer open.
De eerste 404-melding kwam binnen om 04:12 CET. Het was een fabrikant-URL-patroon dat we hadden gemist (/marque/, Frans, op een Nederlandstalige site, uit een theme dat de klant in 2019 had geïmporteerd). Drieënveertig URL's in totaal, gefixt in een vervolg-reload van de nginx-map om 04:35.
Eindstand een week na launch: 13 niet-gemapte URL's gevonden in Search Console, allemaal van vergeten campagne-landingspagina's. Het organische verkeer in week vier zat binnen 3% van de pre-migratie-baseline, wat voor een platformwissel in e-commerce zo dicht bij een schoon resultaat komt als je ooit gaat zien.
Wat we de volgende keer anders zouden doen
Twee dingen. Ten eerste zouden we de URL-inventaris eerst uit de CDN-logs bouwen en pas daarna uit de database. De Cloudflare-logs brachten URL-patronen aan het licht waarvan we niet wisten dat ze bestonden: oude mobile-subdomain rewrites, een oud Magento-pad dat een Russische aggregator nog steeds hitte, twee PDF's in de upload-folder die rankten op high-intent queries. De database vertelt je wat het platform denkt dat er bestaat. De logs vertellen je wat het internet denkt dat er bestaat.
Ten tweede zouden we het verificatieharnas vanaf dag één draaien, niet vanaf dag veertig. Er is geen reden waarom het niet tegen staging kan draaien terwijl de mapping nog 40% compleet is. De failure-count zien zakken van 70.000 naar 50 over vijf weken is nuttiger dan 1.847 issues ontdekken in de laatste week voor launch.
Toen we deze migratie draaiden voor een woonwinkel in de Benelux, was wat ons bijna nekte de layered-navigation-URL's die PrestaShop genereert voor filtercombinaties. We losten het op met een regex catch-all die elke ?selected_filters=-querystring naar zijn parent-categorie mapte, die Google vervolgens binnen twee weken opnieuw crawlde en consolideerde. Als je voor een vergelijkbare verouderde migratie staat, zijn de URL-inventaris en de verificatieloop waar je 80% van het budget aan moet besteden.
Trek vanmiddag de Pages-export uit Search Console. Diff 'm tegen je sitemap.xml. Het gat tussen die twee bestanden is het project dat je nog niet hebt gescoped.
Kern
Als je redirect-tabel groter is dan 10.000 regels, haal 'm uit WordPress en zet 'm in de webserver. Plugins doen de editoriale long tail; bulk-redirects horen boven PHP.
FAQ
Hoe lang duurt een migratie van 60k producten van PrestaShop naar WooCommerce?
Acht tot twaalf weken als je scoopt op URL-aantal, niet op SKU-aantal. De datamigratie is het snelle deel. URL-mapping, redirects en verificatie vullen het schema.
Kan ik niet gewoon de Redirection-plugin gebruiken voor bulk-redirects?
Tot ongeveer 10.000 regels, ja. Daarboven verhuis je de tabel naar nginx of Apache. Plugin-redirects voegen 200 tot 500ms latentie per request toe op grote schaal.
Verlies ik SEO-rankings tijdens de verhuizing?
Niet als elke geïndexeerde oude URL binnen de eerste 24 uur een 301 teruggeeft naar een relevante nieuwe URL. Verwacht een dip van 5 tot 15% in week twee. Schone migraties herstellen binnen vier tot zes weken.
En de productafbeeldingen en hun URL's?
Verhuis ze waar mogelijk met dezelfde bestandsnaam en mapstructuur. Als het pad moet veranderen, voeg image-level redirects toe in dezelfde nginx-map, of accepteer de tijdelijke re-crawl-kost.