090 —AI
ChatGPT in a 2015 Joomla admin: the smallest safe shape
A client wants an AI writing assistant inside their Joomla 3 admin. Here is the smallest safe shape we have found, and the parts we refuse to ship.
The Loom came in at 23:41 on a Tuesday. A Dutch publisher we work with, about 14 editors, runs a Joomla 3.10 install that was last templated in 2015. Their commercial director had spent the weekend with ChatGPT and now wanted the same box inside the article editor. Specifically: a sidebar that rewrites the lede, suggests an SEO title, and proposes three social pull-quotes. The brief ended with the sentence every freelancer dreads: "It should be quick, right? It is just an API call."
It is not just an API call. But it is also not a rebuild. After doing this on four legacy Joomla sites in the last eight months, there is a shape that keeps working. This post is that shape, written for the developer who has to deliver it next Friday and would like to still own the maintenance contract in 2027.
What the client is actually asking for
Before any code, separate the request into the three things that are always tangled together in the brief.
- An assistant in the admin. Editors want a panel next to the article body. Not a new tab, not a separate tool. The cost of context-switching is what made them ask in the first place.
- Knowledge of their content. They want suggestions that sound like their house style, not generic GPT prose. This is where most cheap implementations fail and get switched off within a week.
- Some form of governance. Usually unspoken. The commercial director will ask about cost caps the day after launch. The DPO will ask about data flow the day after that.
If you only deliver the first one, you ship a toy. The smallest useful shape covers all three, and on a Joomla 3 site it fits in roughly 400 lines of PHP plus a small Vue island. No core hacks, no template surgery, no com_users patches.
The shape: a plugin, an endpoint, and a drawer
The architecture is three pieces, deliberately boring.
- A Joomla
systemplugin that injects a small JS bundle into the admin only, and only oncom_contentedit views. - A single PHP endpoint under
/administrator/index.php?option=com_ajaxthat proxies to the LLM provider. The browser never sees the API key. - A Vue-in-a-script-tag drawer that reads the current article fields from the DOM, posts to the endpoint, and writes suggestions back as pending changes the editor can accept.
The plugin skeleton is the part most teams over-engineer. Here is the load gate, which is the only complicated bit:
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]);
}
That gate matters. Joomla's admin loads a lot of views you do not want to touch (com_installer, com_users, com_config). Scope the injection or you will get a support ticket about the assistant appearing on the global configuration page, which is exactly where you do not want an LLM helpfully rewriting database credentials.
The endpoint, and what it refuses to do
The proxy endpoint is where the security argument is won or lost. Three rules, in order of how loudly the client will push back on them.
One: the LLM key never goes to the browser. The endpoint reads it from a file outside the webroot, or from a Joomla plugin parameter stored in #__extensions. If your client's first instinct is "just hardcode it in the JS so we can test," the answer is no. The key will end up cached by an unrelated CDN within a month. This is not theoretical; see OWASP's notes on sensitive data exposure for the canonical version of why.
Two: the endpoint is allow-listed by intent. One endpoint, a fixed enum of operations: rewrite_lede, suggest_title, pull_quotes. Not a generic "send a prompt" tunnel. The moment you expose freeform prompting from the admin, you have built a way for any user with editor access to spend the company's API budget on whatever they like. Joomla's session token check (JSession::checkToken()) goes on every request.
Three: the endpoint logs the prompt and the response, with the user id, into a small table. Not for surveillance. For the inevitable conversation in week three where someone says "the AI wrote something weird and we published it." Without the log, you are guessing. With it, you have a row.
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);
}
The 8000 character cap is not arbitrary. It is the size at which a single editor can no longer accidentally cost you €4 in tokens by pasting the entire archive. Pick a number, document it, write it on the change ticket.
House style without a vector database
The temptation on every one of these jobs is to reach for embeddings and a vector store. Resist it for the first delivery. A Joomla 3 publisher with 14 editors does not need a RAG pipeline; they need their style guide pasted into the system prompt.
What works: store a single text file at /administrator/components/com_assistant/style.md (or a plugin parameter) containing the publisher's actual style guide. Two pages, plain markdown. The endpoint reads it once per request and prepends it. That is the entire "house style" feature. It will get you 80 percent of the perceived quality of a fine-tune for zero of the infrastructure.
If, six months later, the editor team is still using it daily and asking for sharper recall of their own archive, then you have evidence to justify embeddings. Build for the evidence, not the brochure.
The Joomla 3 elephant in the room
Joomla 3.10 is end of life. The official docs have said so since August 2023. The honest conversation with the client is that you are putting a 2026 feature on a CMS branch that no longer receives security patches, and the LLM endpoint is now part of their attack surface.
Two things make this responsible rather than reckless. First, the endpoint above does not introduce new auth, it inherits Joomla's. If their admin is compromised, the assistant is the least of their problems. Second, the proxy gives you a single chokepoint where you can rate-limit, log, and kill the integration in one config flip. Compare that to letting editors paste content into chat.openai.com on their phones, which is what they are doing today.
What we leave out, on purpose
The shape above deliberately does not include: image generation, auto-publish, multi-step agents, scheduled rewrites, or comment moderation. Every one of those has been requested by a client in the first conversation. Every one of those has been quietly dropped by the same client within two months, because the editorial workflow does not actually want a robot making publish decisions. It wants a faster draft.
Build the faster draft. Ship it. Watch what editors actually use for four weeks. Then decide what comes next from real usage logs, not from the commercial director's weekend with ChatGPT.
The Pier-shaped footnote
The boring part of all this, the part nobody quotes on, is what happens when the assistant writes something into an article and the editor publishes it and two days later somebody wants to know what changed. On a legacy site with no audit trail, you are reading Apache logs and hoping. When we built Pier we ran into this exact thing on a Magento job; the way we ended up handling it was to keep a per-file version history for every write that goes through the editor, including the AI-assisted ones, so the "what did the robot change on Tuesday" question has a one-click answer instead of a forensics session.
If you take one thing from this post into Friday's delivery, make it the proxy endpoint with the three-intent allow-list. That single decision is what separates a clean assistant from a public, authenticated, token-burning prompt tunnel sitting inside a decade-old admin.
— Questions —
Can I do this without writing a Joomla plugin?
You can put the drawer in a template override, but you lose the clean uninstall path and the load gate. A system plugin is about 60 lines and removes itself cleanly.
Should I upgrade to Joomla 4 or 5 first?
If the budget exists, yes. If it does not, ship the assistant with the proxy and logging in place, and use the running cost data to justify the upgrade in the next quarter.
Which model should the proxy call?
Pick whichever your client already has a billing relationship with. The shape of the endpoint does not change. Swap providers behind the AssistantClient class without touching the plugin.
Do editors need training?
About 20 minutes. Show them the three buttons, the accept and reject flow, and the rule that the assistant never publishes. That is the whole curriculum.
What about GDPR?
Document the data flow in your DPA addendum, name the model provider, and confirm the proxy does not send personal data from the article body unless the editor explicitly chooses to. Logging is internal, not shared.