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:

  • label and description are language keys, resolved from the template's own language file.
  • com_templates looks for config/config.xml inside the template first and falls back to templateDetails.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:

  1. Repositories registers the source your package comes from. The screen edits the repositories block of the hub's app/composer.json.
  2. Packages installs it. Hubzero\Utility\Composer runs Composer with app/ as its working directory, so the require lands in app/composer.json and the files under app/.
  3. Core Migrations runs the template's up(), which writes the #__extensions and #__template_styles rows. Until this step the template exists on disk and is invisible to the CMS.
  4. 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.