Joomla Error, Offline and Component Pages

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 template is more than index.php. Three more files in the template folder decide what a visitor sees when something goes wrong or when the site is not meant to be seen: error.php renders every uncaught exception (the "page not found" a hiker meets after following an old trail link), offline.php renders the whole site while it is in maintenance mode, and component.php renders a page without the site chrome, which is what print views, popups and iframes need. This page explains how Joomla chooses each of them, what the core Cassiopeia versions do, how to replace them in a child template, which module positions and Global Configuration settings feed them, what they must never reveal, and how the Joomla Dialog opens a tmpl=component URL in a modal window. It ends with the pieces that run outside Joomla (web server error pages), because an error page produced by Joomla cannot help when PHP or the database is down.

It was written against Joomla 6.2 (current, requiring PHP 8.3 or newer) and checked against the Developer Manual and the 6.2 source tree on 2026-10-30. The same files and the same lookup exist in Joomla 4 and 5. Where a feature arrived later, the text says so. The audience is the themer, the site builder and the administrator; the examples are code for the cassiopeia_tallpine child template of the Tallpine Hiking Club. Accessibility rules that apply to any page (a clear heading, a visible way back, contrast, focus) are in Web accessibility and are not repeated here; the dark-mode and right-to-left side of the same files is in Colour modes, RTL and accessibility.

The entry files are introduced briefly in Templates and template styles; the inheritance rules that make a child template able to replace one of them are in Joomla child templates; a template that has to provide all of them itself is built in Building a template from scratch. This page is the detailed treatment of the three special files.

Which file does Joomla render

For the HTML document type, SiteApplication::render() decides the theme file name and HtmlDocument::_fetchTemplate() locates it. Reading both in the 6.2 source gives the following order:

  1. The file is <value of tmpl>.php, where the tmpl request variable defaults to index. So tmpl=component selects component.php.

  2. If tmpl=offline is requested while the site is online, index.php is used instead, so nobody can preview the maintenance page from outside.

  3. If the site is offline and the current user does not have the core.login.offline permission, the file becomes offline.php and the response header Status: 503 Service Temporarily Unavailable is set. The user state users.login.form.data is filled with the requested address, so a successful offline login returns to it.

  4. If the template folder of the active template does not exist and the site is online, component.php is used.

  5. When an exception escapes the application, ExceptionHandler builds an ErrorDocument and its render() forces the file name error.php (see the next section).

  6. _fetchTemplate() looks for the file in templates/<template>/. If it is missing and the style has a parent, it looks in templates/<parent>/. Otherwise it falls back to templates/system/, and if that folder lacks the file too, it uses templates/system/index.php.

The last step is why a child template can simply omit error.php, offline.php and component.php and inherit the parent’s, and why a complete replacement in the child hides the parent’s copy entirely: Joomla loads one file, never a merge of two.

flowchart TD REQ[Request reaches SiteApplication] --> OFF{Site offline and user lacks core.login.offline?} OFF -- yes --> F1[offline.php with Status 503] OFF -- no --> TMPL{tmpl request value} TMPL -- component --> F2[component.php] TMPL -- absent or index --> F3[index.php] EXC[Uncaught exception anywhere] --> EH[ExceptionHandler builds ErrorDocument] EH --> F4[error.php with Status = exception code] F1 --> LOOK{File in templates/child?} F2 --> LOOK F3 --> LOOK F4 --> LOOK LOOK -- yes --> RUN[Run it] LOOK -- no --> PAR{File in templates/parent?} PAR -- yes --> RUN PAR -- no --> SYS[templates/system copy] SYS --> RUN

The three files differ in what they may assume about the page:

File $this is Modules available Typical output

error.php

ErrorDocument (extends HtmlDocument), with $this->error and $this->debug

Only when the application has an identity and a language

A branded explanation and a way back

offline.php

HtmlDocument

Not used by the core versions

Site name, message, image and a login form

component.php

HtmlDocument

Not used by the core versions

Messages and the component output only

error.php and the ErrorDocument

What happens when an exception is thrown

Joomla installs ExceptionHandler::handleException() as the exception handler. It does four things, in order:

  1. Logs the exception. A RouteNotFoundException (a 404) and a NotAcceptable (406) are logged at notice level in the category client-error, so broken links do not flood the log. Every other Throwable is logged as critical in the category error, with the class, message and stack trace.

  2. For a 404 on a site that is offline, redirects to index.php, so visitors of a site under maintenance see the offline page and not a "not found". This only applies to the site and the administrator, not to the command line or the Web Services API.

  3. Clears all output buffers, creates a fresh document of the requested format (HTML by default; the format request value is honoured for a document that does not exist yet) and fires onAfterInitialiseDocument again on the new document.

  4. Renders it. If the rendering itself throws, the original error is rethrown together with the rendering failure, so a bug in error.php surfaces as a plain PHP error.

