Developers
Packaging
A finished template has two manifests. templateDetails.xml tells the CMS what
the template offers — its parameters, its module positions and its metadata.
composer.json tells Composer how to fetch and place it. Neither of them
installs anything on its own; a migration does the actual
registering.
Why you would package anything
If northgate only ever runs on one hub, you do not need this chapter. Copy
the directory into app/templates, run the migration, done.
Package it when the template has to reach a hub you do not administer, or more
than one hub, or a hub whose deployment is scripted. Packaging is what turns
"copy this directory and remember to run the migration" into something a hub's
own app/composer.json records and can update.
Be clear about what packaging does not get you. There is no XML installer and no upload-a-zip screen. Read Installing before you plan around it.
What the tree should look like
app/templates/northgate/
css/
html/ Output overrides
img/
js/
language/
en-GB/
en-GB.tpl_northgate.ini
en-GB.tpl_northgate.sys.ini
migrations/
Migration…TplNorthgate.php
component.php
error.php
index.php
composer.json
templateDetails.xml
template_thumbnail.png
favicon.ico
Structure says which of these are required and which are
conventions. The .sys.ini is the one file no shipped template has and every
template with its own module positions should — see
Languages.
The thumbnail
template_thumbnail.png is what the administrator's template list shows.
206 pixels wide; kimera's is 206×150 and kameleon's 206×118, so the height
is not fixed. PNG, and it must be that exact filename —
TemplatesHelper::thumb()
looks for nothing else.
Put an optional template_preview.png beside it and the thumbnail becomes a
link that opens the full-size image in a modal.
templateDetails.xml
Contrary to what older documentation said, this file is not deprecated.
com_templates, com_modules and com_installer all read it, and a template
without one is configurable only by editing files.
The smallest one that does anything useful is four lines of metadata and a position list:
<?xml version="1.0" encoding="utf-8"?>
<extension type="template" version="2.5">
<name>northgate</name>
<version>1.0</version>
<description>Northgate University site template</description>
<positions>
<position>user3</position>
<position>left</position>
<position>right</position>
</positions>
</extension>
That is enough for the three positions to appear in the module editor's dropdown. A complete example:
<?xml version="1.0" encoding="utf-8"?>
<extension type="template" version="2.5">
<name>northgate</name>
<creationDate>2026-09-10</creationDate>
<author>Northgate University Web Team</author>
<authorEmail>web@northgate.example</authorEmail>
<authorUrl>northgate.example</authorUrl>
<copyright>Copyright (c) 2026 Northgate University</copyright>
<license>http://opensource.org/licenses/MIT MIT</license>
<version>1.0</version>
<description>Northgate University site template</description>
<files>
<filename>index.php</filename>
<filename>component.php</filename>
<filename>error.php</filename>
<filename>css/index.css</filename>
<filename>js/hub.js</filename>
<filename>template_thumbnail.png</filename>
</files>
<languages>
<language tag="en-GB">en-GB.tpl_northgate.ini</language>
</languages>
<positions>
<position>notices</position>
<position>search</position>
<position>user3</position>
<position>breadcrumbs</position>
<position>left</position>
<position>right</position>
<position>footer</position>
<position>endpage</position>
</positions>
<config>
<fields name="params">
<fieldset name="basic">
<field name="header" type="list" default="light"
label="TPL_NORTHGATE_FIELD_HEADER_LABEL"
description="TPL_NORTHGATE_FIELD_HEADER_DESC">
<option value="light">TPL_NORTHGATE_FIELD_HEADER_LIGHT</option>
<option value="dark">TPL_NORTHGATE_FIELD_HEADER_DARK</option>
</field>
</fieldset>
</fields>
</config>
</extension>
The root element
<extension type="template"> is the current form and what every shipped
template except lucent uses. <install type="template"> is the legacy form
and is still accepted; lucent uses it. <metafile> is for languages only.
Anything else and the parser gives up and the template shows no metadata.
The type attribute must say template. kameleon also carries
client="administrator", but nothing reads that attribute — the client is
whatever addTemplateEntry() wrote into #__extensions.
What each element is for
| Element | Read by | Effect |
|---|---|---|
<name>, <version>, <description>, <author>, <authorEmail>, <authorUrl>, <copyright>, <creationDate> |
com_templates, com_installer |
Shown in the template and extension lists. Missing author and creationDate display as Unknown. |
<positions> |
com_modules |
Fills the position dropdown when someone places a module. |
<config> |
com_templates |
Becomes the style-editing form. |
<languages> |
Nothing | The language file is found by naming convention, not by this list. |
<files> |
Nothing | Left over from the old XML installer. kimera lists 18 files out of the 140 it ships; lucent and welcome list none. Harmless either way. |
<license> |
Nothing | Informational. |
Positions
<positions>
<position>introblock</position>
<position value="user3">Utility bar</position>
</positions>
com_modules reads the element text as the position name. Give a value
attribute and the text becomes the label instead. With neither, the
administrator sees a label looked up as
TPL_{TEMPLATE}_POSITION_{POSITION}, falling back to
COM_MODULES_POSITION_{POSITION} from com_modules' own language file. Define
your key in the template's .sys.ini — the main .ini is not loaded for
this list. See
Languages.
Parameters
The <config> block is an ordinary
Hubzero\Form fieldset definition, and
Parameters covers the field types. Two
things are specific to templates:
labelanddescriptionare language keys, resolved from the template's own language file.com_templateslooks forconfig/config.xmlinside the template first and falls back totemplateDetails.xml. Either works; nothing shipped uses the separate file.
Read the values back in a layout with $this->params->get('header').
composer.json
This is what makes the template installable as a package. kimera's, whole:
{
"name": "hubzero/tpl_kimera",
"description": "Kimera template for the HUBzero CMS",
"type": "hubzero-template",
"keywords": ["hubzero"],
"homepage": "https://hubzero.org",
"license": "MIT",
"authors": [
{
"name": "HUBzero",
"email": "support@hubzero.org"
}
],
"support": {
"email": "support@hubzero.org",
"issues": "https://help.hubzero.org"
},
"require": {
"php" : "^5.4"
},
"extra": {
"install-directory": "/templates/kimera/"
}
}
| Key | Notes |
|---|---|
name |
{vendor}/tpl_{template}. The vendor is yours, not hubzero. |
type |
Must be one of hubzero-component, hubzero-module, hubzero-plugin, hubzero-template. This is what tells the hub's installer where the package belongs. |
license |
An SPDX identifier. The shipped templates say MIT. |
extra.install-directory |
Where the files should land, relative to app/. |
require |
The shipped templates pin "php": "^5.4", which is long obsolete; set something honest for your own. |
Installing
There is no package installer. There is no XML installer, no upload-and-install screen, and no working end-to-end path from a package file to a registered template. Installation is meant to go through Composer, and the Composer screens are unfinished — read this section before you plan a deployment around them.
Installation is meant to go through Composer.
Extensions → Extension Manager in the administrator opens com_installer,
which has these screens:
| Screen | Controller | What it does |
|---|---|---|
| Hubzero Core | manage |
Lists installed extensions; enable, disable, remove |
| Core Migrations | migrations |
Runs pending up() methods |
| Custom Extensions | customexts |
Installs an extension from a Git repository |
| Warnings | warnings |
Environment checks |
| Packages | packages |
Lists what the registered repositories offer, and installs one |
| Repositories | repositories |
Registers a Composer repository |
Installing a packaged template is:
- Repositories registers the source your package comes from. The screen
edits the
repositoriesblock of the hub'sapp/composer.json. - Packages installs it.
Hubzero\Utility\Composerruns Composer withapp/as its working directory, so the require lands inapp/composer.jsonand the files underapp/. - Core Migrations runs the template's
up(), which writes the#__extensionsand#__template_stylesrows. Until this step the template exists on disk and is invisible to the CMS. - Hubzero Core lists the result and lets an administrator enable or disable it.
What to do instead
Until those screens are finished, the reliable route — and the one to script — is three steps with no user interface in them:
# 1. Put the directory where the loader will find it
rsync -a northgate/ /path/to/hub/app/templates/northgate/
# 2. Register it
php core/bin/muse migration -f -e=tpl_northgate
# 3. Clear the cached template list, or wait out `cachetime`
That is also what you will do while developing. Ship the composer.json
anyway: it costs nothing, it records the vendor, licence and install directory,
and it is what a working installer would read. The
deployment chapter covers packaging a
finished extension for other people.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.