# Your First Theme

https://dev.wisecp.com/es/your-first-theme

Four files, one directory and one button: a theme the installation will actually serve.

## Overview

A theme is not registered anywhere. Create a directory under `templates/website`, put a manifest in it, and the panel finds it.

Two gates decide whether the operator can switch to it: the manifest, and a default layout plus a home view.

## Prerequisites

- Write access to `templates/website` and `temp`, where compiled templates land.
- An installation you may switch themes on: activation replaces its public site.
- [The Theme Engine](https://dev.wisecp.com/en/the-theme-engine) read once, for the manifest-versus-values split.
- `developer` turned on in `coremio/configuration/debug.php`, or a template error comes back as an empty page.

## Structure

### What the Gates Actually Require

The theme screen gates nothing: one card per directory, manifest or not. Everything is decided on Activate, and the first failing check is the message.

| Requirement | Checked when | If it is not met |
| --- | --- | --- |
| `theme.php` exists | Activating, and by the card | The card appears with no engine and an Unknown author; activation refuses it |
| `status` is not `'development'` | Activating, **first** | Refused with the development message; preview still works |
| `layouts/default.*` | Activating, after the status check | Refused with the incomplete package message |
| `views/home.*` | Activating, after the status check | Same: the two files are checked as a pair |
| `locale/{lang}.php` | Never | Optional; without it labels print as raw keys |

The `*` is the extension your `engine` chose. The examples below use Smarty.

## Step by Step

### 1. Create the Directory

1. Create `templates/website/Acme/`. The folder name is the theme's identity.
2. Create `layouts/`, `views/`, `locale/` and `assets/css/` in it.

Reload the theme screen. The card shows an empty folder: placeholder cover, folder name, Unknown author.

### 2. Write the Manifest

1. Create `templates/website/Acme/theme.php` returning the array below.
2. Set `engine` deliberately: it decides every view's extension. Leaving it out means plain PHP.
3. Leave `update-url` empty while you build: the theme is then never checked for updates.

Reload the screen. The card now carries the name, version, author and engine. Activate is refused: `status` is `'development'`, checked first.

### 3. Add the Page Shell

1. Create `layouts/default.tpl`: the document, and the blocks a view fills.
2. Declare five blocks: `title`, `head`, `content`, `scripts`, `body_end`. The shipped themes use these names.
3. Use `{asset}` for every file and `{lang}` for every string.

Half the gate is satisfied; activation stays refused until the second file.

### 4. Add the Home View

1. Create `views/home.tpl`, extending the layout and filling `content`.
2. Print one real value: `{$company_name}` is on every client page.

Both halves of the integrity gate are in place; only `status` is left.

### 5. Add the Theme's Own Wording

1. Create `locale/en.php`: a flat map of key to text.
2. Put `name` and `description` in it; the card reads these before the manifest.
3. Add a key for every string the home view prints, referenced with `{lang key='...'}`.

The card now shows your name and description.

### 6. Activate It

1. Change `status` to `'ready'` in `theme.php`. While it says `'development'` the button is refused.
2. Press Activate on the Acme card in `{admin}/settings/theme?group=theme`.
3. Open the site root in another tab.

The home page is your markup, stored as a single key in `coremio/configuration/theme.php`.

## Reference

### What a View Can Call

Both tag engines run in a sandbox with an empty class allowlist. Nine functions are the entire bridge: Smarty named, Twig positional.

| Function | Smarty | Twig | Returns |
| --- | --- | --- | --- |
| `link` | `{link route='x' p1='a' p2='b'}`
`{link page='pages/1'}` | `link('x', null, 'a', 'b')` | A client URL for a route key, or a stored page's target |
| `lang` | `{lang key='k' foo='bar'}` | `lang('k')` | The theme's translation of `k`. Extra Smarty parameters fill `{foo}`; **Twig takes the key** |
| `asset` | `{asset path='css/x.css'}` | `asset('css/x.css')` | URL under the theme's `assets/`, versioned for css and js |
| `config` | `{config key='favicon'}` | `config('favicon')` | One value from the **theme** configuration. A key with a slash returns an empty string |
| `money` | `{money amount=$v currency=$c}` | `money(v, c)` | The amount with its symbol; currency defaults to the visitor's |
| `hook` | `{hook name='ui:client.head.css'}` | `hook('ui:client.head.css')` | Every listener's string return |
| `captcha` | `{captcha area='a' tray='t' class='' force=false}` | `captcha('a', 't', '', false)` | The active provider's widget, or empty when captcha is off there |
| `csrf` | `{csrf form='key'}` | `csrf('key')` | The hidden token input for that form key |
| `content` | `{content var=$page.content}` | `content(page.content)` | Operator authored HTML, printed raw. Template syntax inside it is compiled |

The Smarty sandbox also permits twenty plain PHP functions. Twig has no PHP access at all: twelve tags and seventeen filters. Anything else raises a sandbox error.

```php
// Smarty: the only PHP functions a view may call
count  sizeof  nl2br  number_format  htmlspecialchars  strip_tags
strlen  mb_strlen  substr  mb_substr  str_contains  str_starts_with
str_ends_with  ucfirst  date  time  implode  explode  in_array  is_array

// Twig: the only tags
if  for  set  block  with  apply  autoescape  verbatim  extends  include  use  embed
```

Escaping is on by default: Smarty unless you append `nofilter`, Twig unless you pipe through `raw`.

## Example

The complete theme, four files.

```php
return [
    'meta' => [
        'name'    => 'Acme',
        'version' => '1.0.0',
        'author'  => 'Acme Ltd',
        'website' => 'https://acme.example',
        'image'   => 'cover.png',   // beside this file, not under assets/
    ],
    'update-url' => '',             // empty while you build: no version check at all
    'engine'     => 'smarty',       // decides the extension of EVERY view
    'status'     => 'development',  // preview only; set to 'ready' when it can be activated
    'settings'   => [
        'groups' => [],
        'fields' => [],
    ],
];
```

```smarty
<!doctype html>
{* $ui_lang and $ui_dir arrive on every render; writing them by hand breaks RTL packs. *}
<html lang="{$ui_lang}" dir="{$ui_dir}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{block name=title}{$page_title|default:$company_name}{/block}</title>

    <link rel="icon" href="{asset path='favicon.svg'}">
    <link rel="stylesheet" href="{asset path='css/default.css'}">

    {* Page specific CSS goes here. Page specific SCRIPTS do not: see below. *}
    {block name=head}{/block}
    {hook name='ui:client.head.css'}
</head>
<body>
    {hook name='ui:client.body.begin'}

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

    <main>{block name=content}{/block}</main>

    <footer>&copy; {$current_year} {$company_name}</footer>

    {* Core scripts first, then the page's own: defer executes in document order. *}
    <script src="{asset path='js/default.js'}" defer></script>
    {block name=scripts}{/block}

    {* Modals live here, outside <main>, so a fixed overlay is not trapped in its stacking context. *}
    {block name=body_end}{/block}
    {hook name='ui:client.body.end'}
</body>
</html>
```

```smarty
{extends file='layouts/default.tpl'}

{block name=title}{lang key='home_title'} | {$company_name}{/block}

{block name=head}
    <link rel="stylesheet" href="{asset path='css/home.css'}">
{/block}

{block name=content}
    <section class="hero">
        <h1>{lang key='home_headline' brand=$company_name}</h1>
        <p>{lang key='home_lead'}</p>
        <a class="btn" href="{link route='sign-in'}">{lang key='home_cta'}</a>
    </section>
{/block}
```

```php
return [
    // Read by the theme card in the panel, ahead of meta.name / meta.description.
    'name'        => 'Acme',
    'description' => 'A minimal starter theme.',

    // The home view's strings. {brand} is filled by the tag: {lang key='home_headline' brand=$company_name}
    'home_title'    => 'Home',
    'home_headline' => 'Everything {brand} runs, in one place.',
    'home_lead'     => 'Hosting, domains and licences from a single account.',
    'home_cta'      => 'Sign in',
];
```

Set `status` to `'ready'` when Activate should work. Until then the theme is previewable but not activatable.

## Pitfalls

> **A missing view is a blank page, not an error**
> 
> The platform catches everything a template engine throws and returns an empty string. A typo or an unclosed block produces an empty page. Turn `developer` on to see it.

> **Layouts and partials are referenced from the theme root**
> 
> `{extends file='layouts/default.tpl'}`, not a path relative to the view: the template directory is the theme directory.

> **The view has nine functions and no classes**
> 
> Every platform class is unreachable from a view. A theme that needs the whole language declares `engine => 'php'`.

> **Do not build the second surface by copying the first**
> 
> Markup that appears twice belongs in `components/`, shell pieces in `partials/`.

> **A view never queries anything**
> 
> Controllers prepare the data as named variables. When a surface needs more, add a listener in the theme's hooks file.

## Related Articles

- [The Theme Engine](https://dev.wisecp.com/en/the-theme-engine)
- [Theme Anatomy](https://dev.wisecp.com/en/theme-anatomy)
- [Theme Assets](https://dev.wisecp.com/en/theme-assets)
- [Template Variables](https://dev.wisecp.com/en/template-variables)
- [Translating a Theme](https://dev.wisecp.com/en/translating-a-theme)