ErrorDocument::render() then sets the HTTP status from the exception code. A code below 400 or above 599 becomes 500. When the Error Reporting setting of the Global Configuration is development or maximum, the exception message is appended to the status text of the response line, which means that a development configuration leaks messages in the HTTP headers as well as on the page. It sets $this->debug from the debug value of the render parameters (System Debug in the Global Configuration), sets $this->error, and finally renders the file error.php through the normal template lookup. Note that ErrorDocument::loadRenderer() forces the HTML renderers, so the usual jdoc:include elements work in error.php.

<?php
// What error.php can use (libraries/src/Document/ErrorDocument.php, simplified)
/** @var Joomla\CMS\Document\ErrorDocument $this */

$this->error;             // the Throwable: getCode(), getMessage(), getPrevious(), getTrace()
$this->debug;             // true when System Debug is on
$this->renderBacktrace(); // HTML for the trace, rendered by the layout joomla.error.backtrace
$this->countModules('error-404'); // inherited from HtmlDocument

The consequence of the fresh document is important for template authors: the application is not fully initialised when an early failure occurs (for example while the database is unreachable or before a language is loaded). Cassiopeia therefore starts error.php with a guard that its own source explains in a comment (the module renderer does not work properly because of incomplete application initialisation):

// Get the error code
$errorCode = $this->error->getCode();

// The module renderer will not work properly due to incomplete Application initialisation
$renderModules = $app->getIdentity() && $app->getLanguage();

and wraps every jdoc:include type="modules" in if ($renderModules && ...). Copy that guard into every error page you write.

What the core templates do

Cassiopeia’s error.php rebuilds the site header (logo, optional site description, menu and search positions), then the content area, then a footer. In the content area:

  • If $renderModules is true and the position error- followed by the status code contains modules, those modules are the page content, together with the system messages.

  • Otherwise it prints a fixed block: the heading from the language string JERROR_LAYOUT_PAGE_NOT_FOUND (in English, "The requested page can’t be found."), an explanation, and a list of likely causes (out-of-date bookmark, mistyped address, outdated search engine listing, no access to the page). The heading is the same for every status code, so a 500 in the default template still says that the page cannot be found. Replace it for any site where a server error must read differently.

  • With debug on, it renders the backtrace and then walks getPrevious() to render every chained exception, using setError() to switch the document to each one and restoring the original at the end.

  • The footer position and, last, the debug position are rendered when modules can be rendered.

The core templates/system/error.php is the last fallback: it sets the page title to the code and the escaped message, loads system-site-error.css (and an RTL variant), and also honours the error- positions. It prints the code and the escaped message in a box, so the system fallback is more talkative than Cassiopeia’s.

The error-403 and error-404 positions

Cassiopeia declares two positions in templateDetails.xml, with a comment saying they are used directly in error.php and listed so that they appear in the list of available positions:

<positions>
    <!-- ... the regular positions ... -->
    <!-- used directly in the error.php and included here so the position will appear in the list of available positions -->
    <position>error-403</position>
    <position>error-404</position>
</positions>

This is a feature that site builders can use without touching PHP. The Developer Manual’s page on custom error pages states that, since Joomla 5.0, the default Cassiopeia template can show custom 403 and 404 pages: publish a module in the position error-403 or error-404 and it replaces the default message for that code, and if no module is published in the position the default text is shown. When debug is on, the debug information still appears below the module. Source: Joomla Manual: Custom Error Pages. Because the code builds the position name from the actual status code (error- plus $this->error->getCode()), the mechanism is not limited to 403 and 404: any other status, for example error-410 for a trail that was removed on purpose, works as soon as a module is assigned to a position with that name. Module positions are free text in the module’s Position field, so you can type a position that the manifest does not list; listing it only makes it appear in the drop-down, which is why a child template should list the codes it uses.

For the Tallpine Hiking Club the board wants the 404 page to point hikers to the trail list, a search box and the newest news. A site builder does this in the Administrator:

  1. Content, Site Modules, New, Custom.

  2. Title Trail not found, Position error-404, Status Published, Menu Assignment On all pages.

  3. In the editor write a heading, a short text and links to the trails category and the home page, then save.

  4. Request a URL that does not exist, such as /no-such-trail, in a private window, and confirm the status code with the command line (see the testing section).

A module assigned to error-404 is loaded only when modules can be rendered. If the 404 occurs before the language or identity exists, the default text appears instead. Treat the module as an improvement and the template text as the guarantee.

A Tallpine error.php

The Tallpine child replaces error.php to give 404, 403 and 500 different headings and to hide the exception message from visitors. Everything is escaped, nothing about the failure is shown unless $this->debug is on, and the guard protects the modules.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

/** @var Joomla\CMS\Document\ErrorDocument $this */

$app = Factory::getApplication();
$wa  = $this->getWebAssetManager();

