090 —AI
ChatGPT in een Joomla-admin uit 2015: de kleinste veilige vorm
Een klant wil een AI-schrijfassistent in zijn Joomla 3-admin. Dit is de kleinste veilige vorm die wij kennen, en de onderdelen die we weigeren te leveren.
De Loom kwam binnen op een dinsdag om 23:41. Een Nederlandse uitgever waar we mee werken, zo'n 14 redacteuren, draait een Joomla 3.10-installatie waarvan het template voor het laatst in 2015 is aangeraakt. Hun commercieel directeur had het weekend met ChatGPT doorgebracht en wilde nu hetzelfde vakje in de artikel-editor. Concreet: een zijbalk die de lede herschrijft, een SEO-titel voorstelt en drie social pull-quotes aandraagt. De briefing eindigde met de zin waar elke freelancer van huivert: "Dat moet snel kunnen, toch? Het is gewoon een API-call."
Het is niet gewoon een API-call. Maar het is ook geen rebuild. Na dit de afgelopen acht maanden op vier verouderde Joomla-sites te hebben gedaan, is er een vorm die blijft werken. Dit stuk is die vorm, geschreven voor de developer die het vrijdag moet opleveren en in 2027 nog steeds het onderhoudscontract wil hebben.
Wat de klant eigenlijk vraagt
Splits de vraag, voordat je code schrijft, in de drie dingen die in elke briefing door elkaar lopen.
- Een assistent in de admin. Redacteuren willen een paneel naast het artikelveld. Geen nieuw tabblad, geen aparte tool. De kosten van context-switchen zijn precies waarom ze het vragen.
- Kennis van hun content. Ze willen suggesties die klinken als hun huisstijl, geen generieke GPT-proza. Dit is waar de meeste goedkope implementaties stranden en binnen een week worden uitgezet.
- Enige vorm van governance. Meestal onuitgesproken. De commercieel directeur vraagt de dag na livegang naar kostenplafonds. De FG vraagt de dag daarna naar de datastromen.
Lever je alleen het eerste, dan ship je speelgoed. De kleinste bruikbare vorm dekt alle drie, en op een Joomla 3-site past die in ruwweg 400 regels PHP plus een klein Vue-eilandje. Geen core hacks, geen template-chirurgie, geen com_users-patches.
De vorm: een plugin, een endpoint en een lade
De architectuur bestaat uit drie stukken, bewust saai.
- Een Joomla
system-plugin die een kleine JS-bundle alleen in de admin injecteert, en alleen opcom_content-bewerkviews. - Eén PHP-endpoint onder
/administrator/index.php?option=com_ajaxdat proxiet naar de LLM-aanbieder. De browser ziet de API-sleutel nooit. - Een Vue-in-een-script-tag lade die de huidige artikelvelden uit de DOM leest, naar het endpoint post, en suggesties terugschrijft als hangende wijzigingen die de redacteur kan accepteren.
Het plugin-skelet is het stuk waar de meeste teams overengineeren. Dit is de load-gate, het enige echt ingewikkelde stukje:
public function onBeforeRender()
{
$app = JFactory::getApplication();
if (!$app->isClient('administrator')) return;
$input = $app->input;
$option = $input->getCmd('option');
$view = $input->getCmd('view');
$layout = $input->getCmd('layout', 'default');
if ($option !== 'com_content' || $view !== 'article' || $layout !== 'edit') {
return;
}
$doc = JFactory::getDocument();
$doc->addScript(JUri::root(true) . '/media/plg_system_assistant/drawer.js', [], ['defer' => true]);
}
Die gate is belangrijk. De Joomla-admin laadt een hoop views waar je niet aan wil komen (com_installer, com_users, com_config). Scope de injectie of je krijgt een supportticket dat de assistent op de globale configuratiepagina verschijnt, wat precies de plek is waar je geen LLM behulpzaam databasecredentials wil laten herschrijven.
Het endpoint, en wat het weigert te doen
In het proxy-endpoint wordt het security-argument gewonnen of verloren. Drie regels, in volgorde van hoe hard de klant zal tegensputteren.
Eén: de LLM-sleutel komt nooit in de browser. Het endpoint leest hem uit een bestand buiten de webroot, of uit een Joomla-pluginparameter in #__extensions. Als het eerste idee van je klant is "hardcode het even in de JS zodat we kunnen testen", is het antwoord nee. Die sleutel staat binnen een maand in de cache van een willekeurige CDN. Dit is niet theoretisch; zie OWASP's notities over sensitive data exposure voor de canonieke versie van waarom.
Twee: het endpoint heeft een allow-list per intentie. Eén endpoint, een vaste enum aan operaties: rewrite_lede, suggest_title, pull_quotes. Geen generieke "stuur een prompt"-tunnel. Op het moment dat je vrije prompting blootlegt vanuit de admin, heb je een manier gebouwd waarop elke gebruiker met redacteurrechten het API-budget van het bedrijf kan verbranden aan wat hij maar wil. De Joomla-sessiecheck (JSession::checkToken()) staat op elke request.
Drie: het endpoint logt de prompt en het antwoord, met user-id, in een kleine tabel. Niet voor surveillance. Voor het onvermijdelijke gesprek in week drie waarin iemand zegt "de AI heeft iets raars geschreven en we hebben het gepubliceerd." Zonder de log gok je. Mét de log heb je een rij.
public function onAjaxAssistant()
{
JSession::checkToken('get') or jexit('Invalid token');
$user = JFactory::getUser();
if ($user->guest || !$user->authorise('core.edit', 'com_content')) {
throw new RuntimeException('Forbidden', 403);
}
$input = JFactory::getApplication()->input;
$intent = $input->getCmd('intent');
$body = $input->get('body', '', 'RAW');
$allowed = ['rewrite_lede', 'suggest_title', 'pull_quotes'];
if (!in_array($intent, $allowed, true)) {
throw new RuntimeException('Unknown intent', 400);
}
// hard cap on input size, in characters not bytes
if (mb_strlen($body) > 8000) {
throw new RuntimeException('Body too large', 413);
}
return AssistantClient::call($intent, $body, $user->id);
}
De grens van 8000 tekens is niet willekeurig. Het is de grootte waarbij één redacteur je niet langer per ongeluk €4 aan tokens kost door het hele archief te plakken. Kies een getal, documenteer het, schrijf het op het wijzigingsticket.
Huisstijl zonder vector-database
De verleiding is bij elk van deze klussen om naar embeddings en een vector store te grijpen. Doe het niet bij de eerste oplevering. Een Joomla 3-uitgever met 14 redacteuren heeft geen RAG-pipeline nodig; ze hebben hun stijlgids in de system prompt nodig.
Wat werkt: zet één tekstbestand op /administrator/components/com_assistant/style.md (of een pluginparameter) met de daadwerkelijke stijlgids van de uitgever. Twee A4'tjes, platte markdown. Het endpoint leest hem één keer per request en plakt hem voorop. Dat is de hele "huisstijl"-feature. Het levert 80 procent van de gevoelde kwaliteit van een fine-tune op, voor nul procent van de infrastructuur.
Als de redactie het zes maanden later nog dagelijks gebruikt en scherpere recall uit het eigen archief vraagt, dan heb je bewijs om embeddings te rechtvaardigen. Bouw op het bewijs, niet op de brochure.
De Joomla 3-olifant in de kamer
Joomla 3.10 is end of life. De officiële docs zeggen dat sinds augustus 2023. Het eerlijke gesprek met de klant is dat je een 2026-feature op een CMS-tak zet die geen security patches meer krijgt, en dat het LLM-endpoint nu deel uitmaakt van hun aanvalsoppervlak.
Twee dingen maken dit verantwoord in plaats van roekeloos. Eén: het endpoint hierboven introduceert geen nieuwe auth, het erft die van Joomla. Als hun admin gecompromitteerd is, is de assistent het kleinste van hun problemen. Twee: de proxy geeft je één chokepoint waar je kan rate-limiten, loggen en de integratie met één config-flip kan stilleggen. Vergelijk dat met redacteuren die op hun telefoon content in chat.openai.com plakken, wat ze vandaag doen.
Wat we bewust weglaten
De vorm hierboven omvat bewust niet: beeldgeneratie, auto-publish, multi-step agents, ingeplande herschrijvingen of reactiemoderatie. Elk daarvan is in het eerste gesprek door een klant gevraagd. Elk daarvan is binnen twee maanden door diezelfde klant stilletjes afgeschoten, omdat de redactionele workflow eigenlijk geen robot publicatiebeslissingen wil laten nemen. Hij wil een snellere kladversie.
Bouw de snellere kladversie. Lever hem op. Kijk vier weken naar wat redacteuren feitelijk gebruiken. Beslis daarna wat volgt op basis van echte gebruikslogs, niet op basis van het ChatGPT-weekend van de commercieel directeur.
De Pier-vormige voetnoot
Het saaie deel van dit alles, het deel waar niemand voor betaalt, is wat er gebeurt als de assistent iets in een artikel zet, de redacteur publiceert het, en twee dagen later wil iemand weten wat er is veranderd. Op een legacy site zonder audit trail zit je Apache-logs te lezen en te hopen. Toen we Pier bouwden liepen we hier precies tegenaan bij een Magento-klus; de manier waarop we het uiteindelijk hebben opgelost was om per bestand een version history bij te houden voor elke schrijfactie via de editor, inclusief de AI-ondersteunde, zodat de vraag "wat heeft die robot dinsdag aangepast" een antwoord van één klik is in plaats van een forensisch onderzoek.
Als je één ding uit dit stuk meeneemt naar de oplevering van vrijdag, maak het dan het proxy-endpoint met de allow-list van drie intenties. Die ene beslissing scheidt een nette assistent van een publieke, geauthenticeerde, token-verbrandende prompt-tunnel die in een tien jaar oude admin zit.
— Vragen —
Kan dit ook zonder een Joomla-plugin te schrijven?
Je kunt de lade in een template-override stoppen, maar dan verlies je het schone uninstall-pad en de load-gate. Een system-plugin is zo'n 60 regels en ruimt zichzelf netjes op.
Moet ik eerst upgraden naar Joomla 4 of 5?
Als het budget er is, ja. Zo niet, lever dan de assistent op met de proxy en logging op orde, en gebruik de werkelijke kostendata om de upgrade in het volgende kwartaal te onderbouwen.
Welk model moet de proxy aanroepen?
Pak degene waar je klant al een factureerrelatie mee heeft. De vorm van het endpoint verandert niet. Wissel aanbieders achter de AssistantClient-klasse zonder de plugin aan te raken.