Developers

Page layouts

A layout is the outermost HTML of a page. The component has already produced its markup by the time a layout runs; the layout wraps it, adds the masthead and the footer, and marks the places where modules go. Layout files sit at the top level of a template directory — never in a subdirectory — and the CMS picks one per request by name.

A layout is the only file in a template that decides what appears on a page. The stylesheets decide how it looks; the layout decides whether the component's output, the system messages and each module position are on the page at all. If something is missing from every page of a hub, this is the file to open.

The smallest layout that works

Two jdoc tags and the HTML around them:

<?php
defined('_HZEXEC_') or die();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
	<head>
		<jdoc:include type="head" />
	</head>
	<body>
		<jdoc:include type="message" />
		<jdoc:include type="component" />
	</body>
</html>

Save that as app/templates/northgate/index.php, register the template, and every page of the hub renders: com_bookings, the knowledge base, the members directory, all of it, unstyled. That is the floor. Everything else — the masthead, the navigation, the module positions — is added on top.

The shipped system template's component.php is almost exactly this file, which is why it works as the fallback:

<?php
/**
 * @package    hubzero-cms
 * @copyright  Copyright (c) 2005-2020 The Regents of the University of California.
 * @license    http://opensource.org/licenses/MIT MIT
 */

defined('_HZEXEC_') or die();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>" class="<?php echo $this->direction; ?>">
	<head>
		<jdoc:include type="head" />
	</head>
	<body class="contentpane">
		<jdoc:include type="message" />
		<jdoc:include type="component" />
	</body>
</html>

The layout files

File Rendered when
index.php The default. Used whenever nothing else is asked for. Required: a template without it is ignored and the system template runs instead.
component.php ?tmpl=component — the convention for modal, popup, and print output. Most components link to it.
offline.php The site is offline and the visitor lacks core.login.offline.
error.php An uncaught exception reaches the error handler.
cpanel.php The administrator control panel; com_cpanel sets tmpl=cpanel.
login.php The administrator login screen; com_login sets tmpl=login.
help.php The help viewer; com_help sets tmpl=help.
group.php A super group page, and the super group error page.
email.php The wrapper for HTML mail, through Hubzero\Mail\Template.

Any other value of tmpl names a layout the same way, so ?tmpl=print looks for print.php. The name is filtered to A-Z0-9_.- before it is used.

Only index.php is required. Everything else falls back, file by file, to core/templates/system — so a template that ships no error.php still gets a usable error page, styled by the system template rather than by yours.

This is what the shipped templates provide:

index component error offline cpanel login group help email
kimera (site)
lucent (site)
welcome (site)
kameleon (administrator)
system

core/templates/system/index.php is one line: it includes component.php. The system template is a safety net, not a design.

How the CMS picks a layout

Bootstrap\Site\Providers\DocumentServiceProvider runs after the component has produced its output. It reads tmpl from the request, defaults it to index, forces offline when the site is offline, and hands Hubzero\Document\Type\Html four things: the template name, the file name, the templates directory, and the template's saved parameters. The document includes that file, captures the output, replaces the jdoc tags in it, and returns the page.

The templates directory is app/templates when app/templates/{name} exists and core/templates otherwise, so a hub can shadow a shipped template by putting a directory of the same name under app/templates. Everything under app/ is site-local; nothing there is part of the distribution.

Inside a layout

A layout is included by the document object, so $this is that document. It runs in the global namespace, which is why the facades work with no use statements.

On $this What it gives you
$this->template The active template's name
$this->baseurl URL prefix for the templates directory, /app or /core
$this->params Hubzero\Config\Registry of the template style's saved parameters
$this->language, $this->direction Language tag and ltr/rtl, for the <html> element
$this->getTitle(), $this->setTitle() The document title, before head renders it
$this->addStyleSheet(), $this->addScript() Queue a file into the head block
$this->addStyleDeclaration(), $this->addScriptDeclaration() Queue inline CSS or JS
$this->countModules($expr) How many modules a position holds
$this->getBuffer($type, $name) Rendered output for a jdoc type, before it is substituted

Every layout opens with the entry guard:

defined('_HZEXEC_') or die();

countModules() takes a boolean expression over position names, which is how a layout collapses empty columns:

<?php if ($this->countModules('left or right')) : ?>

The expression is evaluated with eval(), so keep it to position names and the operators and, or, xor, arithmetic, and comparisons.

jdoc includes

jdoc:include marks a hole in the layout. The document finds every tag, asks a renderer for the corresponding markup, and substitutes it in.

type Renders
component The component's output. A layout without this shows nothing.
head <base>, meta tags, <title>, <link>s, and <script>s queued by anything on the page.
message The queued system messages, as <p class="passed">, .warning, .error, .info.
modules Every published module in the position named by name.
module One module instance, looked up by name.

