Development
Package a Joomla Extension So It Installs and Updates Cleanly
An extension that installs is easy. An extension that installs, upgrades over its previous version without losing settings, and tells users when a new release exists takes a few more things in the manifest — and they are the things most…
The manifest is the contract
Everything the installer does comes from the XML manifest. A minimal plugin manifest:
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>PLG_SYSTEM_EXAMPLE</name>
<version>1.0.0</version>
<creationDate>2026-08</creationDate>
<author>Your name</author>
<description>PLG_SYSTEM_EXAMPLE_XML_DESCRIPTION</description>
<namespace path="src">Vendor\Plugin\System\Example</namespace>
<files>
<folder plugin="example">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_example.ini</language>
<language tag="en-GB">language/en-GB/plg_system_example.sys.ini</language>
</languages>
<updateservers>
<server type="extension" name="Example Updates">https://example.com/updates.xml</server>
</updateservers>
</extension>
Three attributes carry more weight than they look.
method="upgrade" is the one people forget. Without it, installing over an existing copy is treated as a fresh install, and users get an error telling them the extension already exists — or worse, a partial state.
<namespace path="src"> registers your PSR-4 root with Joomla's autoloader. Miss it and your classes are simply not found, with an error that points nowhere useful.
The plugin="example" attribute on the services folder tells the installer which file provides the plugin's service provider.
Name things predictably
Joomla derives paths and language keys from the element name, so consistency is not cosmetic: plg_system_example as the element, PLG_SYSTEM_EXAMPLE as the language key prefix, plg_system_example.ini as the file. Deviate and strings silently render as their own keys.
Note the two language files. The .sys.ini is what the installer and the extension manager read — without it, your extension appears in lists as a raw constant.
Database changes belong in schema files
If your extension has tables, ship an install script and versioned update SQL:
<install>
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
</sql>
</install>
<update>
<schemas>
<schemapath type="mysql">sql/updates/mysql</schemapath>
</schemas>
</update>
Each file in the updates folder is named for the version it upgrades to — 1.1.0.sql, 1.2.0.sql — and Joomla runs whichever ones the installed site has not seen. Make every statement idempotent (CREATE TABLE IF NOT EXISTS, guarded ALTER), because a partially applied update is the state you will actually be debugging.
The update server
The XML your manifest points at lists available releases:
<updates>
<update>
<name>Example</name>
<element>example</element>
<type>plugin</type>
<folder>system</folder>
<version>1.1.0</version>
<downloads>
<downloadurl type="full" format="zip">https://example.com/dl/example-1.1.0.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="6\.[01]"/>
</update>
</updates>
targetplatform takes a regular expression, and it is what stops a release being offered to sites it does not support. Getting it wrong in the permissive direction means users install a version that breaks their site; too restrictive and nobody is offered the update at all.
Build the package right
Zip the contents, not the folder:
cd path/to/extension
zip -r ../plg_system_example-1.0.0.zip . -x '*.git*' '*.DS_Store'
The manifest must sit at the archive root. A zip containing one folder that contains the manifest is the single most common reason an install fails with "XML file not found".
Test the upgrade, not just the install
Install the previous version, configure it, then install the new one over the top. Check that settings survived, that the schema updated, and that removed files are actually gone — use <update> instructions for files you drop between versions, because the installer will not remove them for you.
An extension that installs cleanly on an empty site and corrupts an existing one is worse than no release at all.