Theme Anatomy
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.
| Directory | Holds | Required |
|---|---|---|
theme.php | The manifest array | Yes |
layouts/ | 4 shells: default, auth, checkout, invoice | default.* only |
views/ | 63 surfaces in 6 areas, plus 6 loose files | home.* only |
partials/ | 16 pieces the layouts assemble | No |
components/ | 33 shared blocks, plus home/ with 10 sections | No |
tables/ | 7 column presets, always .php | No; raw columns without it |
assets/ | css/, js/, images/, favicon, libraries | No |
locale/ | Author defaults, per language and scope | No |
content/ | Operator edits, written from the panel | No. Created on first save |
hooks.php | The theme's listeners, included at boot | No |
config.php | Saved setting values | No. 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.
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
| Key | Type | What it decides |
|---|---|---|
engine | string | smarty, twig or php. Sets the view extension and the sandbox; missing means php |
meta | array | The catalogue entry plus the behaviour flags core reads |
status | string | ready or development. Previewable but not activatable. Missing means ready |
update-url | string | Where the version check posts. Empty, or no meta.version, means never checked |
settings | array | The 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.
installed-version. Missing disables the check.
assets/.
provider wins.
website wins over providerUrl.
price and period label it.
license defaults to Open Source.
Behaviour Flags in meta
Core reads these to decide how it behaves.
settings.groups
Group key to a two key descriptor. Groups only sort the admin form.
$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.
value => label key. Also the allowlist: unknown values fall back to the default.
other field => required value. The row stays collapsed until every condition matches. A hidden field is still saved.
lang => value.
html => true every tag is stripped on save; with it the value is sanitized against allowed_tags.
Field Types and What They Save
| type | Control | Saved value |
|---|---|---|
switch, checkbox | Checkbox, description as label | Real boolean true / false |
color | Swatch plus a typable hex box | '#rrggbb'. 3-8 hex digits, else the default |
number | Number input | Integer, cast |
select | Dropdown built from options | The chosen key, or the default |
textarea | Textarea, or language tabs with multilang | String, or a lang => string map |
| anything else | Text input, or language tabs with multilang | String, or a lang => string map |
Reading the Manifest
// 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.
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],
],
],
],
];
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.
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.',
],
];
{* $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}
// 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
Generated on save and left alone by an upgrade. Hand edits survive only until the operator presses save.
Merged settings are built from the schema fields, not the saved file, so a stray key never reaches the templates.
A settings screen showing set_topbar_enabled means the key is missing from the locale file.
Both return the manifest's settings block. The processed form is allSettings().
The shipped value is a bare cover.png next to the manifest. A leading slash produces a broken card.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.