$dir = $this->direction === 'rtl' ? 'rtl' : 'ltr';

$wa->usePreset('template.cassiopeia.' . $dir)
    ->useStyle('template.active.language')
    ->useStyle('template.user')
    ->useStyle('template.tallpine');

// Keep the dependency chain of the active direction, as the parent template does
$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . $dir]);

$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
$this->setMetaData('robots', 'noindex, nofollow');

$errorCode = (int) $this->error->getCode();

// Server errors share one text; client errors get their own heading
$headingKey = match (true) {
    $errorCode === 403 => 'TPL_CASSIOPEIA_TALLPINE_ERROR_403_TITLE',
    $errorCode === 404 => 'TPL_CASSIOPEIA_TALLPINE_ERROR_404_TITLE',
    default            => 'TPL_CASSIOPEIA_TALLPINE_ERROR_500_TITLE',
};

// The module renderer will not work properly due to incomplete Application initialisation
$renderModules = $app->getIdentity() && $app->getLanguage();
?>
<!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 error_site tallpine-error code-<?php echo $errorCode; ?>">
    <header class="header container-header full-width">
        <a class="brand-logo" href="<?php echo $this->baseurl; ?>/">Tallpine Hiking Club</a>
        <?php if ($renderModules && $this->countModules('menu')) : ?>
            <jdoc:include type="modules" name="menu" style="none" />
        <?php endif; ?>
    </header>

    <main class="container-component">
        <?php if ($renderModules && $this->countModules('error-' . $errorCode)) : ?>
            <jdoc:include type="message" />
            <jdoc:include type="modules" name="error-<?php echo $errorCode; ?>" style="none" />
        <?php else : ?>
            <h1><?php echo Text::_($headingKey); ?></h1>
            <p><?php echo Text::_('TPL_CASSIOPEIA_TALLPINE_ERROR_HELP'); ?></p>
            <p>
                <a class="btn btn-primary" href="<?php echo $this->baseurl; ?>/">
                    <?php echo Text::_('JERROR_LAYOUT_HOME_PAGE'); ?>
                </a>
            </p>
        <?php endif; ?>

        <?php if ($this->debug) : ?>
            <?php echo $this->renderBacktrace(); ?>
            <p><?php echo htmlspecialchars($this->error->getMessage(), ENT_QUOTES, 'UTF-8'); ?></p>
        <?php endif; ?>
    </main>
</body>
</html>

The strings live in the child’s language file, next to tpl_cassiopeia_tallpine.ini. The template name is part of the key prefix by convention, and the file is loaded by _fetchTemplate() before the template file runs, which is why a language string can be used in error.php:

; templates/cassiopeia_tallpine/language/en-GB/tpl_cassiopeia_tallpine.ini
TPL_CASSIOPEIA_TALLPINE_ERROR_403_TITLE="This trail is for members"
TPL_CASSIOPEIA_TALLPINE_ERROR_404_TITLE="We could not find that page"
TPL_CASSIOPEIA_TALLPINE_ERROR_500_TITLE="Something went wrong on our side"
TPL_CASSIOPEIA_TALLPINE_ERROR_HELP="Try the trail list or the search box, or go back to the home page."

Notes on the example:

  • robots is set to noindex, nofollow on purpose: an error page that is cached by a search engine under a soft 200 would be a problem, and the real protection is the status code, which Joomla already sets.

  • The German file under language/de-DE/ carries the same keys, see Language files and internationalisation.

  • Text::_() returns the string as written in the language file; strings with markup are trusted because they come from the template, not from a visitor.

  • The message of the exception is printed only inside the $this->debug branch. A 404 message contains the requested path and a database exception can contain an SQL fragment.

Redirecting broken URLs instead of showing a page

Many 404 responses are not mistakes but changed URLs. The Redirect system plugin handles that before the template gets involved. It subscribes to onError, acts only on the site client and only on code 404, and looks up the requested URL in the table #__redirect_links. If an enabled link exists, it redirects (default status 301, or the status stored on the link when advanced mode is enabled in the Redirects component options). If no link exists and the component option to collect URLs is on, the plugin stores the URL so that a Super User can later fill in a new destination. The error page is only what remains when no redirect applies. The workflow, the options and the SEO side are in SEO, SEF URLs, redirects and Schema.org.

Pitfalls

  • A fatal error in error.php. It is the worst place to fail, because the handler then rethrows. Keep the file small, escape everything and test it with a nonexistent URL and with a forced exception.

  • Modules on an early failure. Without the $renderModules guard, an error that happens before the identity or the language exists produces a second error inside the error page.

  • Status codes. A custom page must not change the status. A page that looks like "not found" but is served with 200 is a "soft 404", and search engines index it. Joomla sets the right status for you; do not call setHeader('status', ...) in error.php.

  • Caching. ExceptionHandler calls allowCache(false), so the response is not cached by Joomla, but a reverse proxy or CDN can still cache 404 pages if it is configured to, see Caching and performance.

  • Debug on in production. System Debug and Error Reporting maximum or development print backtraces, file paths, database names and exception messages. The recommended production values are in Global configuration and Security hardening, MFA and passkeys.

  • Not just HTML. The Web Services API and the console use other renderers; error.php is only for the HTML (site and administrator) document type.

  • The administrator. The Atum administrator template has its own error.php with the same contract (ErrorDocument); a site child template never affects it, see The Administrator template Atum.

