CSS, JS and assets
There is only one rule for theme assets: CPalius does not compile theme assets. Tailwind, Sass, Vite, esbuild or plain CSS; use whichever you like. You put the output under Resources/dist/ and declare it in theme.json. The system serves only the compiled file.
theme/
src/ your own source (does not need to go into the package)
package.json your own build tools (optional, only on your machine)
Resources/
dist/
style.css ← compiled output
main.js
fonts/ img/ files the CSS calls with url()
Serving files: cp_theme_url
<link rel="stylesheet" href="{{ cp_theme_url('dist/style.css') }}">
<script{{ csp_nonce_attr() }} src="{{ cp_theme_url('dist/main.js') }}"></script>
cp_theme_url does the following:
- Validates the path. Only paths under
dist/orassets/with[A-Za-z0-9._/-]characters are accepted;..is rejected. Anything else returns empty. - Finds the file. First in the active theme, then in other installed themes. In a partial theme, if the file is not with you, it comes from the default theme.
- Copies. On first use it copies under
public/themes/<theme>/dist/...; if the source is newer it refreshes. After that the web server serves the file directly. - Adds a version. The address comes back with
?v=<modification time>: when the file changes, the browser cache is busted automatically.
So do not copy CSS/JS by hand under public/themes/; keep it in the theme folder (Resources/dist).
Fonts and images inside CSS
Fonts and images that CSS calls with url() do not pass through cp_theme_url. That is why, when a CSS file is copied, the url() dependencies inside it are copied at the same time. A .deps marker file reduces this to the cost of a single stat() per request, without scanning the file system on every request. The dependencies must exist in the theme folder; a url() that calls a path that does not exist becomes a broken image.
public/themes/<theme>/dist/ is an output folder (produced automatically, together with *.css.deps files). Do not put these under version control; the real file is under cp-content/themes/<theme>/Resources/dist/.
No external calls
Themes do not call fonts, icons and scripts from outside (CDN). The reasons:
- Privacy: a page that pulls a font from a third party tells that third party about every reader.
- CSP: external domains are blocked by the content security policy.
- Resilience: your site should not break when an external service goes down.
Put fonts and icon libraries in the theme's own dist/ folder or use the fonts the administrator installed from AACP (Menu, widgets and appearance).
CSP and scripts
- Inline scripts/styles must carry
csp_nonce_attr(). evaland inline event handlers (onclick="...") may be blocked; bind event listeners in the JS file.- If a third-party script is needed, the administrator must add it to the CSP settings (
security.csp_script_src); this should not be a theme decision.
The core panel and a theme are not the same thing
| Core panel (AACP/Studio) | Theme | |
|---|---|---|
| Build | Symfony AssetMapper + standalone Tailwind binary | Your tool, the system does not compile |
| Output location | public/assets |
public/themes/<theme> (automatic copy) |
| Classes | The Tailwind source scans core templates and module views | The theme's own CSS |
| Node.js | Not required | Your choice |
Modules' admin screens are drawn with the core's Tailwind build; they do not affect your theme CSS and must not clash with it. For front-end components, modules bring their own CSS files where possible; a theme may override them but must not break them.
Local development loop
1. change in src/
2. build with your own tools (npm run build, sass, …)
3. refresh the browser (cp_theme_url takes the version from the modification time)
Thanks to the versioned address you do not need to clear the cache by hand. When you change a template (Twig), in the development environment the Twig cache refreshes itself; in production cache:clear is needed.
Dark / light theme
Switch with CSS variables and the data-theme attribute:
:root, [data-theme="light"] { --bg: #fff; --fg: #1f2328; }
[data-theme="dark"] { --bg: #0d1117; --fg: #e6edf3; }
Also listen to the system preference (prefers-color-scheme) and write the choice to localStorage with try/catch (in a private tab or with cookies blocked localStorage throws). The Githeme theme applies this pattern with a single small githeme.js and offers temporary forcing with ?gh-theme=dark|light.
Generated files
If a CSS file of yours is generated from another CSS (like Githeme's githeme-recolor.css file), mark it clearly and keep the generator (tools/recolor.php) together with the theme; editing a generated file by hand is lost on the next build.