Module and modules tags are rendered before every other type, so a module may push a stylesheet or a script and still have it appear in the head block.

The tag is matched by a regular expression, not by an XML parser, and the expression is strict:

  • type must be the first attribute, and its value must be in double quotes;
  • a single space must follow the closing quote of type;
  • the tag must be self-closing.

<jdoc:include type="component" /> works. <jdoc:include type="component"/> does not — it is left in the page as literal text. If a hole in your layout renders as nothing at all, check the spacing first. Viewing source is the quickest test: a mis-spaced tag appears verbatim in the delivered HTML, where the browser drops it as an unknown element.

Module positions

A position is just a string. A layout asks for one by name, and the loader returns whatever the administrator has assigned to it. Kimera's content region shows the usual shape — an optional left column, the component, an optional right column, and a footer:

			<div id="wrap">
				<main id="content" class="<?php echo Request::getCmd('option', ''); ?>">
					<div class="inner<?php if ($this->countModules('left or right')) { echo ' withmenu'; } ?>">
					<?php if ($this->countModules('left or right')) : ?>
						<section class="main section">
							<div class="section-inner">
					<?php endif; ?>

					<?php if ($this->countModules('left')) : ?>
							<aside class="aside">
								<jdoc:include type="modules" name="left" />
							</aside><!-- / .aside -->
					<?php endif; ?>
					<?php if ($this->countModules('left or right')) : ?>
							<div class="subject">
					<?php endif; ?>

								<!-- start component output -->
								<jdoc:include type="component" />
								<!-- end component output -->

					<?php if ($this->countModules('left or right')) : ?>
							</div><!-- / .subject -->
					<?php endif; ?>
					<?php if ($this->countModules('right')) : ?>
							<aside class="aside">
								<jdoc:include type="modules" name="right" />
							</aside><!-- / .aside -->
					<?php endif; ?>

					<?php if ($this->countModules('left or right')) : ?>
							</div>
						</section><!-- / .main section -->
					<?php endif; ?>
					</div><!-- / .inner -->
				</main>

				<footer id="footer">
					<jdoc:include type="modules" name="footer" />
				</footer>
			</div><!-- / #wrap -->
		</div>
		<jdoc:include type="modules" name="endpage" />

The <positions> block in templateDetails.xml does not restrict anything. It is read only by com_modules when it builds the position picker on the module edit screen, so a position that is used in index.php but not declared still renders; it simply does not appear in that list. Kimera is an example of the drift: it uses breadcrumbs and endpage without declaring them, and declares banner and introblock without using them.

Template Positions used in its layouts
kimera helppane, notices, search, user3, breadcrumbs, welcome, left, right, footer, endpage
lucent html-head, notices, helppane, user3, left, right, search, endpage
kameleon notices, menu, title, toolbar, submenu
system notices, helppane, endpage (in group.php only)
welcome none

Why a position renders nothing

This is the single most common "the framework is broken" report, and it is almost never the framework. Four independent things have to line up, and nothing in the CMS checks that they do.

  1. A layout has to include the position. A <jdoc:include type="modules" name="mainnav" /> tag somewhere in the layout that is actually running.
  2. A module has to be published to that exact string. Positions are compared literally. left and Left are two positions.
  3. The module has to be enabled and visible to the current user and the current menu item.
  4. The layout that is running has to be yours. Which is not a given — see below.

The position picker in the administrator does not verify step 1. Look at Modules::positionsTask(): the list it offers is the union of the <positions> declared by every installed template for that client and every DISTINCT(position) already present in #__modules. So a position appears in the picker if any template mentions it, or if any module already sits in it. Being in the list means nothing about whether anything will draw it.

The shipped install demonstrates all of this

Two facts about a fresh hub, both in core/bootstrap/Install/sql/mysql/data.sql, and both worth checking before you suspect your own code.

The default site style is welcome, and welcome renders nothing.

INSERT INTO `#__template_styles` (`id`, `template`, `client_id`, `home`, `title`, `params`)
VALUES (1,'welcome',0,'1','Welcome Template','{"flavor":"","template":"kimera"}');

welcome/index.php contains no jdoc:include tag of any type — not modules, not message, not even component. It prints a fixed splash page and discards whatever the request produced. On a fresh hub, therefore, no module renders and no component output reaches the page, and both are working as built.

The splash page's ready link is ?getstarted=1. That branch of welcome/index.php looks up the style named by the template's own template parameter — seeded as kimera — sets home = 0 on every site style and home = 1 on that one, and redirects. From then on the hub renders kimera. An administrator can do the same thing from Extensions → Templates by making another style the default.