offline.php and maintenance mode

The settings

System, Global Configuration, Site contains the offline settings. The form definition in the 6.2 source gives their exact behaviour:

Setting (property) Values Meaning

Site Offline (offline)

No (0, default) or Yes (1)

Switches maintenance mode on. It is a boolean filter.

Offline Message display (display_offline_message)

Hide (0), Custom message (1, default), Use language string (2)

Shown only when the site is offline. Custom uses offline_message; the language string is JOFFLINE_MESSAGE ("This site is down for maintenance.<br>Please check back again soon.").

Custom Message (offline_message)

Text, filter safehtml

Shown only for display mode 1. The safehtml filter strips dangerous HTML (scripts, event handlers) when the configuration is saved, but permits ordinary markup such as links.

Offline Image (offline_image)

A media file

Shown only when the site is offline. A relative URL is accepted.

All four values end up in configuration.php as $offline, $display_offline_message, $offline_message and $offline_image, see configuration.php and .htaccess. The template reads them with $app->get('offline_message').

Who still sees the site

The check in SiteApplication::render() is $this->getIdentity()->authorise('core.login.offline'). Two things follow:

  • The permission is a normal ACL action Offline Access in the Global Configuration permissions. By default only Super Users hold it, and group permissions can add more. Granting it to the Tallpine Administrator group lets board members log in while the club’s site is in maintenance, see Users, groups, access levels and ACL.

  • A guest never has it, so a guest always sees offline.php with a 503. A logged-in member without the permission is still offline: Joomla shows the offline page to them too, and the way out is to log in as an authorised user.

The Administrator is not subject to this check, so a Super User can always sign in at /administrator and switch the site back on. That is also the safety net if the offline login form on the site is broken.

The core sets only the Status header in this code path, so the page is a 503 without a Retry-After header. If your search engine strategy needs one, add it at the web server or reverse proxy.

The command line

The console has two commands for the switch, available since 4.0, which makes maintenance scriptable (for example around a deployment or a backup):

# Put the site into offline mode
php cli/joomla.php site:down

# Bring it back
php cli/joomla.php site:up

Both write the offline option of configuration.php (internally the command runs the config:set logic with offline=true). See The Joomla command line interface.

What the core offline.php does

Cassiopeia’s offline.php loads the same colour theme, font scheme and assets as index.php, plus the template.offline style. In the body it shows:

  1. the system messages (jdoc:include type="message")

  2. the logo or site title

  3. the offline image, if one is set, through HTMLHelper::_('image', ...)

  4. the offline message when display_offline_message is 1 and offline_message is not empty, or JOFFLINE_MESSAGE when it is 2. The custom text is printed as stored, not escaped, because the safehtml filter already applied when it was saved

  5. a login form that posts to the real login task: hidden inputs option=com_users, task=user.login, a return value that is base64_encode(Uri::base()) and the CSRF token from HTMLHelper::_('form.token')

  6. extra login buttons from AuthenticationHelper::getLoginButtons('form-login'), which is how passkey (WebAuthn) and other authentication plugin buttons appear on the offline page. The buttons come with data- attributes, an icon and an onclick handler that the template copies through.

The system template’s offline.php is the fallback and does the same with its own CSS (system-site-offline.css and system-site-general.css). It prints the sitename escaped and shows the image only if the file exists.

A Tallpine offline.php

The club closes the site twice a year, for the annual membership renewal, and wants a plain page with the logo, the message from Global Configuration, and the same login buttons as the main site so that the Trail Wardens can use their passkeys. The file reuses the parent’s assets and keeps the form’s security properties.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Users\Administrator\Helper\AuthenticationHelper;

/** @var Joomla\CMS\Document\HtmlDocument $this */

$app = Factory::getApplication();
$wa  = $this->getWebAssetManager();

$dir = $this->direction === 'rtl' ? 'rtl' : 'ltr';

$wa->usePreset('template.cassiopeia.' . $dir)
    ->useStyle('template.active.language')
    ->useStyle('template.offline')
    ->useStyle('template.user')
    ->useStyle('template.tallpine');

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . $dir]);

$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
$this->setMetaData('robots', 'noindex, nofollow');

