← Blog

Joomla

Van Joomla 3.10 naar Strapi: base64-K2-val kostte 10 dagen

Dag drie van een Joomla 3.10-uitfasering: onze redirect map werd stil. Geen 404, geen errors. Een K2-plugin loste base64-JSON op tijdens runtime, en niemand wist het.

Jacob Molkenboer· Oprichter · A Brand New Company· 24 jul 2025· 8 min
Open leren logboek met handgeschreven pagina's, koperen sleutel op kaartje, stempel, groen lint op ivoor linnen.

Een dinsdagmiddag in maart opende onze project manager Slack en stelde, met de bestudeerde rust van iemand die het antwoord al weet, één vraag: "Gaan we dit weekend live?" De team lead op een migratie van Joomla 3.10 naar Strapi staarde naar een redirect map van 612 regels die, tegen alle logica in, het in staging deed en stilletjes brak in productie. We zaten op dag drie. We zouden pas twee weken later live gaan.

De klant was een SaaS-leverancier uit Utrecht, 19 mensen. Volwassen product, 8.000 organische bezoeken per maand, een marketingsite die sinds 2014 in leven werd gehouden door drie opeenvolgende freelancers. Ze hadden ons ingehuurd om de Joomla 3.10-stack uit te faseren voordat de volgende ronde CVE's hen zou inhalen. Joomla 3 bereikte end of life in augustus 2023, en Akeeba was gestopt met het backporten van fixes waar de klant op kon bouwen. We kozen Strapi als nieuw CMS en Astro voor de marketingsite, dezelfde setup die we dit jaar al voor een half dozijn bureaus hebben opgeleverd.

De site die we erfden

Joomla 3.10 met K2, een content construction kit die populair was tussen ruwweg 2012 en 2018. K2 wordt nog steeds onderhouden door JoomlaWorks, maar de meeste teams die er in 2014 op bouwden, komen niet meer aan de integratiecode. De site van onze klant had K2-categorieën, K2-items, K2 extra fields, plus vier custom plugins geschreven door een freelancer die in 2017 vertrok. Geen van de plugins had een README. Twee hadden geen comments. Eén was via FTP rechtstreeks in productie aangepast, wat we ontdekten omdat de modificatietijd van het bestand op de server drie jaar nieuwer was dan dezelfde file in de git-repo van de klant.

De catalogus: 412 artikelen, 38 categoriepagina's, 22 landingspagina's, ongeveer 600 oude URL-aliassen die zich door redesigns hadden opgestapeld. SEO-verkeer verdeeld over de long tail, precies de vorm waarbij elke URL-misstap je oplopend omzet kost.

Het plan dat had moeten werken

Standaardaanpak. We hadden dit zes of zeven keer eerder gedaan.

  1. Crawl de live Joomla-site en leg elke URL vast die 200 teruggeeft, plus de canonical en de uiteindelijke bestemming.
  2. Exporteer de K2-content via de JSON API in een genormaliseerde vorm die Strapi kan inlezen.
  3. Bouw de Astro-site route-voor-route tegen de Strapi-staging.
  4. Genereer een Nginx redirect map uit de URL-inventaris.
  5. Verleg DNS in één weekend.

We crawlden. We exporteerden. We bouwden. We zetten de staging tegen het live Joomla zodat het team afwijkingen kon spotten vóór de cut-over. Tegen dag drie was de staging-build pixel-gelijk op elke URL die we testten. We hadden een redirect map van 612 regels. We lagen op schema.

Toen vroegen we onze SEO-specialist een laatste crawl op staging te draaien, met de oude site gesimuleerd offline. Zevenenveertig URL's gaven 404 terug. Geen daarvan stond in onze redirect map. Geen daarvan stond in de menu manager van Joomla, in de K2-item-tabel of in de .htaccess van de site.

Waar de URL's écht woonden

De zevenenveertig URL's waren oude campagne-landers uit 2015 tot 2018. Ze waren al jaren naar huidige pagina's geredirect. Ze leverden ongeveer een derde van het verkeer naar de pricingpagina op. We zagen de redirects in productie afgaan. We konden de bron niet vinden.

We deden eerst de voor de hand liggende dingen. Grep door de codebase op de URL-slugs. Niets. Grep door de database-dump. Niets zichtbaars. .htaccess openen, elke RewriteRule bekijken. Niets dat matchte. De tabel #__redirect_links van Joomla checken. Leeg.

De doorbraak kwam van een junior in het team die naar de database wees en vroeg: "Wat is dit?" De #__extensions-rij voor een plugin met de naam plg_system_legacyseo had een params-veld van 84 KB. Daarin zag één key er zo uit:

{
  "canonical_map": "eyJ2MSI6eyJyb3V0ZXMiOlt7ImZyb20iOiIvcHJpY2luZy1xY3AtMjAxNiIsInRvIjoiL3ByaWNpbmciLCJjb2RlIjozMDF9XX19",
  "version": "1.2.0",
  "last_updated": "2017-11-04T14:22:08Z"
}

