Skip to content

Theme contract

A theme is the package that draws the site's front end. A module decides what is shown (a blog post, a forum topic), the theme decides how it is shown. This split is deliberate: when you change the theme, modules and data do not change, only the look does.

Themes are completely separate from the core. A theme author can use Vite, esbuild or plain CSS; CPalius serves only the compiled output and does not compile theme assets.

Folder layout

cp-content/themes/example-theme/
  theme.json                    manifest (required)
  screenshot.png                store and theme list image
  Resources/
    views/
      layout.html.twig          front-end main template (required)
      partials/header.html.twig
      partials/footer.html.twig
      blog/ forum/ page/ ...    module pages (optional overrides)
    dist/                       compiled CSS and JS (declared in theme.json)
    translations/               theme-specific translations (optional)

The directory name must be kebab-case (example-theme, cpalius-website); otherwise the package is rejected.

theme.json

{
    "name": "Example Theme",
    "version": "1.0.0",
    "description": "Short description.",
    "author": "Me",
    "screenshot": "screenshot.png",
    "entry": {
        "css": ["dist/style.css"],
        "js":  ["dist/main.js"]
    },
    "supports": ["blog", "menu", "pages"],
    "menu_locations": {
        "main_header": "Main header",
        "footer": "Footer"
    }
}
Field Required Meaning
name yes Display name
version yes MAJOR.MINOR.PATCH; 0.0.0 or empty is invalid
description, author no Shown in the theme list
screenshot no Preview image (path relative to the theme folder)
entry.css, entry.js no The published CSS/JS files. Written relative to Resources/ and the files must exist
supports no The modules whose templates the theme brings (blog, forum, pages, menu, roadmap…)
menu_locations no The theme's menu locations (key → display name); see Menu, widgets and appearance

All fields except name are read as optional: a theme with an incomplete manifest does not break the list, while a broken theme.json drops that theme from the list (counts as empty) and does not affect other themes.

Package rules

A theme package (zip or directory) is checked by code before activation. It is rejected if any of the following holds:

  • theme.json is missing or is invalid JSON.
  • name or version is missing.
  • Resources/views/layout.html.twig is missing.
  • A CSS/JS file declared in entry is missing.
  • The package contains server-side code: .php, .phtml, .phar, .pht extensions or .htaccess, .user.ini, web.config files.
  • The package contains a cp-core/ or cp-includes/ path, or a zip entry has .. (path traversal) or an absolute path.
  • It declares the App\ namespace in its own file.

This means a theme cannot run code on the server: it carries only Twig, CSS, JS and images.

Why no PHP?

A theme is the presentation layer that draws the front end. A theme that could run server code would be a back door with access to every module and every administrator account. If you need logic, write a module.

Template resolution: @Theme

Modules call their templates not directly by the theme's name but through the shared @Theme namespace: @Theme/layout.html.twig, @Theme/blog/show.html.twig. The core sets up this namespace like this:

  1. The active theme's Resources/views/ folder is the first entry of the @Theme path.
  2. Other installed themes are added in order as fallbacks.

Result: if the active theme defines a template, it is used; if not, it comes from the fallback themes (usually the default theme). That is why you can write a theme partially:

githeme/Resources/views/
  layout.html.twig            ← overridden
  partials/header.html.twig   ← overridden
  (every other template comes from the default theme)

The Githeme theme overrides only layout, partials/header and the portal hero templates; it takes the rest from the default theme, so the default theme must stay installed.

Each theme also has its own namespace (@ExampleThemeTheme: the PascalCase form of the directory name + Theme), so a theme can explicitly call another theme's template.

Changing the theme needs the cache

Twig paths are set up at build time. When the active theme is changed the cache needs to be rebuilt; the AACP theme screen does this itself after saving. If you put files by hand, run cache:clear.

Default theme and fallback theme

The theme named cpalius-website is the fallback theme: it is used if the active theme setting (core.active_theme) is invalid. Whatever your own theme does, this theme must stay intact; do not delete it.

Theme-specific translations

Resources/translations/messages+intl-icu.<lang>.yaml adds the active theme's translations. These files are added at the very start of the translation list; that is, core and module translations override them and a theme can never change a core key. For theme-specific keys use a prefix like theme..

Getting started

Copying an existing theme folder and changing its name and theme.json content is the quickest way. Then:

  1. Template structure and helpers: Templates and Twig helpers
  2. Menu, widget area, colour and font: Menu, widgets and appearance
  3. CSS and JS: CSS build and assets
  4. Publishing: Publishing a theme
Last updated: 13.10.2026