$sitename     = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
$mode         = (int) $app->get('display_offline_message', 1);
$offlineText  = trim((string) $app->get('offline_message', ''));
$extraButtons = AuthenticationHelper::getLoginButtons('form-login');
?>
<!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 offline tallpine-offline">
    <jdoc:include type="message" />
    <main class="offline-card">
        <h1><?php echo $sitename; ?></h1>

        <?php if ($app->get('offline_image')) : ?>
            <?php echo HTMLHelper::_('image', $app->get('offline_image'), $sitename, [], false, 0); ?>
        <?php endif; ?>

        <?php if ($mode === 1 && $offlineText !== '') : ?>
            <?php // Stored through the safehtml filter by Global Configuration ?>
            <p><?php echo $offlineText; ?></p>
        <?php elseif ($mode === 2) : ?>
            <p><?php echo Text::_('JOFFLINE_MESSAGE'); ?></p>
        <?php endif; ?>

        <form action="<?php echo Route::_('index.php', true); ?>" method="post" id="form-login">
            <div class="mb-3">
                <label for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
                <input name="username" id="username" type="text" class="form-control"
                       autocomplete="username" autocapitalize="none" required>
            </div>
            <div class="mb-3">
                <label for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
                <input name="password" id="password" type="password" class="form-control"
                       autocomplete="current-password" required>
            </div>

            <button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>

            <?php foreach ($extraButtons as $button) : ?>
                <?php $dataKeys = array_filter(array_keys($button), static fn ($key) => str_starts_with((string) $key, 'data-')); ?>
                <button type="button" id="<?php echo htmlspecialchars((string) $button['id'], ENT_QUOTES, 'UTF-8'); ?>"
                        class="btn btn-secondary <?php echo htmlspecialchars((string) ($button['class'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
                        <?php foreach ($dataKeys as $key) : ?>
                            <?php echo htmlspecialchars((string) $key, ENT_QUOTES, 'UTF-8'); ?>="<?php echo htmlspecialchars((string) $button[$key], ENT_QUOTES, 'UTF-8'); ?>"
                        <?php endforeach; ?>
                        <?php if (!empty($button['onclick'])) : ?>
                            onclick="<?php echo htmlspecialchars((string) $button['onclick'], ENT_QUOTES, 'UTF-8'); ?>"
                        <?php endif; ?>>
                    <?php if (!empty($button['icon'])) : ?>
                        <span class="<?php echo htmlspecialchars((string) $button['icon'], ENT_QUOTES, 'UTF-8'); ?>"></span>
                    <?php endif; ?>
                    <?php echo Text::_($button['label']); ?>
                </button>
            <?php endforeach; ?>

            <input type="hidden" name="option" value="com_users">
            <input type="hidden" name="task" value="user.login">
            <input type="hidden" name="return" value="<?php echo base64_encode(Uri::base()); ?>">
            <?php echo HTMLHelper::_('form.token'); ?>
        </form>
    </main>
</body>
</html>

Each entry returned by getLoginButtons() is an array with an id, a language key in label (hence Text::_()), an optional class, icon, image or svg, an onclick handler and any number of data- keys at the top level, which is exactly what the core offline.php forwards. The example above copies the data- keys, onclick and the icon class, and escapes them. An authentication plugin that adds a button is trusted code, but escaping costs nothing.

Pitfalls

  • CSRF token. The form posts to the real login task. A template author who drops HTMLHelper::_('form.token') makes the offline login fail with an invalid token.

  • Caching. If the reverse proxy caches the 503, visitors keep seeing it after you bring the site back. Make sure the proxy does not cache a 503, or purge it after site:up.

  • MFA. When multi-factor authentication is on for the Super User, the login through the offline form continues to the second step after the first factor; see Security hardening, MFA and passkeys.

  • Third-party assets. The offline page is the last page that should depend on an external CDN or font host. If your fonts are loaded from a remote URL, the page is slower and visitors' addresses are sent to that host. Use the local font scheme.

  • Message HTML. The safehtml filter applies when the Global Configuration is saved. A value written straight to configuration.php bypasses it; print only values that came from the form, or escape them yourself as in the system fallback for the site name.

  • Forgetting to switch it off. A failed deployment can leave the site offline. Put site:up in the deployment script’s cleanup path, and see Updating Joomla and Automated Core Updates for the recovery steps after a failed update.

component.php

When it is used

component.php is the skeleton of every page requested with tmpl=component: the modals of the Administrator and of site pickers, iframe targets, a print view, and the pages loaded by embed code. It is also the template file that Joomla uses on an online site when the template folder of the active style cannot be found. The point of the file is what it leaves out: no header, no menus, no sidebars, no footer and no module positions.

Cassiopeia’s component.php registers the same assets as index.php (the colour theme, the optional font scheme, the template.cassiopeia.ltr or .rtl preset, template.user), adds the favicon links, and then outputs only metas, a viewport meta element, styles, scripts, message and component, in a body with the classes contentpane component (plus rtl for right-to-left languages). The system template has an even smaller version.

