← Blog

Joomla

Joomla 3 naar 5 migreren: het K2 en Falang draaiboek

Om 23:00 in februari staarde een Joomla 3.10 EOL banner de marketing lead van een toeristensite met 2.800 artikelen aan. Dit is het draaiboek dat we daarvandaan draaiden.

Jacob Molkenboer· Oprichter · A Brand New Company· 1 jan 2025· 11 min
Leren logboek half open op ivoren bureau, koperen sleutel op crème kaart, groen papieren tabje, rood zegel.

Om 23:00 op een dinsdag in februari stuurde de marketing lead van een stedelijke toeristendienst ons een screenshot van haar Joomla admin door. Versie 3.10.12. Bovenaan een rode banner met de tekst this release has reached end of life. Daaronder 2.800 artikelen in drie talen, een K2 catalogus van zo'n 600 locaties met zeven custom fields per stuk, en een Falang routing-laag waar sinds 2019 niemand meer naar gekeken had. De hosting provider gaf haar 90 dagen voordat ze een PHP-upgrade zouden forceren die de site niet zou overleven.

We hebben dit soort migraties vaak genoeg gedraaid om er een checklist voor bij te houden. Deze post is die checklist, uitgeschreven, met de onderdelen die bijten erbij. Zit je voor dezelfde rode banner, dan kun je hem gebruiken.

De afgrond waar je op staat

Joomla 3.10 bereikte end of life op 17 augustus 2023. De officiële aankondiging is duidelijk: geen security patches meer, geen bugfixes meer, geen compatibility-garanties meer. Joomla 5.0 verscheen in oktober 2023 en vereist PHP 8.1 als harde ondergrens. Joomla 3 voelde zich het lekkerst op PHP 7.4 en tolereerde 8.0 als je hem bij de hand nam.

In dat gat sterven de meeste verouderde Joomla sites stilletjes. De host bumpt PHP naar 8.2 omdat een CVE ze daartoe dwingt, de site geeft een witte pagina, het bureau dat hem in 2017 bouwde is al vier jaar weg, en de marketing lead is een weekend kwijt met het zoeken naar een back-up die nog boot. Welke security advisories er ook landen tussen nu en je migratiedatum, die ga je niet ontvangen, want het Joomla security team publiceert alleen voor ondersteunde branches. Draait je install een kwetsbare Phoca, JCE of Akeeba versie naast core, dan lees je over de exploit op een forum achteraf, niet in je inbox vooraf.

Je kunt niet in één stap van 3.10 naar 5.x. Het ondersteunde pad is 3.10 naar 4.4 naar 5.x. K2 en Falang moeten allebei meehoppen, en allebei kunnen ze breken op manieren die er in de admin prima uitzien en pas drie weken later op een publieke URL opduiken.

Inventariseer voordat je iets aanraakt

De eerste dag van zo'n migratie is read-only. We klonen productie naar een staging subdomein, zetten er basic auth op, en schrijven op wat er daadwerkelijk op de site staat. De meeste klanten zijn verrast door wat er terugkomt. Op deze site stonden 41 extensies geïnstalleerd. Twaalf waren sinds 2018 niet meer geüpdatet. Drie waren forks van dode projecten. Eén was een SOAP client die met een toeristen-API praatte die in 2020 vervangen was door REST en op elk request HTTP 410 teruggaf. Een custom plugin van een al lang vertrokken freelancer hing aan onContentPrepare op elke artikel-load en slikte exceptions stil in, en dat verklaarde meteen waarom het marketingteam al jaren af en toe een lege artikelpagina zag zonder dat iemand de oorzaak vond.

Voor K2 beginnen we met een content audit direct op de database. Deze query laat zien welke custom fields daadwerkelijk werk doen en welke dode schema zijn:

SELECT
  ef.id,
  ef.name,
  ef.type,
  COUNT(DISTINCT i.id) AS items_using
FROM jos_k2_extra_fields ef
LEFT JOIN jos_k2_items i
  ON i.extra_fields LIKE CONCAT('%"id":"', ef.id, '"%')
  AND i.trash = 0
  AND i.published = 1
GROUP BY ef.id, ef.name, ef.type
ORDER BY items_using DESC;

Op de toeristensite gaf deze query 14 custom fields terug. Zeven werden intensief gebruikt. Twee waren nooit op een item ingevuld. Vijf waren spoken uit een redesign van 2018. We verwijderden nog niets, maar het rapport stuurde elke beslissing die volgde: welke velden schoon moesten migreren, welke we stilletjes konden uitfaseren, en welke front-end templates we item voor item moesten hertesten.