The main menu is published to a position no site template includes.

INSERT INTO `#__modules` (…, `position`, …, `published`, `module`, …, `client_id`, …)
VALUES (1,'Main Menu',…,'position-7',…,1,'mod_menu',…,0,'*');

position-7 appears in no layout of kimera, lucent, welcome or system. The seeded Login Form module is in the same position. So after switching to kimera the hub still has no main menu, and the module manager still shows both modules as published — to a position labelled a friendly Position 7, because com_modules ships a COM_MODULES_POSITION_POSITION-7 string for it.

The seeded Breadcrumbs module, by contrast, is in breadcrumbs, which kimera does include, and it appears immediately. That is the difference, and it is entirely in the position column.

Move the menu to a position your layout includes — user3 for kimera and northgate, or whatever northgate calls its main navigation — and it renders.

The check to run

When a position of yours is empty, in this order.

1. Which layout is actually running? The home style for the site client, or whatever the menu item's template_style_id chose:

SELECT id, template, home FROM `#__template_styles` WHERE client_id = 0;

2. Does that template's layout include the position?

grep -rn 'name="mainnav"' app/templates/northgate/*.php

3. What is published there, and to which client?

SELECT id, title, position, published, client_id FROM `#__modules` WHERE position = 'mainnav';

Step 1 catches the fresh-hub case and every case where a menu item selected a different style. Step 2 catches the typo and the layout you edited that is not the one running. Step 3 catches the module published to a position one character away from yours, and the module published to the administrator client.

See Modules: loading for what the loader does with a position, and Overrides for module chrome — the style attribute on a jdoc tag and the modChrome_* functions a template can add.

The head block

<jdoc:include type="head" /> emits everything the request has queued: the <base> tag, meta tags, the description and generator, <title>, link relations, stylesheets, style declarations, scripts, script declarations, and the strings object that Lang::script() fills for JavaScript translation.

Order matters, and the head block is late in the sequence, so anything a layout must load before extension assets goes in the markup above it. Kimera links its own stylesheet first and lets the head block follow:

<!DOCTYPE html>
<html dir="<?php echo $this->direction; ?>" lang="<?php echo $this->language; ?>" class="<?php echo implode(' ', $cls); ?>">
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<link rel="stylesheet" type="text/css" media="all" href="<?php echo $this->baseurl; ?>/templates/<?php echo $this->template; ?>/css/index.css?v=<?php echo filemtime(__DIR__ . '/css/index.css'); ?>" />

		<jdoc:include type="head" />

		<!--[if IE 9]>
			<link rel="stylesheet" type="text/css" media="screen" href="<?php echo $this->baseurl; ?>/templates/<?php echo $this->template; ?>/css/browser/ie9.css" />
		<![endif]-->
		<!--[if lt IE 9]>
			<script type="text/javascript" src="<?php echo $this->baseurl; ?>/templates/<?php echo $this->template; ?>/js/html5.js"></script>
			<link rel="stylesheet" type="text/css" media="screen" href="<?php echo $this->baseurl; ?>/templates/<?php echo $this->template; ?>/css/browser/ie8.css" />
		<![endif]-->
	</head>
	<body>

Anything the layout adds with $this->addStyleSheet() or $this->addScript() instead appears inside the head block, after whatever the component queued. Kameleon does that for its own index.css, which is why its rules need enough specificity to win.

Error layouts

error.php is handed an Error document with two extra members:

  • $this->error — the Exception. getCode() and getMessage() are what you want; the code is not guaranteed to be an HTTP status, so test it before displaying it, as kimera/error.php does.
  • $this->debug — true when the site's Debug System setting is on. $this->renderBacktrace() returns the stack trace as a table.

Never show getMessage() unless $this->debug is true. Kimera maps 403, 404 and everything else onto three translated strings and shows the raw message only when debugging.

Parameters

A template declares parameters in the <config> block of its templateDetails.xml, and an administrator sets them per template style. $this->params reads them back. Kimera uses them for a header variant, a background pattern, and two accent colours, then builds a style declaration from the result:

$bground = $this->params->get('backgroundImage', $this->params->get('background', 'delauney'));
$styles  = include_once __DIR__ . '/css/theme.php';
if ($styles)
{
    $this->addStyleDeclaration($styles);
}

Kameleon does the same with css/themes/custom.php for its custom colour theme. Both files return a CSS string rather than printing one.

Because parameters belong to a template style, one template can be installed several times with different settings, and a menu item can select a style through its template_style_id. ?templateStyle={id} overrides that for one request. That is how one northgate template can serve two partner institutions with different accent colours, and it is also the first thing to check when one page of a hub looks different from all the others.

Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.