System Pages

3 vues Markdown

Some pages have to appear while the application is broken, blocked or switched off. Exactly two of them can still come from your theme.

Overview

A missing page and a closed site are ordinary, and your design belongs on screen. A fatal error or a blocked address is not: the page builder may be what failed, so those carry their own shell.

Your theme owns 404 and maintenance. The second is unlike every other view you write: a complete document, not a page body.

Structure

SurfaceComes fromWhen it appears
Not foundYour theme, views/404Any unmatched address, and every failed view guard
MaintenanceYour theme, views/maintenanceEvery address in maintenance mode, unless an administrator is recognised
Maintenance fallbackCore, templates/system/maintenance.phpOnly when the active theme ships no maintenance view
Application errorCore, templates/system/application-error.phpA fatal error on a non AJAX request, answered with HTTP 500
Blocked addressCore, templates/system/blocked-ip.phpThe address blocker rejected the request before routing

The three core pages share one shell, so a style fix lands in all three. Each uses its helpers and nothing else.

templates/system/inc/shell.php Required by each page; returns the helper map below.
lang, dir For the root element. Never hardcode a language code.
e, l The escaper and the translator, which falls back to the string you pass.
head, mark, brand The head links, an inline icon by shape, and the product signature.
mask Replaces the administrator directory. Every printed string passes through it.

Step by Step

Build the Not Found Page

  1. Extend the default layout: this page comes through the normal request path.
  2. Offer the two escape routes behind their switches: knowledge base when enabled, support or contact form by ticket system.
  3. Never print the address that was not found. It is attacker supplied text.
  4. Every failed view guard lands here too — this is what a half installed theme shows.

Build the Maintenance Document

The one view that does not extend a layout. Turn the maintenance switch on, or you cannot see it.

  1. Open with your own doctype, root element and head, taking language and direction from the template variables.
  2. Copy your first paint guard in verbatim, final fallback value included.
  3. Print your skin tokens from the theme settings.
  4. Include only the logo, the message and the switcher — no navigation, cart or account menu.
  5. Keep the four injection points, so a module can still reach a closed site.

Touch the Core Pages Correctly

  1. Require the shared shell and use its helpers; never copy its style block.
  2. Take text from the locale files: the English strings in the PHP are only the translator's fallback.

Reference

The Shell Helpers

what require returns, and how each entry is called
$sys = require __DIR__ . DIRECTORY_SEPARATOR . 'inc' . DIRECTORY_SEPARATOR . 'shell.php';

// Values
$sys['lang'];                       // 'en', 'tr', ... from the language package
$sys['dir'];                        // 'ltr' or 'rtl'
$sys['app'];                        // the installation's base address

// Callables
$sys['e']($text);                   // htmlspecialchars, quotes included, UTF-8
$sys['l']($file, $key, $fallback);  // system/{file}/{key}, or $fallback when unavailable
$sys['head']();                     // local font link + the shared stylesheet
$sys['mark']($shape);               // inline icon: 'alert', 'shield' or 'tools'
$sys['brand']();                    // product signature + the version file
$sys['mask']($text);                // administrator directory replaced by a placeholder

// Typical use, with the per-page locale file bound once:
$e = $sys['e'];
$L = static fn (string $k, string $fallback): string => $sys['l']('error', $k, $fallback);

What the Error Page May Show

SituationMessageTechnical block
An engine error: a type or parse failureGeneric: the original text was written for a developerDevelopment only
An exception raised on purposeShown: the operator needs to read itDevelopment only
A fatal caught at shutdown, so no class is knownGeneric, as an engine errorDevelopment only
A source excerpt around the failing lineNot applicableDevelopment only; readable, unreadable or encoded, and too large handled
The request dataNot applicableDevelopment only, passwords and tokens redacted first

Theme Side Contract

the two calls the maintenance gate makes
// True when the manifest loaded AND the theme directory is really there.
public function exists(): bool;

// True when views/<view>.<ext> is a real file. The extension follows the manifest engine.
public function viewExists(string $view): bool;

// Rendering a system surface outside a request (a CLI health check, a probe):
// chose("website") would skip the theme, so call the theme directly.
public function render(string $view, array $data = []): string;
the maintenance gate The controller asks both, then falls back to the core template. Skip the second and the call returns an empty string: a blank page.
ui:client.maintenance.body Injected before the closing body tag once the page is built, so it reaches theme view and core fallback alike. Return the HTML to add; strings are joined.
views/404 Its own four: $page_title, $meta_robots, and the switches $support_enabled and $kbase_enabled that pick the escape routes. The full client data package is prepared here.