De Falang URL lijst die niemand had

Falang is de meertalige extensie van Faboba. Het haakt in op de router van Joomla en herschrijft artikel-aliases per taal. Als het werkt, merk je het niet. Breekt het tijdens een migratie, dan geeft elke Nederlandse en Franse URL op de site stilletjes een 404 en meldt je sitemap groen, want de canonieke Engelse URLs doen het nog prima.

Voordat we ook maar iets aanraakten, bouwden we een benchmark. Het team had drie sitemaps die door OSMap waren gegenereerd, één per taal. We pingden elke URL en sloegen de statuscode op, de eind-URL na redirects, en de SHA1 van de body:

#!/usr/bin/env bash
# Benchmark every Falang-routed URL before the migration
set -euo pipefail

for lang in nl fr en; do
  curl -s "https://www.example.be/sitemap-${lang}.xml" \
    | xmllint --xpath '//*[local-name()="loc"]/text()' - \
    | tr ' ' '\n' \
  > urls-${lang}.txt

  xargs -a urls-${lang}.txt -P 8 -I {} sh -c '
    code=$(curl -o body.tmp -s -w "%{http_code}" -L "$1")
    sha=$(sha1sum body.tmp | cut -d" " -f1)
    printf "%s\t%s\t%s\n" "$code" "$sha" "$1"
  ' _ {} \
  > baseline-${lang}.tsv
done

De baseline kwam terug met 8.412 URLs verdeeld over drie talen. 27 daarvan gaven al een 404 in productie. Die gingen in een log en negeerden we verder. Al het andere werd het contract: na de migratie moest elke URL ofwel 200 teruggeven op hetzelfde canonieke pad, ofwel ergens zinnigs naar 301-geredirect worden. De SHA1 kolom is net zo belangrijk als de statuscode. Een URL die stilletjes de taal-fallback teruggeeft in plaats van de vertaalde content levert nog steeds 200, en de enige manier waarop je dat ontdekt is dat de body hash tussen baseline en post-migratie verschuift.

Het versiepad

Joomla 3.10 naar Joomla 4.4 is de pijnlijke stap. K2 versies vóór 2.11 booten niet op Joomla 4. Falang stopte in 2023 met het uitleveren van een Joomla 3 build en levert nu alleen nog J4 en J5 builds. Alle custom code die JFactory, JRequest of MooTools aanraakt moet herschreven worden voordat je op de update-knop drukt.

Waarschuwing

Draai de ingebouwde 3 naar 4 updater van Joomla niet voordat elke geïnstalleerde extensie een bevestigde J4-compatibele release heeft. De updater rondt gewoon af met incompatibele extensies nog geïnstalleerd, en je houdt een site over die boot in een fatal error en een database die half geconverteerd is.

Onze pre-flight op de toeristensite kostte acht extensies meteen het leven. Vijf hadden een J4 build die we konden installeren. Drie werden in-house herbouwd als kleine custom components, want de oorspronkelijke maintainers waren weg. Eén was een SuperUser-only utility die we vervingen door een CLI script van 12 regels. Twee andere extensies waren officieel J4-compatibel maar trokken jQuery-globals binnen vanaf een hardcoded CDN-pad; die vervingen we door de meegeleverde Joomla-versie en we verloren een flits van ongestylde content waarvan het marketingteam aannam dat het een hosting-bug was.

Vanaf daar ligt de volgorde vast:

  1. Bump PHP op staging naar 8.1 met de J3 site nog geïnstalleerd. Fix elke deprecation die afgaat.
  2. Upgrade K2 naar de laatste J3-compatibele 2.x build. Bevestig dat de catalogus nog rendert.
  3. Upgrade Falang naar de laatste J3-compatibele build. Draai de URL benchmark opnieuw.
  4. Draai de Joomla 3 naar 4 update vanuit de admin. Loop tien minuten weg.
  5. Herinstalleer K2 met de J4 build eroverheen. K2 levert zijn eigen updater die de schema delta afhandelt.
  6. Herinstalleer Falang met de J4 build. Importeer de taaldefinities opnieuw als Falang erom vraagt.
  7. Draai de J4 naar J5 update. Die verloopt meestal stil als stap 4 schoon was.

Joomla 5 levert een echte CLI console mee. Na de eerste sprong draaide elke volgende actie vanaf de command line:

php cli/joomla.php core:check-updates
php cli/joomla.php extension:installfile /tmp/com_k2_v2.11.5.zip
php cli/joomla.php extension:installfile /tmp/com_falang_v3.0.2.zip
php cli/joomla.php finder:index
php cli/joomla.php session:gc

