Joomla
Joomla K2-migratie: 8.700 affiliaties werden 'unknown'
Twaalf dagen in een Joomla 3.10 naar Astro-migratie stond bij iedere K2-auteur op staging 'unknown'. Een JParameter-blob die de J4-upgrade stilletjes had platgeslagen.

Elf uur 's avonds, een Slack-bericht van de operationeel manager bij een academische uitgever uit Leiden met 25 medewerkers. "De migratie is teruggedraaid. Alweer. Bij elke auteur staat als affiliatie unknown." Hun oude site draaide op Joomla 3.10 met K2 als content engine. 8.700 artikelen, ruwweg 14 jaar peer-reviewed back-catalogus. Het plan was schoon op papier: upgraden naar Joomla 4 op een staging-kloon, exporteren naar JSON, importeren in Payload CMS, renderen met Astro. Dag twaalf van een sprint die we hadden ingeschat op vijf dagen.
Het zichtbare symptoom was klein en pijnlijk. In elk auteursblok op staging stond als instituutsnaam unknown. Bij alle 8.700 items. Hetzelfde veld op productie Joomla 3.10 rendeerde gewoon goed. Iets in de J4-upgrade vrat de data op, stilletjes, zonder fout in de migrator-log en zonder exception in de K2 upgrade-output.
De klant geeft tijdschriften uit voor kunstgeschiedenis en Nederlandse letterkunde. Auteursaffiliaties zijn voor hen geen cosmetisch detail. Affiliatie stuurt de goedkeuringsflow van de redactieraad, de indexering op Google Scholar, de rapportages voor institutionele licenties en de ORCID-naar-auteur koppeling die twee van hun tijdschriften verplicht stellen bij inzending. Verlies van die data was geen optie, en een site live zetten die 8.700 keer "unknown" zegt is geen kleine misser.
Hoe we in deze puinhoop terechtkwamen
We hadden de site overgenomen van de vorige bouwer van de uitgever, een eenmansbureau dat met pensioen was gegaan. De Joomla-installatie draaide op 3.10.11 met K2 v2.10 als content engine en een handvol betaalde extensies voor citatie-export, ORCID en DOI-uitgifte. De hosting was één 4 GB VPS bij een Nederlandse provider die het vorige bureau drie jaar lang niet had aangeraakt. Joomla 3 bereikte end-of-life in augustus 2023, en de externe IT-audit van de uitgever had de site aangemerkt als beveiligingsrisico. Ze hadden een deadline in december om volledig van Joomla 3 af te zijn.
Het schoonste pad was niet Joomla 4. Het was Astro + Payload, om drie redenen. De redacteuren kwamen alleen aan de artikel-editor, nooit aan de rest van Joomla's admin-wirwar. De publieke site was vooral lees-verkeer dat baat had bij static rendering. En de nieuwe ORCID-flow die ze wilden vroeg om een modern fetch- en queue-model dat in Joomla pijnlijk is en in Payload triviaal.
Om de content netjes naar buiten te migreren hadden we onderweg toch Joomla 4 nodig. De data van K2 leeft in K2-tabellen, en de schoonste export-tools gaan allemaal uit van het J4-datamodel. Het plan: upgraden van Joomla 3.10 naar 4.4 op staging, K2's J4-migrator draaien, JSON exporteren, importeren in Payload, Astro erop richten. Daarna de oude VPS archiveren.
Dat was het plan. Dag twaalf, en we hadden nog geen één schoon record geëxporteerd.
Wat K2 in werkelijkheid opsloeg
K2's systeem van "extra fields" is zo'n feature die in 2014 prachtig werkte en daarna stilletjes verouderde tot een risico. Bij deze uitgever stonden auteursaffiliaties niet in K2's reguliere auteursprofiel-velden. Ze waren jaren geleden ingeplakt in #__k2_items.extra_fields als een JSON-blob. De affiliatie-metadata zelf zat genest binnen één veld als een geserialiseerde JParameter-string. INI-vormig, escaped, verpakt in JSON, verpakt in de rij.
Als je ooit een Joomla 1.5- of 2.5-codebase hebt overgenomen, heb je dit formaat gezien. JParameter was Joomla's manier van vóór Registry om key=value config-blobs op te slaan:
institution=Universiteit Leiden
department=Faculty of Humanities
orcid=0000-0002-1825-0097
country=NL
Die string zat in een JSON-array, in de rij, in de tabel. Het werkte omdat elk K2 item-display-template wist dat het JParameter::loadString() moest aanroepen bij het uitlezen. Het vorige bureau had een klein content-plugin toegevoegd dat precies dat deed. De site draaide al tien jaar op die shim.
Niemand had het gedocumenteerd. Geen commentaar in het template. Geen vermelding in de operations-runbook. De enige reden dat we het vonden, was dat een van onze developers in 2010 Joomla 1.5-sites had gebouwd en de vorm vanuit zijn ooghoek herkende.
Waar Joomla 4 zijn mond hield
Joomla 4 heeft JParameter verwijderd. Het was sinds Joomla 1.6 al deprecated ten gunste van Joomla\Registry\Registry, een veel betere klasse, alleen spreekt die een ander dialect. Registry verwacht JSON- of PHP-array vormen, niet de kale INI-strings die JParameter vrolijk slikte. De Joomla 4 migration guide behandelt de overstap op componentniveau, maar helpt je niet als het verouderde formaat in user data zit.
De K2-upgrade die op Joomla 4 draait (K2 v2.11+) doet een eerlijke poging om extra-field-blobs te parsen. Als hij een waarde tegenkomt die hij niet als JSON of Registry kan decoderen, doet hij wat veel goedbedoelende upgraders doen. Hij valt terug op een veilige default en schrijft "unknown" in het veld in plaats van de hele import te laten crashen. Geen exception. Geen waarschuwing. De K2-adminlog noteert de upgrade als geslaagd.
Alleen 8.700 rijen met unknown.
Als een vendor-upgrader "geen dataverlies" claimt maar je vindt geen unit tests voor de parser op verouderde formaten, behandel die belofte dan als marketingtekst. Diff een steekproef van rijen voor en na. Ga van niets uit.
We zagen het op dag twee van de upgrade. De volgende tien dagen gingen op aan uitzoeken waar de echte data nog wel stond.
De rollback die productie vervuilde
Het eerste instinct klopte. Staging-DB terugdraaien, een verse dump uit productie importeren, opnieuw proberen. Maar de uitgever had een probleem dat we pas op dag vier zagen. De "staging"-omgeving van het vorige bureau had een cronjob die elke zes uur content-wijzigingen uit staging terugschoof naar productie, voor een feature die ze twee jaar geleden hadden uitgezet. Niemand had het uitgezet.
Elke upgrade-poging op staging had K2-items opnieuw opgeslagen, waardoor de inmiddels verminkte extra_fields via K2's J3-model terug naar productie werden geserialiseerd. Op dag drie stond ook in productie in zo'n 1.400 rijen unknown.
Zoiets zie je alleen als je een SELECT id, extra_fields FROM jos_k2_items diff tussen snapshots van bekende tijdstippen. Het kostte ons een ochtend om het diff-script te schrijven en nog een ochtend om door te hebben dat de cron de bron was. We stopten de cron, herstelden productie vanuit de meest recente schone nachtelijke dump, en pas toen hadden we een echt plan.
Data terughalen uit de pre-upgrade back-up
De vluchtroute was de nachtelijke InnoDB-dump die de hosting-partij van de uitgever in cold storage had staan. We trokken een snapshot van vóór de eerste upgrade-poging en zetten die terug in een geïsoleerde MySQL 5.7-container op onze eigen infrastructuur. Daarna schreven we een kleine extractor die de JParameter-strings op de oude manier parste, ze mapte naar het Payload-schema en JSON uitspuwde.
<?php
// extract-k2-affiliations.php
// Reads K2 extra_fields, finds the JParameter blob, returns clean rows.
declare(strict_types=1);
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=k2_legacy;charset=utf8mb4',
'root',
getenv('DB_PASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
function parseJParameter(string $blob): array {
$out = [];
foreach (preg_split('/\r?\n/', $blob) as $line) {
if ($line === '' || str_starts_with($line, ';')) continue;
if (!str_contains($line, '=')) continue;
[$k, $v] = explode('=', $line, 2);
$out[trim($k)] = trim($v);
}
return $out;
}
$stmt = $pdo->query('SELECT id, title, extra_fields FROM jos_k2_items');
foreach ($stmt as $row) {
$fields = json_decode($row['extra_fields'], true) ?? [];
foreach ($fields as $field) {
if (($field['id'] ?? null) !== 17) continue; // affiliation field id
$aff = parseJParameter((string) $field['value']);
echo json_encode([
'k2_id' => (int) $row['id'],
'title' => $row['title'],
'institution' => $aff['institution'] ?? null,
'department' => $aff['department'] ?? null,
'orcid' => $aff['orcid'] ?? null,
'country' => $aff['country'] ?? null,
], JSON_UNESCAPED_UNICODE) . "\n";
}
}
Twaalf minuten uitvoertijd. 8.700 rijen, 8.612 met geldige affiliatie-data. 88 rijen met echt lege waardes, wat we steekproefsgewijs verifieerden tegen de productie-frontend. Die artikelen hadden simpelweg geen affiliatie in het systeem. Niet één rij unknown.
We valideerden op drie manieren voor we het vertrouwden. We controleerden 30 willekeurige rijen handmatig tegen de live productie-rendering. We vergeleken de instituut-verdeling met een CSV die de uitgever zes maanden eerder had geëxporteerd voor een interne audit. En we draaiden een fuzzy match tegen het ROR institutional identifier registry om instituutsnamen te markeren die binnen edit distance 2 geen bekende ROR-entry hadden. Er kwamen drie flags terug, allemaal echt obscure Nederlandse theologische seminaries die ROR niet dekt.
Payload-schema, opgezet om nooit meer een shim nodig te hebben
De reden dat het überhaupt zo'n puinhoop werd, was dat affiliaties in een vrij-tekst extra veld waren gepropt. Het Payload-schema behandelt ze als first-class.
// payload/collections/Article.ts
import type { CollectionConfig } from 'payload/types'
export const Article: CollectionConfig = {
slug: 'articles',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'legacyK2Id', type: 'number', unique: true, index: true },
{
name: 'authors',
type: 'array',
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'institution', type: 'text' },
{ name: 'department', type: 'text' },
{ name: 'orcid', type: 'text' },
{ name: 'country', type: 'text' },
],
},
{ name: 'body', type: 'richText', required: true },
{ name: 'publishedAt', type: 'date', index: true },
],
}
We hielden legacyK2Id aan omdat de redacteuren in hun interne Trello-board nog op de oude artikelnummers navigeren, en 14 jaar aan inkomende links vanaf Google Scholar naar de oude URL-slugs wijzen. De /article/[slug]-route in de Astro-frontend valt voor de long tail terug op een /k2/[legacyK2Id]-redirect.
De vijf-minuten audit die je vanavond kunt draaien
Sta je op het punt om een Joomla 3 K2-site naar Joomla 4 te upgraden, doe dit dan voor je iets aanraakt.
- Open phpMyAdmin of je favoriete MySQL-client tegen een kopie, niet tegen productie.
- Draai
SELECT id, extra_fields FROM jos_k2_items WHERE extra_fields LIKE '%=%' LIMIT 50;om kandidaat-JParameter-rijen boven water te krijgen. Het is-gelijk-teken binnen een extra-field-waarde is het signaal. - Als de
extra_fieldsvan een rij INI-achtigekey=value-regels bevatten in plaats van geneste JSON-objecten, heb je verouderde JParameter-data. De J4-upgrader gaat het opvreten. - Maak een
.ibd-back-up metmysqldump --single-transactionvoordat de K2 J4-migrator draait. - Diff dezelfde query na de upgrade. Zeggen de verdachte rijen nu
unknown, herstel dan vanuit de pre-upgrade dump en schrijf een parser.
Dit kost meer tijd om te lezen dan om te draaien. Het scheelt je twaalf dagen.
Toen we deze migratie bouwden voor de uitgever in Leiden, zat de echte winst niet in de parser. Hij zat in het vasthouden aan de pre-upgrade .ibd-back-up als bron van waarheid, in plaats van vertrouwen op de half-geüpgradede staging-DB. Heb je een verouderde Joomla-, Drupal- of Magento-site die richting een end-of-life-deadline kruipt, dan begint ons werk aan een legacy migratie meestal met diezelfde saaie back-up-check, ruim voor er over een nieuwe stack gesproken wordt.
Kern
Als een CMS-upgrader 'slaagt' maar 'unknown' in je velden schrijft, is de data niet weg. Hij staat nog in de pre-upgrade back-up. Herstel daar eerst vanuit.
FAQ
Wat is JParameter en waarom veroorzaakte het dit?
JParameter was Joomla's config-klasse van vóór Registry voor INI-achtige key=value-blobs. Joomla 4 heeft hem verwijderd. Data die nog in dat formaat staat, wordt tijdens de upgrade omgezet of vervangen door een veilige default, vaak zonder enige melding.
Repareert K2 v2.11 verouderde JParameter-data automatisch?
Hij probeert het. Als hij een waarde niet als JSON of Registry kan parsen, schrijft hij een veilige default als 'unknown' in plaats van te crashen. De upgrade-log noteert succes, maar de originele data is verdwenen uit de live database.
Hoe controleer ik of mijn Joomla 3 K2-site hetzelfde risico loopt?
Query jos_k2_items.extra_fields op een back-up. Zoek naar rijen waarvan de waarden INI-achtige key=value-regels bevatten in plaats van JSON-objecten. Iedere match betekent dat de Joomla 4-upgrader die data onderweg platslaat.
Waarom in 2026 nog van Joomla 3 af als hij nog werkt?
Joomla 3 bereikte in augustus 2023 zijn end-of-life. Sindsdien geen security patches meer. Elke site die nog op 3.x draait, sleept ongepatchte kwetsbaarheden mee die elke maand verder oplopen, en de meeste verzekeraars markeren het bij een audit.