Theme Anatomy

6 Aufrufe Markdown

Every key a theme can declare in theme.php, and which directory holds what.

Overview

A theme is a directory and a manifest: a plain PHP array returned from theme.php.

theme.php is the schema, config.php the values. An upgrade replaces the first and never touches the second.

Structure

The Directories

Counts are from the shipped WStyle theme.

DirectoryHoldsRequired
theme.phpThe manifest arrayYes
layouts/4 shells: default, auth, checkout, invoicedefault.* only
views/63 surfaces in 6 areas, plus 6 loose fileshome.* only
partials/16 pieces the layouts assembleNo
components/33 shared blocks, plus home/ with 10 sectionsNo
tables/7 column presets, always .phpNo; raw columns without it
assets/css/, js/, images/, favicon, librariesNo
locale/Author defaults, per language and scopeNo
content/Operator edits, written from the panelNo. Created on first save
hooks.phpThe theme's listeners, included at bootNo
config.phpSaved setting valuesNo. Created on first save

tables/ holds plain PHP even in a Smarty or Twig theme: a preset is not a view. The list component includes it with a $table variable in scope. The file name is the preset the controller asked for.

Inside views/

Views are grouped by area. A controller asks for a path without prefix or extension; the engine adds both.

templates/website/WStyle/views
views/
├── account/     25   the client area: dashboard, services, invoices, tickets, settings
├── auth/         6   sign in, sign up, forgotten password, reset, activate, accept invite
├── checkout/    14   configure, cart, checkout, pay, order complete
├── content/     14   knowledge base, news, contact, legal
├── products/     3   catalog surfaces
├── page/         1   file based pages: views/page/{slug} answers /{slug}
├── home.tpl          the one view apply_theme insists on
├── 404.tpl           theme's own not-found surface
├── maintenance.tpl   rendered while the site is closed
└── domain.tpl        the public domain search

render("account/dashboard")  ->  views/account/dashboard.tpl   (smarty)
                             ->  views/account/dashboard.twig  (twig)
                             ->  views/account/dashboard.php   (php)

Reference

Top Level Keys

KeyTypeWhat it decides
enginestringsmarty, twig or php. Sets the view extension and the sandbox; missing means php
metaarrayThe catalogue entry plus the behaviour flags core reads
statusstringready or development. Previewable but not activatable. Missing means ready
update-urlstringWhere the version check posts. Empty, or no meta.version, means never checked
settingsarrayThe schema: groups and fields. The admin form is generated from it

The meta Block

Read by the theme list, the detail panel and the update check.

name Display name; the locale file wins.
version Installed version, sent as installed-version. Missing disables the check.
description One sentence for the catalogue card; the locale file wins.
image Card thumbnail, resolved relative to the theme directory, not assets/.
author, provider Two names for one field; provider wins.
website, providerUrl Author's address. Here website wins over providerUrl.
commercial, premium, price, period Either flag makes the card paid; price and period label it.
official, license, support, updates, features Detail panel rows; license defaults to Open Source.
docsUrl, supportUrl, demoUrl, purchaseUrl, learnMoreUrl Detail panel buttons, printed only when set.

Behaviour Flags in meta

Core reads these to decide how it behaves.

signup_minimal Boolean. Set it when the register view asks for the core fields only; requirement settings for the omitted fields are then not enforced.
dashboard_due_soon_alert Boolean. Off, the dashboard reminder strip carries the overdue invoice only. On, also the next due invoice.
disabled_routes Route keys the theme does not serve: those addresses answer 404 and leave the sitemap.

settings.groups

Group key to a two key descriptor. Groups only sort the admin form.

shape
$settings = [
    'groups' => [
        // group key      label = a key in the theme's locale file, NOT literal text
        'appearance' => ['label' => 'grp_appearance', 'icon' => 'bi-palette'],
        'checkout'   => ['label' => 'grp_checkout',   'icon' => 'bi-cart3'],
    ],
];

settings.fields

Setting key to a descriptor: the POST name, the config key and the name views read.

type One of the six below; anything else is a text field saving a string.
group Key of the group. An undeclared group still appears, ungrouped.
label, desc, placeholder Keys into the theme's own locale file, with an English fallback.
default Used until the operator saves one, and when a submission is invalid.
options Select only, as value => label key. Also the allowlist: unknown values fall back to the default.
depends other field => required value. The row stays collapsed until every condition matches. A hidden field is still saved.
multilang Text and textarea only. One tab per active language, saved as lang => value.
rows Textarea height, default 4.
html, allowed_tags Text and textarea only. Without html => true every tag is stripped on save; with it the value is sanitized against allowed_tags.
from_logo Color only, an integer index. Groups the field into the brand colour pair and adds the "pick from logo" action.

Field Types and What They Save