De waarde van canonical_map was een base64-encoded JSON-object met de redirect-regels. De plugin decodeerde het tijdens runtime, bij elke request, matchte de huidige URL tegen de patronen en stuurde een 301 terug. In die blob zaten 247 regels. Twaalf waren kapotte patronen die simpelweg nooit ergens op matchten. Zevenenveertig waren de ontbrekende URL's.

Waarschuwing

Audit je een Joomla-site voor migratie? Dump dan het params-veld van elke plugin en grep erdoorheen. Verouderde plugins stoppen daar standaard routing-regels, feature flags en API-tokens in, soms encoded.

Waarom die base64-wrapper er zat

Hiervoor hebben we genoeg van de commit-historie van de freelancer gereconstrueerd (uit een tarball-back-up die de klant op een NAS vond). De originele plugin sloeg regels op als plain JSON. In 2016 voegde de freelancer ondersteuning toe voor redirect-regels die ampersands en query strings met haakjes bevatten. De params-opslag van Joomla wikkelde waarden destijds in een JSON-in-JSON-in-INI-structuur, en bepaalde tekens triggerden bij opslaan in het adminformulier een dubbele htmlentities-encoding. De fix van de freelancer was om de binnenste blob base64 te encoden vóór opslag. Het werkte. Hij ging door met andere dingen.

Negen jaar later hadden drie eerdere migratieprojecten deze map niet opgemerkt. De reden: de K2-exporttools, de redirect manager van Joomla en elk kant-en-klaar Joomla-auditscript dat we kennen (waaronder die van Akeeba) lezen de redirect-tabellen, niet de plugin-params. Tenzij je elke plugin afgaat en de config inspecteert, vind je het niet.

De kosten: tien dagen

Dag vier tot zes: we joegen de verkeerde hypothese na. We gingen ervan uit dat de ontbrekende URL's in een of ander K2 extra field zaten of in een #__menu-alias die we niet hadden geparset. We herschreven onze crawler twee keer. Achtenveertig uur niets.

Dag zeven: we vonden de plugin. De rest van die dag besteedden we aan het doorgronden van de resolver, omdat de regels regex-patronen bevatten en een volgorde van precedentie die ertoe deed.

Dag acht: we decodeerden de blob, parseerden de regels en haalden ze door een dedup-pass tegen de redirects die we al hadden. We schreven een klein PHP-scriptje dat de resolver nabouwde en testten elke campagne-URL tegen zowel de oude plugin als onze nieuwe Nginx-map. We vonden nog drieëntwintig verschillen die we op dag één niet hadden gezien.

Dag negen tot twaalf: volledige crawl opnieuw draaien, redirect map fixen, SEO-specialist laten tekenen, cut-over inplannen.

Dag dertien: live. De site ging op een vrijdag om 22:30 CET live. Het organische verkeer op de maandag erna lag binnen 4% van de maandag ervoor, en dat is het enige cijfer dat telt als je een CMS verwisselt.

Het script dat we op dag één hadden willen hebben

Dit is het hulpmiddel dat we nu op elke Joomla-site draaien voordat we een migratie offreren. Het dumpt het params-veld van elke plugin, probeert het JSON te decoden, loopt recursief door elke string-waarde en signaleert alles wat ruikt naar base64-encoded JSON of een lijst URL's.

<?php
// audit-joomla-plugin-params.php
// Usage: php audit-joomla-plugin-params.php > report.txt

$db = new PDO('mysql:host=127.0.0.1;dbname=joomla;charset=utf8mb4', 'user', 'pass');
$rows = $db->query("SELECT name, element, params FROM j_extensions WHERE type='plugin'");

foreach ($rows as $r) {
    $params = json_decode($r['params'], true);
    if (!is_array($params)) continue;
    walk($r['element'], $params);
}

function walk($plugin, $node, $path = '') {
    foreach ($node as $k => $v) {
        $p = $path ? "$path.$k" : $k;
        if (is_array($v)) { walk($plugin, $v, $p); continue; }
        if (!is_string($v) || strlen($v) < 40) continue;

        // Base64 of JSON?
        $decoded = @base64_decode($v, true);
        if ($decoded !== false && @json_decode($decoded) !== null) {
            echo "[$plugin] $p: base64 JSON, " . strlen($decoded) . " bytes\n";
            continue;
        }
        // Raw URL list?
        if (preg_match('#https?://|^/[a-z0-9\-/]+$#im', $v)) {
            echo "[$plugin] $p: contains URLs (" . strlen($v) . " bytes)\n";
        }
    }
}

Draai het op een verse database-dump, niet op de live site. Op het Utrechtse project had dit script de canonical_map-blob in minder dan een seconde gesignaleerd. We draaien het nu als allereerste commando in elke Joomla-audit, nog voordat we het adminpaneel openen.

Wat we in onze aanpak veranderden

Drie dingen, geen ervan dramatisch. Het soort procesveranderingen dat je pas doorvoert nadat je tien dagen kwijt bent.