What the Maintenance View Receives

The whole list. The client data package is not prepared: menus, cart, announcements and account variables are absent; reading one shows nothing and logs a warning.

$ui_lang, $ui_dir The language code and ltr or rtl for your root element. Added by the template engine, so they survive a missing data package.
$light_logo_link, $dark_logo_link, $company_name The two logo variants and the operator's name. Print both and let your stylesheet pick: there is no server side detection.
$lang_list, $lang_count, $selected_lang_key Each entry carries key, name, link, selected and flag-img. Print the control only when the count is above one.
$current_year, $setting The year for a copyright line, and your theme's settings. $setting reaches every themed view, so a closed site keeps operator colours.

Example

views/maintenance.tpl, a complete document
<!DOCTYPE html>
{* NOT layouts/default.tpl: the site is closed, so no nav, cart or account chrome
   may link back into it. The language switcher is the only control, and it
   re-renders THIS page in the chosen language. *}
<html lang="{$ui_lang|default:'en'}" dir="{$ui_dir|default:'ltr'}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex, nofollow">
    <title>{lang key='system/maintenance/meta'}</title>

    {* Copy the guard VERBATIM from the main layout. Its final fallback value must
       match the theme script's, or the page paints in one mode and flips to the
       other one frame later. The bug only shows on a visitor with no stored value,
       which is why a developer's own browser never reproduces it. *}
    <script>
      var t = localStorage.getItem('wstyle-theme') || 'light';
      document.documentElement.setAttribute('data-bs-theme', t);
    </script>

    <link rel="stylesheet" href="{asset path='css/theme.css'}">
    <link rel="stylesheet" href="{asset path='css/maintenance.css'}">

    {* Operator colours still apply on a closed site. *}
    <style>:root { --brand: {$setting.primary_color}; }</style>

    {hook name='ui:client.head.css'}
    {hook name='ui:client.head.js'}
</head>
<body class="maintenance-page">
{hook name='ui:client.body.begin'}

<header>
    <a href="{link route='home'}"><span>{$company_name}</span></a>

    {* The ONLY control on the page. *}
    {if $lang_count > 1}
      {foreach $lang_list as $l}
        <a href="{$l.link}">{$l.name}</a>
      {/foreach}
    {/if}
</header>

<main id="maintenance-content">
    <p>{lang key='system/maintenance/text'}</p>
</main>

{hook name='ui:client.body.end'}
</body>
</html>
the controller that picks between theme and fallback
public function main(): void
{
    // Minimal on purpose: a closed page needs no menus, cart or announcements.
    $this->takeDatas(["language", "website_logos", "company_name", "lang_list"]);
    $this->addData("current_year", date("Y"));

    // BOTH checks are required. Without viewExists a theme that ships no
    // maintenance view falls through to the plain-PHP path, finds no file
    // and returns an empty string: a blank page with no error anywhere.
    $theme = \Theme::active();
    if ($theme->exists() && $theme->viewExists("maintenance"))
        $html = $this->view->chose("website")->render("maintenance", $this->data, true);
    else
        $html = $this->view->chose("system")->render("maintenance", $this->data, true);

    // Injected after rendering, so it reaches the theme view and the fallback alike.
    $injection = implode('', \Hook::run('ui:client.maintenance.body', $this->data));
    if ($injection) $html = preg_replace('/<\/body>/i', $injection . '</body>', $html, 1);

    echo $html;
}

Pitfalls

The first paint guard must match the theme script

Both read the same stored value and need the same final fallback. A mismatch paints one mode and flips a frame later, seen only by a visitor with no stored preference.

The administrator directory must not reach the markup

It reaches the request address, the dumped request data and a retry button's target, so a screenshot leaks it. Mask everything; the retry link stays empty.

No external request on a core system page

They appear when the server is unwell, so a content network buys nothing. Icons are inline, the typeface is local with a system fallback, and there is no framework.

Hiding the message and hiding the detail are different

An engine failure carries developer text, so a generic sentence replaces it. An exception raised on purpose is shown: the operator has to read it.

Nothing on a system page folds away

Every section arrives open: the reader solves the problem here. Centre with automatic margins; centring alignment crops the top edge once content overflows.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.