The component renderer itself does nothing special: ComponentRenderer::render() returns the content already produced by the component. That is why a tmpl=component page is the cheapest way to embed a component view, and why a modal that loads one pays for the whole component dispatch, but for no modules.

Print views

Joomla 3 and 4 articles showed a print icon that opened tmpl=component&print=1. In the 6.2 source, the shared icon layouts under layouts/joomla/content/icons contain only create, edit and edit_lock; the print icon layouts are gone, so a print link is something a template or an override adds itself. The component still understands the request: the com_content article view reads print as a boolean and, when it is true, sets the meta robots tag to noindex, nofollow, so a print view does not compete with the real article in search results.

A link that the Tallpine trail article layout can add in its override (see Template overrides and alternative layouts):

<?php
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;

// $this->item is the article of the trail report
$printUrl = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))
    . '?tmpl=component&print=1';
?>
<a class="btn btn-secondary" href="<?php echo htmlspecialchars($printUrl, ENT_QUOTES, 'UTF-8'); ?>"
   rel="nofollow" target="_blank">
    Print this trail
</a>

The print view needs a print stylesheet; a screen stylesheet written for the site grid produces poor paper output. The asset is registered in the child’s joomla.asset.json with the media attribute so the browser applies it to printing only (the Web Asset Manager is described in Web Asset Manager):

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "cassiopeia_tallpine",
  "version": "1.0.0",
  "assets": [
    {
      "name": "template.tallpine",
      "type": "style",
      "uri": "templates/cassiopeia_tallpine/css/tallpine.css",
      "dependencies": ["template.cassiopeia.ltr"]
    },
    {
      "name": "template.tallpine.print",
      "type": "style",
      "uri": "templates/cassiopeia_tallpine/css/print.css",
      "attributes": { "media": "print" }
    }
  ]
}

A Tallpine component.php

The child needs its own component.php only to load the print stylesheet and a small page title block. It starts from the parent’s asset setup and uses the same five includes.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;

/** @var Joomla\CMS\Document\HtmlDocument $this */

$app   = Factory::getApplication();
$wa    = $this->getWebAssetManager();
$print = $app->getInput()->getBool('print', false);

$dir = $this->direction === 'rtl' ? 'rtl' : 'ltr';

$wa->usePreset('template.cassiopeia.' . $dir)
    ->useStyle('template.active.language')
    ->useStyle('template.user')
    ->useStyle('template.tallpine')
    ->useStyle('template.tallpine.print');

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . $dir]);
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
</head>
<body class="contentpane component tallpine-component<?php echo $print ? ' is-print' : ''; ?><?php echo $this->direction === 'rtl' ? ' rtl' : ''; ?>">
    <jdoc:include type="message" />
    <jdoc:include type="component" />
</body>
</html>

Pitfalls

  • Missing includes. Without scripts, a modal form loses its validation and the buttons that talk to the parent window. Without message, a "Saved" or error notice is lost. Keep all five includes.

  • Third-party templates. Some templates ship a component.php without the asset setup. Their popups then show unstyled content. Test the tmpl=component view of every component page you rely on.

  • Access control is not relaxed. tmpl=component changes the layout of the page, not who may see it. The component still checks ACL and access levels, so a Trail Warden-only report is still protected. Do not treat the parameter as a way to hide something.

  • Indexing. The print view duplicates content. The core sets noindex, nofollow for print=1; for another tmpl=component URL that you link publicly, add it yourself or exclude it in robots.txt.

  • Framing. If a page is meant to be embedded, remember that Joomla’s HTTP Headers plugin can send X-Frame-Options or a frame-ancestors policy that stops a foreign site from embedding it. The Dialog (below) frames pages from the same site, which those headers allow by default.

What it is

The Joomla Dialog is the popup library of the current Joomla. It is a custom element (<joomla-dialog>) built on the native HTML <dialog> element, loaded from the Web Asset Manager as the script asset joomla.dialog (an ES module registered in the import map) with an optional companion joomla.dialog-autocreate. The core uses it for the pickers that open a tmpl=component view in a frame: the media field, the user field, the content selector in the editor and toolbar buttons all declare joomla.dialog as a dependency in the system asset registry (media/system/joomla.asset.json in a built site). Older code and older books open modals with Bootstrap’s modal, usually through HTMLHelper::_('bootstrap.renderModal', ...); that helper still exists in 6.2, but the Dialog is what the Developer Manual documents for new work. The Developer Manual page for the Dialog is written for the current 6.x documentation and does not state when the feature was introduced; the script is already present in the 5.4 source tree, so treat it as available in the 5.x series and standard in 6.x.

The popup types, from the manual and the source (popupType):

Type Content

inline

Text or HTML (popupContent), or the content of an element or <template> selected by src

iframe

A URL in an <iframe>, the type for tmpl=component views

ajax

HTML fetched with fetch(), passed through Joomla.sanitizeHtml() before insertion

image