Eén: een Joomla-migratie-intake bevat nu een vaste audit van 90 minuten op de plugin-params. We greppen, decoden en inventariseren elke configblob in de plugins-tabel, de modules-tabel en de templates-tabel. We gaan ervan uit dat elke plugin geschreven vóór 2018 iets verbergt.

Twee: ons crawl-and-diff-harnas draait tegen de oude site mét de nieuwe redirect map toegepast via een lokale proxy, niet alleen tegen staging. Als een oude URL in de index van Google staat en onder de nieuwe configuratie 404 zou geven, willen we dat weten voordat DNS wijzigt.

Drie: we genereren de Astro _redirects-map (of Nginx-config, afhankelijk van de hosting) rechtstreeks uit één bron van waarheid, inclusief de regels uit de plugin-params. Geen losse redirect-lijstjes meer die ergens in iemands spreadsheet rondzwerven.

Kernpunt

Op elke Joomla-site die ouder is dan vijf jaar geldt: ga ervan uit dat er routing-regels buiten de redirect manager verstopt zitten. Dump het params-veld van elke plugin en grep erdoorheen voordat je het project offreert.

Kort over de Astro-kant

Voor de volledigheid: zo ziet het redirects-bestand eruit dat we na de audit genereren. Astro leest dit bij build-time in en stuurt zowel static-host redirect-headers als een kleine middleware-fallback voor hosts die dat nodig hebben. Het patroon is letterlijk overgenomen uit de Astro routing-docs.

# Auto-generated from joomla-redirect-inventory.json
# Source: legacy K2 menu (412 rules)
# Source: plg_system_legacyseo.canonical_map (247 rules, decoded)
# Source: .htaccess RewriteRule (53 rules)
# Generated: 2026-03-22T08:14:22Z

/pricing-qcp-2016         /pricing                          301
/campaign/saas            /product/main                     301
/old-blog/:slug           /blog/:slug                       301
/legacy/help/:topic       /docs/:topic                      302

Eén bestand. Eén bron van waarheid. Gegenereerd, nooit met de hand bewerkt. De dag dat dit in version control leeft, is de dag waarop een migratie geen reeks kleine verrassingen meer is.

Sta je hier zelf voor

Joomla 3 is klaar. Joomla 4 en 5 zijn prima als je op het platform wilt blijven, maar de meeste marketingsite-teams waar we mee werken willen eruit: statisch of bijna-statisch, headless CMS, snellere builds, minder plugins om in de gaten te houden. De migratie is alleen rechttoe-rechtaan als de oude site dat ook is. De site die je erft, is dat bijna nooit.

Toen we de Strapi- en Astro-migratie voor de Utrechtse klant bouwden, zat de verrassing niet in de nieuwe stack. Hij zat in de negen jaar oude hack die één freelancer in een plugin-params-veld had laten staan. Sindsdien hebben we nog vier legacy-migraties opgeleverd met het auditscript hierboven als allereerste commando. Geen van die vier is over de oorspronkelijke offerte heen gegaan.

Het kleinste wat je vandaag kunt doen: open je Joomla-database en draai SELECT element, LENGTH(params) FROM your_prefix_extensions WHERE type='plugin' ORDER BY LENGTH(params) DESC LIMIT 10. Als iets boven de 5 KB je verrast, decode het. Daar zit je migratierisico.

Kern

Op elke Joomla-site ouder dan vijf jaar: ga ervan uit dat er routing-regels buiten de redirect manager verstopt zitten. Dump elke plugin-params en grep erdoorheen voordat je offreert.

FAQ

Is Joomla 3.10 nog veilig om in productie te draaien?

Joomla 3 bereikte end of life in augustus 2023. Het Joomla-project geeft geen security patches meer uit. Voor elke site op 3.x moet je op korte termijn een migratie of upgrade plannen.

Moet ik migreren naar Joomla 4 of naar een headless stack zoals Strapi en Astro?

Als je team in een vertrouwd adminpaneel wil blijven editten en de site veel content bevat, is Joomla 4 of 5 een prima keuze. Wil je snellere builds en minder plugins om in de gaten te houden, dan past headless meestal beter.

Hoe lang duurt een typische Joomla-marketingsite-migratie?

Voor een site met 400 artikelen zonder verrassingen: twee tot vier weken. De variabele zit in de oude plugins, niet in de nieuwe stack. Audit de plugin-params voordat je offreert.

Wat is K2 en is het nog veilig om te gebruiken?

K2 is een content construction kit voor Joomla, nog steeds onderhouden, maar met een kleine resterende gebruikersgroep. Hij draait op Joomla 4 met de officiële update. Voor nieuwe projecten raden we core articles of een headless CMS aan.

Hoe vind ik redirect-regels die verstopt zitten in een Joomla-plugin?

Dump de params-kolom uit de plugins-tabel, JSON-decode elke rij en loop door elke string-waarde. Signaleer elke waarde die base64 naar geldige JSON decodeert of URL's bevat. Een PHP-script van 30 regels is genoeg.

joomlamigrationlegacy sitesphpmysqlseo

Iets bouwen?

Start een project