Building a Joomla Template from Scratch
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
A Joomla site template is a folder of PHP, XML, CSS and language files that turns the output of components and modules
into a complete HTML page. Most sites never need to write one from nothing: a
child template of Cassiopeia changes colours, fonts and layouts with a handful of
files. Building a template from scratch is still worth doing once, because it shows exactly which files Joomla reads, which
statements it replaces and which conventions the Web Asset Manager expects, and because some projects (a design system
delivered by an agency, an intranet with its own markup) really do need a template that inherits from nothing. This page
builds the smallest useful standalone template, tallpine_minimal, for the Tallpine Hiking Club: a manifest, a main
layout with jdoc statements, assets registered through joomla.asset.json, module positions, an error page, an
offline page, a component-only layout and language files. It then covers three ways to build SCSS, an accessibility
checklist, and how to package and install the template. It was checked against the Developer Manual, the Joomla User
Guide and the 6.2 source tree on 2026-10-30.
The behaviour described is that of Joomla 6.2 (current), which requires PHP 8.3 or newer. Template manifests, jdoc
statements and the Web Asset Manager work the same way in Joomla 4.x and 5.x, so most of this page also applies to
Joomla 5.4 (security fixes only); where 6.x differs, the text says so. The audience is the themer and the developer.
Template styles, the Templates manager and the choice between template and child template are explained in
Templates and template styles; the shipped template that this page
uses as its model is described in
Cassiopeia and Cassiopeia Extended.
What a template is made of
A site template is installed in two places. The PHP and XML files go to templates/<name>/; static files (CSS,
JavaScript, images) go to media/templates/site/<name>/, the folder the web server serves directly. Joomla treats the
template name as the folder name, so tallpine_minimal must match the folder in templates/ and the destination of the
media element in the manifest.
| File | Purpose | Required |
|---|---|---|
|
Manifest: metadata, file lists, media folders, positions, language files and style parameters |
yes |
|
Main page layout for the front end; contains the |
yes |
|
Registers the template’s stylesheets and scripts in the Web Asset Manager |
recommended |
|
Bare layout used when the request asks for |
optional |
|
Layout used when an exception such as 404 or 403 is displayed |
optional |
|
Layout used when Global Configuration sets the site offline |
optional |
|
Translatable strings for the front end and for the installer and administrator lists |
recommended |
|
Template overrides |
optional |
If error.php, offline.php or component.php are missing, Joomla falls back to the versions in templates/system,
which are deliberately plain. A minimal template can therefore ship only templateDetails.xml and index.php; the
sections below add the rest because a real site notices their absence.
Joomla resolves the active template for a request from the template style assigned to the menu item (or the default style). Template styles, and the way the same template can have several styles with different parameters, are in Templates and template styles. The flow from request to page is:
The standalone template has this file layout (the media folder is moved to media/templates/site/tallpine_minimal
by the installer):
tpl_tallpine_minimal/
templateDetails.xml
index.php
error.php
offline.php
component.php
joomla.asset.json
language/
en-GB/
tpl_tallpine_minimal.ini
tpl_tallpine_minimal.sys.ini
de-DE/
tpl_tallpine_minimal.ini
tpl_tallpine_minimal.sys.ini
media/
css/
template.css
js/
template.js
images/
logo.svg
The manifest: templateDetails.xml
The manifest is an <extension type="template" client="site"> document. It names the template, lists every file that
the installer must copy, declares module positions and language files, and describes the parameters of the template style
form. The Developer Manual page on templateDetails.xml documents the elements and is, together with the Cassiopeia
manifest in the source tree, the reference for this section.
<?xml version="1.0" encoding="UTF-8"?>
<extension type="template" client="site">
<name>tallpine_minimal</name>
<version>1.0.0</version>
<creationDate>2026-10</creationDate>
<author>Tallpine Hiking Club</author>
<authorEmail>web@tallpine.example</authorEmail>
<copyright>(C) 2026 Tallpine Hiking Club</copyright>
<license>GNU General Public License version 2 or later</license>
<description>TPL_TALLPINE_MINIMAL_XML_DESCRIPTION</description>
<inheritable>1</inheritable>
<files>
<filename>component.php</filename>
<filename>error.php</filename>
<filename>index.php</filename>
<filename>joomla.asset.json</filename>
<filename>offline.php</filename>
<filename>templateDetails.xml</filename>
</files>
<media destination="templates/site/tallpine_minimal" folder="media">
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
</media>
<positions>
<position>header</position>
<position>menu</position>
<position>breadcrumbs</position>
<position>sidebar</position>
<position>main-top</position>
<position>main-bottom</position>
<position>footer</position>
<position>debug</position>
<position>error-403</position>
<position>error-404</position>
</positions>
<languages folder="language">
<language tag="en-GB">en-GB/tpl_tallpine_minimal.ini</language>
<language tag="en-GB">en-GB/tpl_tallpine_minimal.sys.ini</language>
<language tag="de-DE">de-DE/tpl_tallpine_minimal.ini</language>
<language tag="de-DE">de-DE/tpl_tallpine_minimal.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="advanced">
<field
name="fluidContainer"
type="radio"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
label="TPL_TALLPINE_MINIMAL_FLUID_LABEL"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="siteTitle"
type="text"
default=""
filter="string"
label="TPL_TALLPINE_MINIMAL_TITLE_LABEL"
/>
</fieldset>
</fields>
</config>
</extension>
Source: Cassiopeia templateDetails.xml in the joomla-cms source tree, Joomla Manual: templateDetails.xml
Points that matter in practice:
-
nameis the identifier. It must equal the template folder name, use lowercase letters, digits and underscores, and never change between versions, because template styles, menu assignments and overrides refer to it. -
descriptionand<position>labels are language keys. The installer and the Templates manager look the keys up in the.sys.inifile, so they show real text rather thanTPL_TALLPINE_MINIMAL_XML_DESCRIPTION. -
inheritableset to1allows another template to name this one asparent. Cassiopeia sets it. Leave it out if you do not want children; the details are in Joomla child templates. -
filesandmediaare exhaustive. A file that is not listed is not copied. The installer lists folders such ashtmlwith a<folder>element. Forgetting a file is the most common cause of a template that works from the working tree and breaks after installation. -
media destinationplaces the folder undermedia/templates/site/<name>. Theurivalues injoomla.asset.jsonare resolved relative to that folder’scssandjssubfolders. -
Parameters declared under
<config>appear on the Advanced tab of the template style and are read in PHP with$this->params->get(). Give every field afilterso that stored values are cleaned;integerfor switches,stringfor plain text. Treat each value as untrusted when you echo it. -
Since Joomla 5.1 the Menu Assignment tab of a template style can be overridden in the
configsection with a custom form field, according to the Manual page; the minimal template does not need it. -
Joomla stores the list of positions in the manifest only to offer names in the module editor. A module can still be assigned to a position the manifest does not list, by typing the name; the template must then print that position.
The main layout: index.php
index.php runs in the context of the Joomla\CMS\Document\HtmlDocument object, so $this gives access to the language
($this->language), the text direction ($this->direction), the base URL ($this->baseurl), the style parameters
($this->params), the module counter ($this->countModules()) and the Web Asset Manager
($this->getWebAssetManager()). The file is plain PHP mixed with HTML. Joomla replaces the <jdoc:include> placeholders
after the template has run, so they are not PHP and are never executed.
<?php
/**
* @package Tallpine.Site
* @subpackage Templates.tallpine_minimal
*
* @copyright (C) 2026 Tallpine Hiking Club
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
/** @var Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
$wa = $this->getWebAssetManager();
// Enable the assets registered in joomla.asset.json
$wa->usePreset('template.tallpine_minimal.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
->useStyle('template.active.language')
->useStyle('template.user')
->useScript('template.user');
$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
$wrapper = $this->params->get('fluidContainer') ? 'wrapper-fluid' : 'wrapper-static';
$title = $this->params->get('siteTitle') ?: $sitename;
$hasAside = $this->countModules('sidebar', true);
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="site <?php echo $wrapper . ($hasAside ? ' has-sidebar' : ''); ?>">
<a class="skip-link" href="#content"><?php echo Text::_('TPL_TALLPINE_MINIMAL_SKIP_TO_CONTENT'); ?></a>
<header class="site-header">
<a class="brand" href="<?php echo $this->baseurl; ?>/"><?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></a>
<?php if ($this->countModules('header', true)) : ?>
<jdoc:include type="modules" name="header" style="none" />
<?php endif; ?>
<?php if ($this->countModules('menu', true)) : ?>
<nav aria-label="<?php echo Text::_('TPL_TALLPINE_MINIMAL_MAIN_NAVIGATION'); ?>">
<jdoc:include type="modules" name="menu" style="none" />
</nav>
<?php endif; ?>
</header>
<div class="site-grid">
<div class="content-area">
<jdoc:include type="modules" name="breadcrumbs" style="none" />
<jdoc:include type="modules" name="main-top" style="none" />
<jdoc:include type="message" />
<main id="content" tabindex="-1">
<jdoc:include type="component" />
</main>
<jdoc:include type="modules" name="main-bottom" style="none" />
</div>
<?php if ($hasAside) : ?>
<aside class="sidebar" aria-label="<?php echo Text::_('TPL_TALLPINE_MINIMAL_SIDEBAR'); ?>">
<jdoc:include type="modules" name="sidebar" style="none" />
</aside>
<?php endif; ?>
</div>
<?php if ($this->countModules('footer', true)) : ?>
<footer class="site-footer">
<jdoc:include type="modules" name="footer" style="none" />
</footer>
<?php endif; ?>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>
The jdoc statements
| Statement | What Joomla puts there |
|---|---|
|
Character set, viewport and description meta tags, robots, generator, canonical and alternate links |
|
The |
|
The |
|
All three of the above at once; still valid and used by |
|
System messages (errors, success notices) rendered by the message layout |
|
The output of the component selected by the menu item or the URL |
|
Every published module assigned to position |
|
One named module (the |
Splitting the head into metas, styles and scripts, as Cassiopeia does, lets the template place scripts differently
from styles and is the current practice. Older tutorials, and the books Developing Extensions for Joomla! 5 (2023) and
Joomla! 4 Masterclass (2023) in parts, still show a single type="head"; it works, but a new template gains nothing
from it.
The style attribute selects a module chrome, the wrapper around each module: none outputs the bare module,
html5 wraps it in a <div> with the title in a heading, and Cassiopeia’s card produces Bootstrap card markup. The
chromes, and how to write your own, are covered in Layouts and module
chromes. Positions, module assignment and the {loadposition} content tag are in
Modules and positions.
Three details prevent frequent mistakes:
-
Wrap optional positions in
countModules().$this->countModules('sidebar', true)returns the number of published modules assigned to the position, so the markup around an empty position is not printed. Cassiopeia passestrueas a second argument for the positions that wrap markup, and the same call shape is used here. Without the guard the page contains empty landmarks that screen readers announce. -
Always include
messageandcomponent. A template that omitsmessageswallows every error the user should see, including failed logins and validation errors; one that omitscomponentshows nothing from the site. -
Escape what is not trusted. Values from
$this->paramsand$app->get()are configuration, but they come from forms that administrators edit, so print them withhtmlspecialchars(). Never print request input without escaping it.
To see the names of positions on a live page, enable Preview Module Positions in the Templates options and append
?tp=1 to a front-end URL; every position shows as an outlined box with its name, which is the quickest way to check
that the manifest and index.php agree.
The skip link and landmarks
The layout above already contains the structural pieces that accessibility audits ask for first: a skip link before the
header, a single <main> that is the target of the skip link, <nav> and <aside> landmarks with accessible names,
and lang and dir attributes on <html>. The full checklist is further down.
Assets through the Web Asset Manager
Joomla 4 and later load stylesheets and scripts through the Web Asset Manager (WAM). A template does not print
<link> or <script> tags; it registers assets in a JSON file and enables them in PHP, and WAM resolves dependencies,
adds cache-busting versions and prints the tags at the styles and scripts statements. The WAM page of this
sub-section is Web Asset Manager; this section shows what a template needs.
{
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
"name": "tallpine_minimal",
"version": "1.0.0",
"description": "Assets of the Tallpine Minimal site template.",
"license": "GPL-2.0-or-later",
"assets": [
{
"name": "template.tallpine_minimal.ltr",
"description": "Stylesheet for left-to-right languages.",
"type": "style",
"uri": "template.css"
},
{
"name": "template.tallpine_minimal.rtl",
"description": "Stylesheet for right-to-left languages.",
"type": "style",
"uri": "template-rtl.css"
},
{
"name": "template.tallpine_minimal",
"description": "JavaScript of the template.",
"type": "script",
"uri": "template.js",
"attributes": { "type": "module" },
"dependencies": ["core"]
},
{
"name": "template.tallpine_minimal.ltr",
"type": "preset",
"dependencies": [
"template.tallpine_minimal.ltr#style",
"template.tallpine_minimal#script"
]
},
{
"name": "template.tallpine_minimal.rtl",
"type": "preset",
"dependencies": [
"template.tallpine_minimal.rtl#style",
"template.tallpine_minimal#script"
]
},
{
"name": "template.user",
"description": "Optional site-specific CSS written by the administrator.",
"type": "style",
"uri": "user.css",
"weight": 500
},
{
"name": "template.user",
"description": "Optional site-specific JavaScript.",
"type": "script",
"uri": "user.js",
"weight": 500
}
]
}
Source: Joomla Manual: Web Asset Manager
The keys used:
-
nameis the registry key; the same name can be used once per type, so a style and a script may share a name, astemplate.userdoes here. Presets and styles with the same name are told apart by the#styleand#scriptsuffix in dependencies. -
typeisstyle,scriptorpreset. A preset enables a group of assets in oneusePreset()call, which Cassiopeia uses to switch between its LTR and RTL stylesheets. -
uriis relative to the template’s media folder and omits thecssorjssubfolder:template.cssismedia/templates/site/tallpine_minimal/css/template.css. A full URL is also accepted for external files, and a missing file referenced by a relativeuriis silently skipped, so check the browser’s network panel after the first load. -
dependencieslists assets that must load first.coreis Joomla’s JavaScript core, needed by any script that usesJoomla.*. -
attributesare copied onto the tag;{"type": "module"}loads the file as an ES module, anddeferorasyncare also possible. ES5 builds were dropped in Joomla 5.0, so write modern JavaScript. -
weightorders assets that are otherwise equal. A larger weight loads later, which is why a site’s ownuser.csscan override the template. -
The file is read last among the registries:
media/vendor,media/system,media/legacy, the component’s registry and then the active template’s, so a template can override an asset registered earlier by registering one of the same name. Modules and plugins must register their own registry file withaddRegistryFile(); they are not found automatically. -
Joomla 6.1 added
"version": "nocache", which makes Joomla append no version query to an asset URL, useful while developing. Without it, theversionof the registry file or the file’s hash is used for cache busting; see Caching and performance.
In index.php the template then writes usePreset(), useStyle() and useScript() as shown above. The two assets
template.active.language (language-specific CSS such as language/de-DE/de-DE.css) and template.user (the
administrator’s user.css and user.js) are the same hooks Cassiopeia offers, and they are what lets a
child template add CSS without touching its parent.
Older code, and both books, call $doc->addStyleSheet(), HTMLHelper::_('stylesheet', ...) or print <script> tags in
the template, and load jQuery through HTMLHelper::_('jquery.framework'). Those calls are legacy; since Joomla 4 core
does not depend on jQuery, and a template that bypasses the WAM loses dependency ordering, cache busting and the ability
for child templates and administrators to replace assets. Joomla 6.0 also moved the J* aliases such as JFactory and
JHtml into the Backward Compatibility 6 plugin, off by default on new installs, so import the namespaced classes with
use as the examples here do.
A minimal stylesheet to go with the manifest:
:root {
--tp-green: #1f5e3b;
--tp-ink: #1d2b22;
--tp-bg: #f7f9f5;
}
body {
margin: 0;
color: var(--tp-ink);
background: var(--tp-bg);
font: 1rem/1.6 system-ui, sans-serif;
}
.skip-link {
position: absolute;
inset-inline-start: 0.5rem;
inset-block-start: -4rem;
padding: 0.5rem 1rem;
background: #fff;
}
.skip-link:focus {
inset-block-start: 0.5rem;
}
.wrapper-static .site-header,
.wrapper-static .site-grid,
.wrapper-static .site-footer {
max-width: 72rem;
margin-inline: auto;
}
.site-grid {
display: grid;
gap: 2rem;
padding: 1rem;
}
.has-sidebar .site-grid {
grid-template-columns: minmax(0, 1fr) 18rem;
}
@media (max-width: 48rem) {
.has-sidebar .site-grid {
grid-template-columns: 1fr;
}
}
a:focus-visible {
outline: 3px solid currentcolor;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
Logical properties such as inset-inline-start and margin-inline make one stylesheet work for both text directions;
the RTL file can then be nearly empty, and the template’s right-to-left support is covered in the Manual page
"Supporting Right to Left Languages" and in
Colour modes, RTL and accessibility.
The other layouts
component.php
When a URL carries tmpl=component, Joomla uses component.php instead of index.php. Print views, the Media Manager
dialogs and some modal forms rely on it. The templates/system/component.php file is the model: only the head, the
messages and the component.
<?php
defined('_JEXEC') or die;
/** @var Joomla\CMS\Document\HtmlDocument $this */
$wa = $this->getWebAssetManager();
$wa->useStyle('template.tallpine_minimal.ltr');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="contentpane component">
<jdoc:include type="message" />
<main>
<jdoc:include type="component" />
</main>
</body>
</html>
error.php
When an uncaught exception reaches the site, Joomla renders error.php. The $this object is an
ErrorDocument whose error property holds the exception. Since Joomla 5.0, Cassiopeia lets editors fill the
error-403 and error-404 positions with modules and prints them instead of the default message; your own template can
do the same, as the Manual page "Custom Error Pages" shows.
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
/** @var Joomla\CMS\Document\ErrorDocument $this */
$app = Factory::getApplication();
$errorCode = $this->error->getCode();
// The module renderer does not work when the application failed to initialise
$renderModules = $app->getIdentity() && $app->getLanguage();
$this->getWebAssetManager()->useStyle('template.tallpine_minimal.ltr');
$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
</head>
<body class="site error-page">
<main class="container">
<?php if ($renderModules && $this->countModules('error-' . $errorCode)) : ?>
<jdoc:include type="modules" name="error-<?php echo $errorCode; ?>" style="none" />
<?php else : ?>
<h1><?php echo Text::_('TPL_TALLPINE_MINIMAL_ERROR_HEADING'); ?></h1>
<p><?php echo htmlspecialchars($this->error->getMessage(), ENT_QUOTES, 'UTF-8'); ?></p>
<p><a href="<?php echo $this->baseurl; ?>/"><?php echo Text::_('TPL_TALLPINE_MINIMAL_BACK_HOME'); ?></a></p>
<?php endif; ?>
</main>
</body>
</html>
Pitfalls specific to error pages:
-
The exception can be thrown before the application, the database or the modules are available, for example when the database is down. A module position then cannot be rendered, and Cassiopeia guards its module output so that an initialisation failure does not turn into a second error. Keep the default branch free of module and database dependencies and do not call a position unconditionally.
-
$this->error->getMessage()can contain file paths or SQL when debug is on. Print it escaped, and show a backtrace only when the site is in debug mode, as Cassiopeia does. Never show raw messages to visitors on a production site. -
The error page is served with the error’s HTTP status; keep it that way. A custom 404 page that returns 200 is a "soft 404" for search engines.
-
Error pages share responsibilities with the offline and component pages; a fuller treatment is in Error, offline and component pages.
offline.php
With Site Offline set to Yes in Global Configuration, Joomla renders offline.php to visitors who are not logged in to
the administrator. The system template’s version prints the site name, an optional image and a message chosen by the
Display Offline Message setting, then a login form. A minimal version only needs the message:
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
/** @var Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$this->getWebAssetManager()->useStyle('template.tallpine_minimal.ltr');
$message = $app->get('display_offline_message', 1) == 1 && $app->get('offline_message')
? $app->get('offline_message')
: Text::_('JOFFLINE_MESSAGE');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
</head>
<body class="site offline">
<main class="container">
<h1><?php echo htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8'); ?></h1>
<jdoc:include type="message" />
<p><?php echo $message; ?></p>
</main>
</body>
</html>
The administrator message is rich text set by a trusted user, so it is printed as is, as the system template does. If
members must log in while the site is offline, copy the login form of templates/system/offline.php; it posts to
index.php with the user.login task, a return value and a form token created by HTMLHelper::_('form.token').
Remember that the offline page is served with status 503 so crawlers retry later.
Language files
Joomla language files are INI-style files named after the template with a tpl_ prefix. Two files per language are
conventional:
-
tpl_tallpine_minimal.iniholds the strings used on the front end and on the template style edit form. -
tpl_tallpine_minimal.sys.iniholds the strings the installer and the administrator lists need: the description and the position names.
; en-GB/tpl_tallpine_minimal.sys.ini
TPL_TALLPINE_MINIMAL="Tallpine Minimal"
TPL_TALLPINE_MINIMAL_XML_DESCRIPTION="A small, accessible standalone template for the Tallpine Hiking Club."
TPL_TALLPINE_MINIMAL_POSITION_HEADER="Header"
TPL_TALLPINE_MINIMAL_POSITION_MENU="Main menu"
TPL_TALLPINE_MINIMAL_POSITION_SIDEBAR="Sidebar"
TPL_TALLPINE_MINIMAL_POSITION_FOOTER="Footer"
; en-GB/tpl_tallpine_minimal.ini
TPL_TALLPINE_MINIMAL_FLUID_LABEL="Fluid container"
TPL_TALLPINE_MINIMAL_TITLE_LABEL="Site title"
TPL_TALLPINE_MINIMAL_SKIP_TO_CONTENT="Skip to main content"
TPL_TALLPINE_MINIMAL_MAIN_NAVIGATION="Main navigation"
TPL_TALLPINE_MINIMAL_SIDEBAR="Sidebar"
TPL_TALLPINE_MINIMAL_ERROR_HEADING="This trail does not exist"
TPL_TALLPINE_MINIMAL_BACK_HOME="Back to the home page"
Source: the Cassiopeia language files in the joomla-cms source tree
(The two files are shown in one listing; they are separate files on disk.) Rules that save debugging time:
-
Keys are uppercase, use underscores and must start with the template’s prefix so they cannot clash with other extensions. Values are in double quotes; a double quote inside a value is written
"_QQ_". -
The
en-GBfiles must exist even if the site is German. They are the fallback for any language without a translation, and a missing key shows as the raw key. -
Add
de-DEfiles for the second language of the Tallpine site, and list them in the manifest. A template does not need to be translated to be installed, but untranslated keys appear as shouting capitals. -
The same files may be loaded from
language/of the Joomla root (where language packs install) or from the template’s own folder. A site-wide override in System > Language Overrides wins over both; see Language files and internationalisation. -
Use
Text::_()for plain strings andText::sprintf()for strings with placeholders. Do not concatenate sentences: word order differs between languages.
SCSS build options
The stylesheet above is hand-written CSS, which is enough for a small template. Larger templates usually write SCSS and
compile it to CSS before packaging. Joomla itself compiles Cassiopeia from SCSS sources with a Node.js build in the
source repository, and the template manifest lists a scss media folder so the sources can ship with the template.
The decision is about the developer’s toolchain, not about Joomla: the web server serves only the compiled CSS in the
css folder. Three practical options:
| Option | How | When to choose it |
|---|---|---|
Dart Sass command line |
|
A few files, no JavaScript bundling, the simplest setup |
Vite (or another bundler) |
A |
SCSS and JavaScript in one pipeline, hashed names, watch mode with fast rebuilds |
No build step |
Hand-written CSS with custom properties and logical properties |
Small templates, and teams without Node.js |
{
"name": "tpl-tallpine-minimal",
"version": "1.0.0",
"private": true,
"scripts": {
"build:css": "sass --no-source-map --style=compressed media/scss:media/css",
"watch:css": "sass --watch --style=expanded media/scss:media/css"
},
"devDependencies": {
"sass": "1.89.2"
}
}
Source: Sass: Dart Sass command line
Advice for all three options:
-
Use the
sassnpm package (Dart Sass). LibSass and Node Sass are deprecated and no longer receive features; Dart Sass is the only maintained implementation. Language details are in Sass. -
Compile in CI and commit or package the compiled files. The Joomla site must never need Node.js, and the installer copies only the files named in
templateDetails.xml. The compiledtemplate.cssand atemplate-rtl.csshave to exist before packaging, or the page loads without styles and the log shows nothing. -
Generate the RTL stylesheet from the same sources (for example by building with a small
rtlpartial that flips direction-dependent rules) or use logical properties and keep the stylesheet shared. -
Keep colour tokens as CSS custom properties so the same stylesheet supports the dark colour scheme described in the Manual page "Color scheme support": the template sets the
data-color-schemeattribute on<html>and fires thejoomla:color-scheme-changeevent so extensions such as editors can follow it. -
If you import Bootstrap’s SCSS, import only the parts you use. Joomla’s own scripts do not require Bootstrap CSS in a template, but the core layouts and the administrator-facing components emit Bootstrap 5 class names on the front end, so a template without Bootstrap must style those classes itself.
The pipeline is wired into the package build in Build and CI for extensions.
Accessibility checklist
Accessibility is partly a template’s responsibility, because the template owns the page skeleton, the colour and focus styles and the order of landmarks; components and modules own their own markup. The general principles, tools and WCAG success criteria are explained in Web accessibility and the Joomla-specific colour, contrast and right-to-left notes are in Colour modes, RTL and accessibility. For a Joomla template, check at least:
-
Document language and direction:
<html lang="..." dir="...">from$this->languageand$this->direction, so screen readers pick the right voice in each language of the multilingual Tallpine site. -
Landmarks: one
<header>, one<main>,<nav>and<aside>witharia-labelwhere there is more than one of a kind, and a<footer>. Render a landmark only if its position has modules. -
Skip link: the first focusable element jumps to
<main id="content">, is visible on focus and has enough contrast. -
Headings: the template provides the site name as a link or
<p>, not as an<h1>; the article or component supplies the single<h1>. Module chromes must not skip heading levels. -
Focus: never
outline: nonewithout a replacement; use:focus-visiblewith a 3:1 contrast ring. -
Contrast: text 4.5:1 (large text 3:1), non-text controls 3:1, in the light scheme and in the dark scheme if you offer one.
-
Zoom and reflow: the viewport meta tag does not set
user-scalable=noor a maximum scale; the layout reflows at 320 CSS pixels without horizontal scrolling and works at 200% text size. -
Motion: respect
prefers-reduced-motion. -
System messages: keep
<jdoc:include type="message" />before<main>so errors and notices are announced; the default message layout uses an alert role. -
Images: the logo has a text alternative (or the brand name is also printed as text); decorative images use an empty
alt. -
Forms and links: do not remove the label styling that components emit; links must be distinguishable by more than colour.
-
Error and offline pages: they have a language, a
<main>, a visible<h1>and a way back to the site. -
Test: run an automated checker (axe, Lighthouse, WAVE), then tab through every layout with the keyboard and with a screen reader. Automated tools find only a part of the issues.
Joomla’s own guidance, the Joomla Accessibility Team’s work and the Manual are the places to look for accessibility conventions when the template needs to follow Cassiopeia’s markup closely.
Packaging and installing
The template is installed as an extension package: a zip file with templateDetails.xml at its root. The folder name inside
the zip does not matter; the name element and the file lists do. Build the package from the template folder, excluding
development files:
cd tpl_tallpine_minimal
npm ci && npm run build:css
zip -r ../tpl_tallpine_minimal-1.0.0.zip . \
-x 'node_modules/*' -x 'package*.json' -x '*.map' -x '.DS_Store'
Source: Joomla Manual: Packages
Then install it:
-
Log in to the administrator and open System > Install > Extensions.
-
Use Upload Package File, or Install from Folder if the unpacked folder is on the server, or Install from URL.
-
The installer reads the manifest, copies the files to
templates/tallpine_minimal, the media folder tomedia/templates/site/tallpine_minimaland the language files, then creates a default template style named after the template. -
Open System > Site Template Styles, make the new style the default (the star icon) or assign it to chosen menu items under Menu Assignment, then edit it to set the parameters.
-
Open the front end, check the page in the browser’s network panel for 404s on CSS and JS, then use
?tp=1to check the positions.
From the command line, the same install can be scripted with php cli/joomla.php extension:install --path=...; see
Command-line interface.
For update delivery, add an <updateservers> element to the manifest pointing at an XML file on a server you control;
update sites, download keys and the Joomla Extensions Directory are covered in
Update servers and the JED and
Extension types, manifests and packaging. If the template
ships with the pkg_tallpine package, add it as a <file type="template" client="site" id="tallpine_minimal"> entry in
the package manifest so one upload installs the component, module, plugins and template together.
Common errors and pitfalls
-
Installation fails with "Unable to find install package" or "XML setup file not found".
templateDetails.xmlis not at the zip root, or the zip contains one wrapping folder. Re-zip from inside the template folder. -
The template installs but the page has no styles. The compiled CSS is missing from the zip, the
uriinjoomla.asset.jsondoes not match a file inmedia/.../css, orjoomla.asset.jsonis not listed in<files>. -
Positions show raw keys. The
.sys.inifile is missing from the manifest’s<languages>list. -
Uninstalling or reinstalling resets styles. Removing a template deletes its template styles and the menu assignments that used them; menu items fall back to the default style. A default style cannot be deleted while it is the default.
-
Editing files in
templates/on the server. The change vanishes the next time the package is installed. Keep the template in version control and change the package. -
Reusing
cassiopeiaas the name. A name that equals a core template is overwritten by core updates. Always choose a prefix of your own. -
Leaving
index.phpwithoutdefined('_JEXEC') or die;. Without the guard the file can be executed directly from the web, which discloses paths in error messages. In Joomla 6 the guard is_JEXEC;JPATH_PLATFORMis no longer defined without the Backward Compatibility 6 plugin. -
Using a template as a security boundary. A template controls presentation only; access control is applied by components and menu items. See Security in extensions.
Where to go next
The minimal template is deliberately not a replacement for Cassiopeia: it has no menu styling, no card chrome and no colour schemes. For most sites the better route is to start from a child template, as in Joomla child templates, and add overrides as in Template overrides and alternative layouts. Third-party frameworks and page builders that provide their own templates are discussed in Template frameworks and page builders.