Getting started
Theme repository layout
A theme is a small git repo. Minimal layout:
<theme-repo>/
├── styles.src.css # Tailwind source (compiled to styles.css on deploy)
├── img/ # static assets bundled with the theme (logos, decorative SVGs)
└── templates/
├── base.html.twig # extends '_shared/base.html.twig' from cms-core
├── _partials/ # theme-private partials (header, footer, …)
└── pages/
├── default.html.twig # fallback when Page.template doesn't match
├── home.html.twig
├── treatment.html.twig
└── …
templates/pages/<name>.html.twig is what Page.template resolves to. A
page row stores the template name (e.g. home); the renderer looks for
@website/pages/<name>.html.twig and falls back to default.html.twig if
absent.
@website is a per-request Twig namespace that points at this theme's
templates/ directory on NFS. It's set by
cms-core/src/Website/WebsiteTemplateNamespaceConfigurator.php.
How rendering works
visitor request → cms-public PageController
→ Doctrine: load Page by website + locale + slug
→ Twig: render @website/pages/<page.template>.html.twig
→ response cached for 5 min at the CDN
The page and website variables are in scope in every page template —
they're Doctrine entities, but the Twig sandbox only exposes the
allowlisted accessors (see twig-helpers.md).
How the editor works
The admin SPA at cms-admin.internal opens a page for editing:
admin clicks "Edit" → cms-admin SPA
→ fetch /api/admin/pages/{id}/render (cms-api, JWT-auth)
→ cms-api renders the page in editor mode
(FieldExtension wraps every field() in
<div data-cms-field-name="..."> markers)
→ SPA stuffs HTML into a srcdoc <iframe>
→ SPA's editor JS reaches into iframe.contentDocument
and attaches inline editing for html/text fields
and a floating toolbar (with a Settings button)
for media/structured fields
cms-public is not iframed by the editor and never receives editor
JS. Theme templates get the same render path in both contexts; only the
optional data-cms-field-name markers differ.
Building Tailwind locally
# from the theme repo
~/path-to-cms-public/bin/tailwindcss -i styles.src.css -o styles.css --watch
The bin/tailwindcss standalone binary lives in cms-public/bin/,
installed by bin/install-tailwind.sh (Composer post-install hook).
Designers don't need to install Tailwind themselves.
Deploying a theme
A theme deploy is fully async:
- Push a git tag (e.g.
v0.4.2) to your theme repo, on our Gitea or on any other git host the website's theme repository points at (see operators → Theme). - In the admin SPA, open the website's settings → "Deploy theme version".
- cms-api dispatches
Gsid\CmsCore\Message\DeployThemeover Redis Streams. - cms-public-worker downloads the tagged tarball, extracts to
/data/cms/themes/<code>/<version>/, runsbin/tailwindcssagainststyles.src.cssto producestyles.css. - Activation is
Website.themeVersion = "<version>"— pointer flip, no copy. Old version dirs stay on NFS for instant rollback.
Version directories are immutable. To re-deploy after fixing a bug, push a new tag.
The Twig sandbox
Every theme template is parsed inside \Twig\Extension\SandboxExtension
in sandboxed = true mode. The allowlists are in
cms-core/src/Theme/ThemeSandboxPolicy.php:
- Tags:
apply autoescape block do extends for from guard if import include macro set spaceless with. Noembed, nouse, noverbatim. - Filters: standard formatting + escaping (see the file for the full
list). No
convert_encoding, noinline_css, nomarkdown_to_html. - Functions: see twig-helpers.md. No
source(),attribute(),template_from_string(),dump(),constant(). - Methods: explicit per-class list of
get*/is*/has*accessors. Setters and persistence calls are forbidden. - Properties: empty — go through getters.
Any tag, filter, function, or method not on the list throws a
SecurityError at render time. Adding a core capability requires a
cms-core release. Optional modules (cms-core) can extend the
allowlist via Gsid\CmsCore\Theme\SandboxContributorInterface: when a
website has the catalog module enabled, the catalog contributor adds
cms_products, cms_product, cms_categories,
cms_featured_products, cms_price, the cms_money filter, and
read-only getters on Product / Category / TaxClass — see
twig-helpers.md.