An image lightbox

The iframe type sets a default size of 100% by 720 pixels, and the width to 1024 pixels with a maximum of 100% when you give none, so it is usable without extra CSS. The width and height options take any CSS value.

The declarative form: data-joomla-dialog

Loading joomla.dialog-autocreate registers one click listener on the document. Any element with the attribute data-joomla-dialog becomes a trigger. The attribute may be empty or contain JSON with the options. For an anchor without a popupType, the library uses iframe when the link is an ordinary URL and inline when it is a fragment (#id), and takes the href as src. Further data- attributes control behaviour:

Attribute Effect

data-joomla-dialog-cache

Keeps the dialog instance and shows the same one on later clicks

data-reload-on-close

Reloads the page after the dialog closes

data-close-on-message

Closes the dialog when the framed window sends a postMessage()

data-checkin-url

Sends a POST to this URL after closing (used to check in an item that was opened for editing)

For safety, popupTemplate is deleted from a configuration parsed from the attribute, so markup cannot be injected through it. The Tallpine trail list uses this for a quick-look window of the conditions report, without leaving the list:

<?php
// templates/cassiopeia_tallpine/html/com_tallpine/trails/default_item.php (override of the component layout)

use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;

$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->useScript('joomla.dialog-autocreate');

$quickUrl = Route::_('index.php?option=com_tallpine&view=trail&id=' . (int) $this->item->id . '&tmpl=component');

$config = [
    'popupType'  => 'iframe',
    'src'        => $quickUrl,
    'textHeader' => $this->item->title,
    'width'      => '90vw',
    'height'     => '80vh',
];
?>
<button type="button" class="btn btn-outline-primary"
        data-joomla-dialog="<?php echo htmlspecialchars(json_encode($config), ENT_QUOTES, 'UTF-8'); ?>"
        data-joomla-dialog-cache="true">
    Quick look
</button>

The framed page is the component.php output of the trail view, so it contains the trail and nothing else. If the dialog must close itself after an action in the frame (for example "Report this closure" and a submit), the frame sends a message and the trigger carries data-close-on-message; the library compares the message source with the frame window before it closes the dialog:

// In the framed page (media/templates/site/cassiopeia_tallpine/js/closure-report.js)
document.getElementById('closure-report-form').addEventListener('submit', () => {
  // The parent origin is the same site; never post to '*' with sensitive data
  window.parent.postMessage({ type: 'tallpine:closure-reported' }, window.location.origin);
});

The programmatic form

The same class can be imported by a module script, for dialogs that are built from data:

import JoomlaDialog from 'joomla.dialog';

// A trail map in a frame, with a custom header
const dialog = new JoomlaDialog({
  popupType: 'iframe',
  src: 'index.php?option=com_tallpine&view=map&tmpl=component',
  textHeader: 'Trail map',
  width: '90vw',
  height: '80vh',
});
dialog.addEventListener('joomla-dialog:close', () => dialog.destroy());
dialog.show();

// Static helpers return promises
JoomlaDialog.confirm('Report this closure to the park authority?', 'Closure').then((yes) => {
  if (yes) {
    // send the report
  }
});

The manual documents the events joomla-dialog:open, joomla-dialog:close and joomla-dialog:load, the instance methods show(), close() and destroy(), the accessors getBody(), getBodyContent(), getHeader() and getFooter(), and the static JoomlaDialog.alert() and JoomlaDialog.confirm() that return promises. The properties (popupType, src, popupContent, popupButtons, cancelable, width, height, textHeader, iconHeader, textClose, className, data) are set in the constructor or before the dialog is rendered.

Pitfalls and security

  • Wrong asset. useScript('joomla.dialog') loads the class; only joomla.dialog-autocreate makes data-joomla-dialog work. Add the one you need.

  • Escaping. The JSON in the attribute must be HTML-escaped with ENT_QUOTES, as in the example; unescaped quotes end the attribute early and the click does nothing.

  • Ajax content. The ajax type sanitises what it inserts; the inline type with popupContent does not, so never put unfiltered user content in it. Use a <template> element or escaped text.

  • postMessage. Always pass an explicit target origin (the same origin here) and check event.origin on receipt. The autocreate listener closes the dialog on any message from the frame window when data-close-on-message is set, so do not use that attribute on a trigger that frames an untrusted site.

  • Accessibility. The element wraps the native <dialog> and sets a label from textHeader; supply textHeader or ariaLabel so a screen reader announces a name, and keep a close button (the library adds one when you provide no buttons). Keyboard and focus rules are in Web accessibility.

  • Printing from a dialog. The print view is a separate window, not a dialog; use target="_blank" as in the example.

Testing the three files

Run these checks after any change to a template file, in a local site (see Local development with Docker). They use curl with -I for the status line only.

# 404: expect HTTP 404 and the Tallpine page, not 200
curl -sI http://localhost:8080/index.php/no-such-trail | head -n 1

# 403: a Trail Warden-only article requested as a guest
curl -sI "http://localhost:8080/index.php?option=com_content&view=article&id=9" | head -n 1

# component view: the body must contain only the component output
curl -s "http://localhost:8080/index.php?option=com_content&view=article&id=1&tmpl=component&print=1" | grep -c "container-header"

# offline: switch on, expect HTTP 503, switch off again
docker compose exec joomla php cli/joomla.php site:down
curl -sI http://localhost:8080/ | head -n 1
docker compose exec joomla php cli/joomla.php site:up

Expected results: HTTP/1.1 404 Not Found, a 403 or a login redirect depending on the article’s access level and the 403 handling of the component, a count of 0 for the header container in the component view, and HTTP/1.1 503 Service Temporarily Unavailable while offline. A 404 while the site is offline is redirected to index.php by the exception handler, so test the 404 with the site online.

For the exception paths, System Debug can be switched on for a test to see the backtrace branch; remember to switch it off. Strategies and tools for finding the cause of a real error are in Debugging, and the legacy error handling APIs that older extensions still use are in Error handling and legacy APIs.

When Joomla itself cannot answer

error.php and offline.php are produced by PHP, Joomla and (for most paths) the database. If PHP-FPM is down, the database refuses connections before the exception handler can render, or the web server cannot reach the application container, the visitor gets the web server’s page. In a reverse proxy setup, give the proxy a static page of its own, branded the same way:

# /etc/nginx/conf.d/tallpine.conf (proxy in front of the Joomla container)
server {
    listen 80;
    server_name www.example.org;

    location / {
        proxy_pass http://joomla:80;
        proxy_intercept_errors off;   # let Joomla render its own 403 and 404 pages
    }

    # Only upstream failures get the static page
    error_page 502 503 504 /maintenance.html;
    location = /maintenance.html {
        root /var/www/static;
        internal;
    }
}

With proxy_intercept_errors off (the default), the proxy passes Joomla’s own 4xx and 5xx responses through, including the 503 of offline.php; error_page then applies to failures where the proxy itself has no usable upstream response. Apache has the equivalent ErrorDocument 503 /maintenance.html. The details of a proxy deployment, forwarded headers and HTTPS are in Web server, reverse proxy and HTTPS, and a backup before maintenance is part of Backups and recovery.

Version notes and corrections

  • Child templates and entry files. The lookup of error.php, offline.php and component.php through the child, the parent and templates/system has been the same since child templates arrived in 4.1; a child that does not ship a file inherits the parent’s.

  • Error positions. The Developer Manual says that custom 403 and 404 pages through the error-403 and error-404 module positions exist since Joomla 5.0. Material written for Joomla 3 and 4, such as Joomla! 4 Masterclass (2023), predates the positions and therefore customises an error page by copying the whole template or by editing error.php in place. In a current site, prefer the position for text changes and a child error.php for structure.

  • Print and popup links. Joomla 3 and 4 tutorials add a print icon from the core icon layouts and open it with window.open. The 6.2 core icon layouts no longer contain a print icon, and new popups are opened with the Joomla Dialog. Code in books that predate the Dialog, such as Developing Extensions for Joomla! 5 (2023), that opens a Bootstrap modal still works, because the helper remains, but the core pickers have moved to the Dialog.

  • Legacy wiki links. Older books and tutorials link to docs.joomla.org pages for these files. The current documentation is the Developer Manual (manual.joomla.org) and the User Guide (guide.joomla.org); the Manual page on custom error pages is short and covers only the two positions, so this page also cites the 6.2 source.

  • Joomla 6.0 removals. None of the three files is removed or renamed in 6.0. Code inside them that uses the legacy J* class aliases (JFactory, JText, JHtml) or Joomla\CMS\Filesystem depends on the Behaviour - Backward Compatibility 6 plugin, which is off by default on new installs; write use statements for the namespaced classes as the examples do. See Upgrading from Joomla 5 to 6 and Deprecations and preparing for Joomla 7.

  • Joomla 5.4. The three files and the Dialog behave the same in 5.4, which receives security fixes only.

Summary checklist

  1. The child template ships error.php, offline.php and component.php only when it changes them; otherwise it inherits the parent’s.

  2. error.php guards modules with the identity and language check, never prints the exception message or the trace unless debug is on, and keeps the status code Joomla set.

  3. A Custom module in error-404 (and error-403) gives the text without PHP; list the positions in the manifest.

  4. offline.php keeps the login form with the user.login task, the CSRF token, and the extra login buttons.

  5. Authorised users get core.login.offline; site:down and site:up automate the switch.

  6. component.php keeps metas, styles, scripts, message and component.

  7. Popups use the Joomla Dialog with joomla.dialog-autocreate, the JSON attribute escaped.

  8. A static page in the proxy covers failures where Joomla cannot render.