003 —Migration
Joomla-migratie zonder SEO-verlies: een case na 12 jaar
Een Joomla 2.5-site van 12 jaar oud, drie lagen SEF-plugins diep, naar PHP 8.2-hosting. Zo overleefden de redirects, de database en de rankings.
Het telefoontje kwam van een Belgisch bureau waar we mee samenwerken: een reisuitgever, 3.400 artikelen, Joomla 2.5.28, draaiend op shared hosting waarvan de hostingmaatschappij net had laten weten dat PHP 5.6 eind kwartaal uitgefaseerd werd. De site trok nog altijd zo'n 180.000 organische sessies per maand, voornamelijk op long-tail routegidsen die sinds 2014 goed stonden. Het bureau had in oktober een rebuild geoffreerd, de klant had bedankt, en nu was de hosting-deadline nog zes weken weg.
De briefing was smal: verhuis de site naar moderne hosting, zet hem op een ondersteunde PHP-versie, en verlies geen SEO-waarde. Geen redesign. Geen overstap naar WordPress. De Joomla-migratie moest saai zijn, terugdraaibaar, en klaar voor de PHP-cutoff. Zo zag dat eruit, stap voor stap, inclusief wat schuin ging.
De beginnende inventaris
Voordat we iets aanraakten, hebben we een halve dag besteed aan het inventariseren van wat er daadwerkelijk op schijf en in de database stond. Op een site van deze leeftijd zijn de geïnstalleerde extensies de migratie. Core Joomla-upgrades zijn gedocumenteerd; de vijf SEF-plugins die op elkaar gestapeld staan niet.
De shared host gaf ons SSH, met moeite. Eerste ronde:
ssh client@oldhost
php -v
# PHP 5.6.40 (cli)
cd ~/public_html
cat configuration.php | grep -E 'dbtype|host|user|db ='
# mysqli / localhost / [redacted]
ls administrator/components/ | wc -l
# 47
ls plugins/system/ | wc -l
# 22Zevenenveertig admin-componenten en tweeëntwintig system plugins op een 2.5-installatie is archeologie, geen software. We exporteerden de extensies-tabel om te zien wat er echt actief stond:
SELECT name, element, type, enabled
FROM jos_extensions
WHERE enabled = 1
AND type IN ('component','plugin')
ORDER BY type, name;Drie SEF-plugins (search engine friendly URL) stonden tegelijk aan: sh404SEF, de native SEF van Joomla, en iets wat JoomSEF heette en sinds 2015 niet meer was bijgewerkt. Elk schreef URL's op de ander herschreven. De live URL-structuur die de zoekmachines hadden geïndexeerd was de output van alle drie, in volgorde, en geen van de ontwikkelaars die dit had opgezet was nog bereikbaar. Dát is de SEO-waarde: tien jaar Googlebot die een specifieke URL-vorm heeft geleerd die geen enkele component op de server nog from scratch kan reproduceren.
De URL-waarheid vastleggen vóór de verhuizing
De fout bij dit soort migraties van een verouderde site is proberen de redirect-logica vanuit de broncode te begrijpen. Dat gaat je niet lukken. De rewrites zijn emergent. Wat je in plaats daarvan wilt, is een ground-truth lijst van elke URL die Google werkelijk kent, met de HTTP-status en eindbestemming die elk op dat moment retourneert.
We haalden drie bronnen op en voegden ze samen:
- De XML-sitemap die de site serveerde (~3.400 URL's).
- Een Search Console-export van elke URL met minstens één impressie in de afgelopen 16 maanden (~11.800 URL's — veel meer dan de sitemap, zoals verwacht).
- Access logs van de laatste 90 dagen, ge-grep't op 200-GETs met een Googlebot- of Bingbot-user-agent.
Gedupliceerd leverde dat 14.210 canonieke URL's op waar de zoekmachines om gaven. Daarna hebben we ze allemaal zelf gecrawld en de status, de uiteindelijke URL na redirects en de canonical-tag van de pagina vastgelegd:
cat urls.txt | while read url; do
curl -sI -L -o /dev/null -w '%{http_code}\t%{url_effective}\t%{redirect_url}\n' "$url"
done > baseline.tsvDie baseline.tsv is het contract. Na de migratie moet dezelfde crawl op de nieuwe host dezelfde eindbestemmingen opleveren met dezelfde statuscodes (of een 301 naar dezelfde bestemming). Alles wat afwijkt is ranking-verlies. Google's eigen richtlijn over site moves met URL-wijzigingen is de moeite om vooraf door te nemen — het stuk over redirect chains wordt altijd vergeten.
De nieuwe host klaarzetten
De nieuwe hosting was een kleine VPS met PHP 8.2, MariaDB 10.6 en Apache 2.4 met mod_rewrite. Joomla 2.5 draait niet op PHP 8.2. Op PHP 7.4 draait hij ook niet, niet schoon. Het ondersteunde upgrade-pad is 2.5 → 3.10 → 4.x → 5.x, en zelfs de eerste sprong verandert genoeg aan het database-schema om de helft van de third-party extensies te breken.
We kozen de pragmatische route: upgrade naar Joomla 3.10 LTS, die draait op PHP 8.1 met de compatibiliteitsplugin en acceptabel draait op 8.2 met een handvol patches. Joomla 4 had betekend dat we drie custom componenten hadden moeten herschrijven, en dat budget was er niet. 3.10 gaf de klant twee jaar extra en hield het template, de menu's en — cruciaal — de SEF-URL's intact.
De migratie werd uiteraard eerst op staging gedraaid:
# On old host
mysqldump --single-transaction --quick --default-character-set=utf8 \
-u dbuser -p oldjoomla > dump.sql
tar czf files.tar.gz public_html/
# On new host
mysql -u newuser -p newjoomla < dump.sql
tar xzf files.tar.gz -C /var/www/staging/Twee dingen beten ons hier. De oude database was utf8 (drie bytes), geen utf8mb4. Een rechte dump-and-restore in een utf8mb4-database verminkte een handvol artikelen met emoji en oude Windows-1252 smart quotes. We importeerden opnieuw met de originele charset, draaiden de Joomla 3.10-upgrade, en converteerden daarna pas de tabellen naar utf8mb4, nadat we hadden bevestigd dat de upgrade geen tekstkolommen had aangeraakt.
De tweede beet: jos_session had 2,1 miljoen rijen. Het upgrade-script probeerde die te alteren en liep vast. We hebben hem eerst getruncatet. Sessions zijn wegwerp; niemand heeft er ooit een gemist.
Het SEF-ontwarren
Met 3.10 draaiend op staging wezen we een lokale hosts-entry erop en draaiden de baseline-crawl opnieuw. 4.100 URL's retourneerden 404.
Allemaal sh404SEF-gegenereerde URL's met het oude slug-formaat (/reisgidsen/frankrijk/provence-verborgen-dorpjes.html — trailing .html, Nederlandse slug, categorie-dan-slug patroon). De native router van Joomla 3.10 produceerde /reisgidsen/frankrijk/427-provence-verborgen-dorpjes. Ander pad, ID in de slug, geen suffix. Als we dat hadden geshipt, waren tien jaar backlinks weg geweest.
De fix had drie delen.
sh404SEF herinstalleren. De 3.x-compatibele versie bestaat en kan, gelukkig, zijn oude URL-database uit de tabellen van de 2.5-versie importeren. We exporteerden jos_sh404sef_urls uit de oude database, importeerden die in de nieuwe, en lieten sh404SEF de routing weer overnemen. Daarmee werden ongeveer 3.700 van de 404's hersteld.
Statische redirect-map voor de rest. De resterende 400 URL's waren oude JoomSEF-restanten van vóór sh404SEF in 2016 werd geïnstalleerd. Die zitten in geen enkele huidige plugin-database. We hebben de mapping met de hand opgebouwd vanuit het baseline-bestand en in .htaccess geplaatst, boven het Joomla-rewrite-blok:
RewriteEngine On
# Legacy JoomSEF redirects (pre-2016)
RewriteRule ^content/view/(\d+)/\d+/?$ /index.php?option=com_content&view=article&id=$1 [R=301,L]
RewriteRule ^nl/reisgids-(.+)\.htm$ /reisgidsen/$1 [R=301,L,NC]
# Trailing .html on category pages (sh404SEF v2 shape)
RewriteRule ^([a-z-]+)/([a-z0-9-]+)\.html$ /$1/$2 [R=301,L]
# Joomla standard
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L]Canonical afdwingen. De native SEF van Joomla 3.10 produceerde nog altijd een alternatieve URL voor elk artikel (de ID-gebaseerde vorm). We zetten sh404SEF op 301 voor alle niet-canonieke varianten naar de canonieke, en voegden een template-override voor de canonical-tag toe om te garanderen dat de <link rel="canonical"> in de page head overeenkwam. Google pikt het uiteindelijk uit één signaal, maar drie kloppende signalen convergeren sneller.
Zie de Apache mod_rewrite-referentie voor de flag-semantiek; de [L] op de legacy-regels is belangrijk, want zonder vangt de trailing-.html-regel de al herschreven interne requests op en krijg je een loop.
Cutover
DNS-cutover gebeurde op een dinsdag om 04:00 CET. De TTL was de week ervoor al naar 300 seconden verlaagd. De volgorde:
- De oude site in read-only zetten (gebruikersregistratie, reacties, formulieren uitzetten) om 03:00.
- Finale differentiële
mysqldumpvan artikel- en user-tabellen, teruggezet op de nieuwe host. - A-record omzetten.
- De access log op de nieuwe host volgen tot Googlebot aankwam (dat duurde 47 minuten).
- De baseline-crawl opnieuw draaien tegen het live domein.
De crawl kwam terug met 11 niet-matchende URL's. Alle elf artikelen die op de oude site waren bewerkt tussen de laatste volledige dump en de cutover; de content klopte, de modified timestamp niet. Acceptabel.
Wat de rankings deden
De eerlijke cijfers, uit Search Console, 90 dagen na cutover:
- Totaal clicks: −4,2% vs. de 90 dagen ervoor (binnen normale seizoensvariatie voor deze niche).
- Geïndexeerde pagina's: 3.380 → 3.340. Veertig uitgevallen URL's, allemaal pre-2014-artikelen zonder backlinks en met vrijwel nul impressies.
- Gemiddelde positie op de top 500 queries: onveranderd tot +0,3.
- Core Web Vitals: van 18% "good" naar 71% "good" op mobiel, puur door de hostingwisseling.
Geen ranking-inzinking. Het werk gebeurde vóór de cutover, niet erna.
De tool-vraag
Het traagste deel van deze klus was niet de dump of de rewrites. Het waren de kleine SQL-queries tegen de oude database om te begrijpen wat er stond — welke extensies echt in gebruik waren, welke artikelen non-ASCII-karakters bevatten, welke user-accounts sinds 2017 slapend waren — en aantekeningen bijhouden van wat we veranderden en waarom, zodat het rollback-plan eerlijk bleef.
Toen we Pier bouwden, waren de Joomla- en Drupal-migraties die we de jaren ervoor hadden gedaan precies de use case. Een dockende MySQL-editor naast de bestandsstructuur, met versiegeschiedenis op elke bewerking, blijkt te zijn wat dit werk werkelijk nodig heeft: geen framework, geen dashboard, alleen de twee panelen tegelijk open met een audit trail eronder.
Eén ding dat je vandaag kunt doen
Als je een verouderde Joomla- of Drupal-site hebt op een host die blijft zeuren over PHP-versies, trek nu alvast een baseline-crawl — sitemap plus Search Console-export plus 90 dagen aan access logs, gedupliceerd, met statuscodes vastgelegd. De migratie is makkelijker als je de vorm van het ding dat je niet mag breken al kent.
— Vragen —
Kun je Joomla 2.5 direct naar Joomla 5 migreren?
Niet schoon. Het ondersteunde pad is 2.5 → 3.10 → 4.x → 5.x, en de meeste third-party extensies breken tussen major versies. Voor oudere sites is 3.10 LTS vaak de pragmatische halte.
Behouden 301-redirects de PageRank?
Google heeft bevestigd dat 301-redirects volledige link-equity doorgeven. In de praktijk hangt ranking-stabiliteit meer af van het compleet houden van de URL-set en het ongewijzigd laten van de content dan van het redirect-type zelf.
Moet ik tijdens de migratie utf8 naar utf8mb4 converteren?
Ja, uiteindelijk wel — utf8 in MySQL is drie bytes en kan geen emoji of sommige CJK-karakters opslaan. Doe het ná de Joomla-versie-upgrade, niet ervoor, om collation-conflicten halverwege de upgrade te voorkomen.