Tussen elke stap maakten we een volledige mysqldump en een tarball van de codebase. Disk is goedkoop, en de derde keer dat je een half geconverteerd schema met de hand moet terugdraaien, leer je die twee minuten in een snapshot te investeren.

De K2 custom fields valkuil

K2 slaat custom field-waardes op als een geserialiseerde blob in jos_k2_items.extra_fields. Oudere K2 builds schreven het in een eigen string-formaat dat ergens tussen PHP serialize() en pipe-gescheiden waardes in zit. De 2.11 build stapte over op een schone JSON array. Het K2 maintenance paneel converteert het oude formaat naar het nieuwe zodra je een resave triggert, maar het read-pad in de nieuwe build vangt het oude formaat niet netjes op: het probeert json_decode, krijgt null terug, en rendert het veld als leeg. Redacteuren merken het een week lang niet, want de front-end valt terug op de body van het artikel.

De verdediging is een smoke test die je na elke migratiestap draait. We schreven een kleine PHP CLI die K2's eigen model laadt en valideert of een steekproef van 50 items nog steeds de verwachte velden teruggeeft:

<?php
// cli/k2-smoke.php — run from Joomla root
define('_JEXEC', 1);
define('JPATH_BASE', __DIR__ . '/..');
require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

$app = Joomla\CMS\Factory::getApplication('site');
$db  = Joomla\CMS\Factory::getDbo();

$ids = $db->setQuery(
  'SELECT id FROM #__k2_items WHERE published=1 AND trash=0 ORDER BY RAND() LIMIT 50'
)->loadColumn();

$failures = 0;
foreach ($ids as $id) {
  $item = K2Items::getInstance()->getItem($id);
  if (empty($item->extra_fields) || !is_array($item->extra_fields)) {
    fwrite(STDERR, "item {$id}: extra_fields empty\n");
    $failures++;
  }
}
exit($failures > 0 ? 1 : 0);

Die smoke test draaiden we na elke stap. De eerste keer dat hij faalde was na de J4 stap. K2 had zijn field-schema gebumpt en eiste een eenmalige resave van elk item om de blob opnieuw weg te schrijven. Het K2 admin heeft daar een knop voor onder Maintenance. Op 600 items draaide dat in 90 seconden. We draaiden de smoke test opnieuw, kregen een schone exit code, en gingen verder.

De Falang routes die niemand benchmarkte

Falang in J4 en J5 ging anders om met de nieuwe Joomla router. De grootste zichtbare verandering: aliases die eerder hoofdletterongevoelig waren, werden hoofdlettergevoelig op bepaalde serverconfiguraties. De site had een Nederlandse URL die /nl/Bezoekers/parkeren luidde, met een hoofdletter B die in 2017 ooit door een menu-edit was ingeslopen. Na de J4 stap gaf die URL een 404 en deed de lowercase versie het wel.

In de admin viel ons dit niet op. We zagen het wel doordat de baseline die we aan het begin gebouwd hadden 31 URLs aangaf waarvan de statuscode stilletjes verschoven was. Achtentwintig daarvan waren het hoofdletter-probleem. Die losten we op met één rewrite-regel in de vhost config (Apache honoreert RewriteMap alleen in server- of vhost-context, niet in .htaccess):

# In the site's vhost config, NOT in .htaccess
<VirtualHost *:443>
    ServerName www.example.be
    RewriteEngine On
    RewriteMap lowercase int:tolower
    RewriteCond %{REQUEST_URI} [A-Z]
    RewriteRule ^(.*)$ ${lowercase:$1} [R=301,L]
</VirtualHost>

Heb je geen controle over de vhost (shared hosting, managed Joomla platforms), dan is de fallback een platte redirect-lijst in .htaccess met één Redirect 301 per geraakte URL. Lelijk, maar het werkt en het staat in vijf minuten live.

De drie andere afgedwaalde URLs waren Falang taaldefinities die in 2019 met de hand waren aangepast om te wijzen naar een microsite die niet meer bestond. Die kregen een nette 301 in een redirect-tabel. We vonden ook twee menu-items in het Nederlandse menu die gekoppeld waren aan een Falang taaldefinitie waarvan het bronartikel jaren geleden in de prullenbak was beland; de J4 router weigerde überhaupt de route te bouwen en gaf een 500 terug. Die fixten we in vijf minuten zodra de benchmark ze zichtbaar maakte.

De staging-dans