typeControlSaved value
switch, checkboxCheckbox, description as labelReal boolean true / false
colorSwatch plus a typable hex box'#rrggbb'. 3-8 hex digits, else the default
numberNumber inputInteger, cast
selectDropdown built from optionsThe chosen key, or the default
textareaTextarea, or language tabs with multilangString, or a lang => string map
anything elseText input, or language tabs with multilangString, or a lang => string map

Reading the Manifest

coremio/classes/Theme.php
// The whole manifest of ANY theme, by folder name. Cached per name, empty array when absent.
public static function manifest(string $themeName): array;

// The running theme. Falls back to Basic when the configured folder is gone.
public static function active(): self;

public function meta(): array;                    // manifest['meta']
public function engine(): string;                 // lowercased, 'php' when unset
public function settings(): array;                // manifest['settings']
public function settingsSchema(): array;          // the SAME array as settings()
public function exists(): bool;                   // manifest is non-empty AND the directory is there

// Values.
public function savedConfig(): array;             // config.php, cached per instance
public function setting(string $key): mixed;      // saved value, else the field's default, else null
public function allSettings(): array;             // every schema field, merged, as views receive it

public static function is_shipped(?string $name = null): bool;   // one of Basic, WStyle, WCOM
public static function engineLabel(string $engine): string;      // 'smarty' -> 'Smarty', for the panel

allSettings() is what views get as $setting: a multilang field as the active language's string, a colour field twice (primary_color plus primary_color_rgb).

Example

One Field, Declared

A promotional strip: a switch that gates a rich text field.

templates/website/Acme/theme.php
return [
    'meta' => [
        'name'        => 'Acme',
        'version'     => '1.0.0',
        'author'      => 'Acme Ltd',
        'website'     => 'https://acme.example',
        'image'       => 'cover.png',      // beside theme.php, NOT under assets/
        'description' => '',               // locale/en.php wins, so it is left empty here

        // Behaviour flags: read by core, not by the catalogue.
        'signup_minimal'           => false,
        'dashboard_due_soon_alert' => false,
        'disabled_routes'          => ['references', 'references_detail'],
    ],
    'update-url' => '',                    // empty: never checked for updates
    'engine'     => 'smarty',
    'status'     => 'ready',
    'settings'   => [
        'groups' => [
            'topbar' => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
        ],
        'fields' => [
            'topbar_enabled' => [
                'type'    => 'switch',
                'group'   => 'topbar',
                'label'   => 'set_topbar_enabled',      // a key in locale/{lang}.php
                'desc'    => 'set_topbar_enabled_desc',
                'default' => false,
            ],
            'topbar_text' => [
                'type'      => 'textarea',
                'group'     => 'topbar',
                'label'     => 'set_topbar_text',
                'default'   => '',
                'rows'      => 3,
                'multilang' => true,        // one tab per active language
                'html'      => true,        // otherwise every tag is stripped on save
                'depends'   => ['topbar_enabled' => true],
            ],
        ],
    ],
];
templates/website/Acme/locale/en.php
return [
    // The catalogue card reads these two from here, not from meta.
    'name'        => 'Acme',
    'description' => 'A compact storefront theme with a promotional strip.',

    'grp_topbar'              => 'Promotional Strip',
    'set_topbar_enabled'      => 'Show the strip',
    'set_topbar_enabled_desc' => 'Prints a single line above the header on every public page.',
    'set_topbar_text'         => 'Strip content',
];

The Same Field, Read Back

The panel writes the values; the theme reads them.

templates/website/Acme/config.php
return [
    'topbar_enabled' => true,
    'topbar_text'    => [
        'en' => '<strong>Launch week</strong> 20% off every plan.',
        'tr' => '<strong>Lansman haftası</strong> tüm planlarda %20 indirim.',
    ],
];
templates/website/Acme/partials/topbar.tpl
{* $setting.topbar_text is already the active language's string, not the map. *}
{if $setting.topbar_enabled}
    <div class="topbar">{$setting.topbar_text nofilter}</div>
{/if}
templates/website/Acme/hooks.php
// setting() returns the RAW stored value: a multilang field is still the lang => value map here.
$strip = Theme::active()->setting('topbar_text');
$lang  = Language::selected() ?: 'en';
$text  = is_array($strip) ? ($strip[$lang] ?? $strip['en'] ?? '') : (string) $strip;

// allSettings() is the resolved form, which is why the templates never do the above.
$resolved = Theme::active()->allSettings();
$text     = (string) ($resolved['topbar_text'] ?? '');

Pitfalls

config.php is not yours to write

Generated on save and left alone by an upgrade. Hand edits survive only until the operator presses save.

A value the schema does not declare never reaches a view

Merged settings are built from the schema fields, not the saved file, so a stray key never reaches the templates.

Labels are locale keys

A settings screen showing set_topbar_enabled means the key is missing from the locale file.

settings() and settingsSchema() are the same array

Both return the manifest's settings block. The processed form is allSettings().

meta.image is relative to the theme, not to assets/

The shipped value is a bare cover.png next to the manifest. A leading slash produces a broken card.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.