We draaien zo'n migratie nooit live op productie. De flow is:

  • Productie naar staging klonen op vrijdagavond. Achter basic auth zetten.
  • De volledige migratie draaien op staging op zaterdag en zondag.
  • De URL benchmark opnieuw draaien. Diffen met de baseline. Elke rode regel fixen.
  • Staging maandagochtend overdragen aan de klant voor content review.
  • Live zetten op dinsdagavond tijdens het stilste verkeersuur.

De cutover zelf is klein. We bevriezen de productie-database, maken een laatste mysqldump, herstellen die op staging, draaien de migratiescripts nog één keer end-to-end (inmiddels warm en getest), swappen DNS, en houden een uur lang het access log in de gaten. De benchmark draait opnieuw vanaf het publieke internet zodra DNS resolved. Wat afdwaalt krijgt vóór zonsopgang een redirect-regel.

Het rollback-plan is één regel: hou de bevroren productie-DB en de codebase tarball klaar op een warme standby-host met DNS gericht op hetzelfde IP. Als er in het eerste uur iets misgaat dat we niet live kunnen patchen, draaien we DNS terug, verliezen we hooguit een uur aan edits (de bevriezing voorkwam writes), en plannen we opnieuw. We hebben deze ontsnappingsroute in ongeveer dertig migraties precies één keer gebruikt, op een site waar een externe reserverings-widget een vast Joomla 3 admin-pad laadde. Weten dat hij er is, is meer waard dan de kans dat je hem inzet.

Take-away

Het migratie-risico zit niet in Joomla. Het zit in K2's stille schema delta en in de URL aliases van Falang. Benchmark elke publieke URL voor je begint, en behandel die benchmark als het contract.

Wat we op de toeristensite opleverden

Elf werkdagen van kickoff tot cutover. 8.412 URLs gebenchmarkt, 31 redirects toegevoegd, 8 extensies eruit gehaald, 3 kleine in-house vervangingen gebouwd. K2 ging mee met alle zeven live custom fields intact. Falang nam alle drie de talen mee zonder dat één artikel zijn vertaalde slug verloor. De marketing lead kreeg haar admin terug zonder de rode banner, en PHP 8.2 was geen doodsbedreiging meer. Als bijwerking van het droppen van de dode extensies en het overstappen op de meegeleverde jQuery zakte de Largest Contentful Paint van de homepage van 3,4s naar 1,9s op een cold cache, wat het team in Search Console al opmerkte voordat wij het ze konden vertellen.

Dit was een legacy migratie die we eerder dit jaar onder deadlinedruk opleverden. Wat we tegenkwamen en in geen enkele documentatie stond, was dat de K2 2.10 naar 2.11 blob-format wijziging pas ná de J4 stap beet, niet ervoor. We losten het op met de smoke test hierboven en een one-click resave in K2's eigen maintenance paneel. Draai je een Joomla site waar in de admin-footer nog 3.x staat, dan is het kleinste nuttige wat je vandaag kunt doen het URL benchmark script tegen je sitemap draaien en het resultaat opschrijven. Zodra je weet wat je hebt, volgt de rest van het draaiboek vanzelf.

Kern

Joomla 5 zelf breekt de migratie zelden. De stille custom-field schema delta van K2 en de URL aliases van Falang doen dat wel. Benchmark eerst elke URL.

FAQ

Kun je rechtstreeks van Joomla 3.10 naar Joomla 5 updaten?

Nee. Het ondersteunde pad is 3.10 naar 4.4 naar 5.x. Je moet eerst landen op de laatste 4.4 release en bevestigen dat de site boot voordat je de 5.x update aanzet.

Overleeft K2 een Joomla 5 migratie?

Ja, mits je K2 voor de Joomla 4 stap upgrade naar 2.11 of hoger en elk item één keer opnieuw opslaat zodra K2 daarom vraagt. De schema-wijziging op het extra_fields veld van items is de meest voorkomende stille fout.

Is er nog een Joomla 3 build van Falang?

Nee. Faboba liet de Joomla 3 build in 2023 vallen. Je moet de J3 naar J4 stap zetten met de laatste compatibele Falang versie, en daarna de J4 of J5 build eroverheen installeren.

Welke PHP versie vereist Joomla 5?

Joomla 5 vereist minimaal PHP 8.1. PHP 8.2 wordt aanbevolen. De Joomla 3.10 site waar je vandaan migreert draait meestal op PHP 7.4 of 8.0.

Hoe lang duurt zo'n migratie van begin tot eind?

Voor een site van 2.800 artikelen met K2 en Falang reken op elf werkdagen, van inventarisatie tot cutover, met het zware werk geconcentreerd in één weekend op staging.

joomlamigrationlegacy sitesphpmysqlseo

Iets bouwen?

Start een project