cecil-theme
Create, structure and package themes for Cecil (visual themes and component themes), following the conventions of the official Cecilapp/theme-* repositories.
SKILL.mdskills/cecil-theme/SKILL.md
GitHubCecil Theme Builder
You are an expert Cecil theme developer. You create reusable themes for Cecil: full visual themes (layouts, styles, assets) and small component themes (a feature plugged into any site or theme), packaged for Composer like the official Cecilapp themes.
For general Cecil usage (content, configuration, CLI), see the cecil skill.
When to Use This Skill
Use this skill when:
- Creating a new Cecil theme from scratch, or turning a site's
layouts/into a reusable theme - Creating a component theme (search, PWA, dark mode toggle, icons, redirects, headers, analytics, etc.)
- Installing or integrating an official component theme (e.g.
flexsearch,pwa,darkmodetoggle) into a site or a visual theme; ask the user first (see "Ask the user before installing") - Adding theme options, translations or assets to a theme
- Packaging and publishing a theme with Composer (
cecil/theme-<name>) - Debugging theme resolution (layouts, assets, config or translations not picked up)
How Cecil Loads Themes
A theme is a website without pages, stored in themes/<name>/ and enabled in the site configuration:
theme: hyde
# or, for several themes (the first one overrides the others)
theme:
- pwa
- hyde
Resolution rules (from src/):
| Resource | Lookup order |
|---|---|
| Layouts | layouts/ → themes/<theme>/layouts/ (in theme list order) → Cecil internal resources/layouts/ |
| Assets | assets/ → themes/<theme>/assets/ → static/ → themes/<theme>/static/ (Sass import paths also include the theme's dirs) |
| Static files | Themes' static/ are copied first (reverse order), then site static/, so the site wins |
| Translations | Cecil internal → themes/<theme>/translations/ (in theme list order, so the last theme wins between themes) → site translations/ (site wins) |
| Data files | Site data/ and each themes/<theme>/data/ are loaded into site.data |
| Configuration | themes/<theme>/config.yml overrides Cecil defaults (including default pages, generators and output formats), site config always wins (first theme wins between themes) |
Consequences:
- The config file must be named
config.yml(notconfig.yaml); it's the only one loaded. - A theme is considered installed when
themes/<name>/layouts/orthemes/<name>/config.ymlexists; otherwise the build fails withTheme "<name>" not found. Did you forgot to install it?. - Any site file with the same relative path overrides the theme file (layout, partial, asset, translation key, static file).
- Theme dependencies are not resolved transitively: a
theme:key inside a theme'sconfig.ymlis ignored as soon as the site definestheme. Document the full list the user must declare (e.g.theme: [links, fontawesome]).
Theme Structure
Visual theme
theme-<name>/
├─ .editorconfig
├─ .gitattributes # * text=auto
├─ .gitignore # vendor/
├─ composer.json
├─ config.yml # default config + namespaced theme options
├─ theme.yml # optional metadata
├─ LICENSE
├─ README.md
├─ docs/
│ └─ screenshot.png
├─ assets/ # processed by asset(): css/, sass/, js/, images
├─ static/ # copied as-is: favicon.ico, fonts/
├─ data/ # optional data files (site.data.*)
├─ layouts/
│ ├─ _default/
│ │ ├─ page.html.twig # base layout (defines blocks)
│ │ ├─ list.html.twig # sections, homepage fallback, terms
│ │ ├─ home.html.twig # optional
│ │ ├─ term.html.twig # optional
│ │ └─ vocabulary.html.twig
│ ├─ <section>/ # optional per-section layouts (e.g. blog/page.html.twig)
│ ├─ partials/ # header, footer, nav, pagination, post-item...
│ │ └─ notes/ # optional Markdown notes templates (`:::<type>` → <type>.html.twig)
│ └─ macros/ # optional reusable macros
└─ translations/
└─ messages.fr.yml
Put layouts under _default/: this way a site can still provide its own top-level page.html.twig or list.html.twig, which take priority in the lookup.
Component theme
A component theme only ships what it needs, for example (structure of theme-flexsearch):
theme-<name>/
├─ .github/workflows/demo.yml # builds demo/ and deploys it to GitHub Pages
├─ .gitattributes # export-ignore for .*, /demo, /docs
├─ composer.json
├─ config.yml # new generated pages, output formats, options
├─ theme.yml
├─ README.md
├─ assets/<name>/ # namespaced assets (e.g. flexsearch/flexsearch.css)
├─ demo/ # demo website using the theme
│ ├─ config.yml
│ ├─ layouts/_default/page.html.twig
│ ├─ pages/
│ └─ link.php # links the repository as demo/themes/<name>
├─ docs/screenshot.png
├─ layouts/
│ ├─ _default/<layout>.<format>.twig
│ ├─ partials/<name>.html.twig # snippet to include in <head> or <body>
│ ├─ partials/<name>/*.html.twig # optional sub-partials (e.g. trigger + dialog)
│ └─ macros/<name>.twig # macros to import
└─ translations/messages.fr.yml
Put a component theme's assets in an assets/<name>/ subfolder so they don't collide with the site's or another theme's files.
Step-by-Step: Create a Theme
Before scaffolding, ask the user which features the theme needs and whether official component themes should provide some of them (search, dark mode, PWA, icons…), as described in "Ask the user before installing". In the same message, confirm the use of the built-in metatags partial (see Step 4).
Step 1: Scaffold a test site
curl -LO https://cecil.app/cecil.phar
php cecil.phar new:site -n --demo
rm -rf layouts # make sure the theme's layouts are used
mkdir -p themes/<name>
Develop the theme directly in themes/<name>/ and enable it in the site config.yml:
theme:
- <name>
Step 2: Write config.yml
Provide sensible Cecil defaults the theme relies on, plus the theme options under a namespace named after the theme:
# Cecil config required by the theme
taxonomies:
tags: tag
pages:
pagination:
max: 5
path: page
static:
exclude:
- '*.scss'
# Theme options
<name>:
sidebar:
sticky: true
color: '' # red, orange, green, blue...
reverse: false
Rules:
- Never rely on a key being set: users may override or remove it. In templates, always guard with
|default()or??(e.g.site.<name>.reverse|default(false)). - Cecil has no default vocabularies: declare
taxonomiesif the theme displays tags or categories. - Keep generic site keys (
title,baseurl,description…) out of the theme config.
Step 3: Write theme.yml (optional metadata)
name: <Name>
github: https://github.com/<vendor>/theme-<name>/
author:
- name: <Author>
url: <https://author.tld>
Step 4: Create the base layout
layouts/_default/page.html.twig defines the HTML skeleton and blocks that other layouts (and users) can override:
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{- include('partials/metatags.html.twig', {page, site}, with_context = false) }}
{%- block head_css %}
{{ html(asset(['css/base.css', 'css/<name>.css']), {title: '<Name>'}, {preload: true}) }}
{%- endblock head_css %}
{{- include('partials/pwa.html.twig', {site}, with_context = false, ignore_missing = true) }}
</head>
<body class="{{ site.<name>.reverse|default(false) ? 'reverse' }}">
{%- block header %}
{{ include('partials/site-header.html.twig') }}
{%- endblock header %}
<main>
{%- block content %}
<article>
<h1>{{ page.title }}</h1>
{%- if page.date %}
<time datetime="{{ page.date|date('c') }}">{{ page.date|format_date('long') }}</time>
{%- endif %}
{{ page.content }}
</article>
{%- endblock content %}
</main>
{%- block footer %}
{{ include('partials/site-footer.html.twig') }}
{%- endblock footer %}
{%- block scripts %}{% endblock scripts %}
</body>
</html>
Guidelines:
- Always include
partials/metatags.html.twigin the<head>of every HTML base layout, and never hand-write<title>, description, canonical, Open Graph or Twitter tags next to it (they would be duplicated). Since it replaces the theme's own<title>, confirm with the user first, explaining the benefit in one line, e.g.: "I'll use Cecil's built-in metatags partial: it generates the title, description, canonical, favicons, Open Graph/Twitter cards, feeds, hreflang alternates (and optional JSON-LD) from front matter and config, with no code to maintain. OK?" Only skip it if the user explicitly declines; in that case, document in the README which tags the theme outputs. - Reuse the other Cecil built-in partials instead of re-implementing them:
partials/paginator.html.twig,partials/navigation.html.twig,partials/breadcrumb.html.twig,partials/languages.html.twig,partials/theme-selector.html.twig. - Pre-wire optional component themes with
ignore_missing = trueso the theme works whether they are installed or not. - Pass an isolated context (
with_context = false) to partials that only needsite/page.
Step 5: Create child layouts
Extend with a list of templates so a site-level override wins over the theme default:
{# layouts/blog/page.html.twig #}
{% extends ['page.html.twig', '_default/page.html.twig'] %}
{% block content %}
<article class="post">
<h1>{{ page.title }}</h1>
{{ page.content }}
</article>
{{- include('partials/post-meta.html.twig') }}
{%- endblock content %}
List layout idiom (set the pages variable before extends):
{# layouts/_default/list.html.twig #}
{% set pages = site.pages.showable %}
{% if page.pages is defined %}
{% set pages = page.pages %}
{% endif %}
{% if page.pagination.pages is defined %}
{% set pages = page.pagination.pages %}
{% endif %}
{% extends ['page.html.twig', '_default/page.html.twig'] %}
{% block content %}
<ul class="posts">
{%- for post in pages %}
{%- include 'partials/post-item.html.twig' with {post} only %}
{%- endfor %}
</ul>
{{- include('partials/paginator.html.twig') }}
{%- endblock content %}
Section alias (e.g. reuse the blog list for the section fallback):
{% extends 'blog/list.html.twig' %}
Remember the lookup rules when naming layouts (see the cecil skill, "Lookup Rules"): <section>/page, <section>/list, section/<section>, taxonomy/<plural>, term, vocabulary, _default/*. For non-HTML outputs, the file name is <layout>.<format>.twig (e.g. list.rss.twig, page.webmanifest.twig).
Step 6: Assets
Put processed files in assets/ and reference them with asset():
{# bundle several CSS files into one #}
{{ html(asset(['css/poole.css', 'css/syntax.css', 'css/hyde.css'])) }}
{# compile Sass and inline it #}
<style>{{ asset('sass/<name>.scss')|inline }}</style>
{# remote asset, downloaded at build time #}
{{ html(asset('https://cdn.jsdelivr.net/npm/lib/dist/lib.min.js', {filename: 'lib.min.js', minify: false}), {defer: ''}) }}
{# responsive image #}
{{ asset(post.image)|html({alt: post.title}, {responsive: true, formats: ['webp']}) }}
{# optional asset #}
{% set logo = asset(site.logo|default('logo.svg'), {ignore_missing: true}) %}
{% if not logo.missing %}{{ logo|html({alt: site.title}) }}{% endif %}
{# small inline CSS/JS #}
<style>{% apply minify_css %}.hero { color: red; }{% endapply %}</style>
- Use
static/for files that must keep their name and not be processed:favicon.ico, web fonts,robots.txtoverrides, etc. Link them withurl('favicon.ico'). - Leave minification and fingerprinting to the site config (
assets.minify,assets.fingerprint) rather than forcing them in templates. - If the theme uses Sass partials in
assets/, exclude sources from the copy withstatic.exclude: ['*.scss']. - For Tailwind CSS, commit the compiled file (e.g.
assets/styles.css) and provide build scripts (see "Packaging").
Step 7: Menus, URLs and collections
<nav>
<ul>
{%- for entry in site.menus.main|sort_by_weight %}
<li{% if url(entry.url) == url(page) %} class="active"{% endif %}><a href="{{ url(entry.url) }}">{{ entry.name }}</a></li>
{%- endfor %}
</ul>
</nav>
- Always build links with
url()(pages, menu entries, paths, assets), never hardcodesite.baseurl. - Filter collections:
site.pages.showable|filter_by('section', 'blog')|sort_by_date|slice(0, 5). - Tags links:
url('tags/' ~ tag|slugify)or iteratesite.taxonomies.tags. - Get a specific page:
site.page('about').
Step 8: Translations
Wrap every UI string so the theme is translatable:
{% trans %}Read more{% endtrans %}
{% trans with {'%title%': site.title} %}%title% on GitHub{% endtrans %}
{{ 'You are offline'|trans }}
Provide at least one translation file, keyed by the source string:
# translations/messages.fr.yml
"Read more": "Lire la suite"
"%title% on GitHub": "%title% sur GitHub"
- File names follow
messages.<locale>.<ext>(yml,yaml,po/mo…), where<locale>matches the language locale or code (fr,fr_FR). - Extract strings with
php cecil.phar util:translations:extract --locale=fr --save --theme=<name>.
Component Themes Patterns
No automatic injection mechanism exists: a component theme plugs into a site using one or more of these patterns.
Generated page + output format (no template change needed), e.g. a
_redirectsfile:# config.yml pages: default: redirects: path: _redirects output: redirects multilingual: false output: formats: - name: redirects mediatype: text/plain extension: ""With the layout
layouts/_default/page.redirects.twig. Useful keys forpages.default.<id>:path,layout,output,uglyurl,multilingual,published,exclude.For a technical generated page (JSON index, manifest, script…), set
exclude: trueto keep it out of lists, andserviceworker: {precache: false}so thepwatheme does not precache it:pages: default: flexsearch: path: flexsearch layout: flexsearch # layouts/_default/flexsearch.json.twig output: json exclude: true serviceworker: precache: falseGenerated pages are multilingual by default (one file per language, e.g.
/flexsearch.jsonand/fr/flexsearch.json); setmultilingual: falsefor a single file.Partial to include, documented in the README and guarded by a feature toggle:
{# layouts/partials/<name>.html.twig #} {%- if site.<name>.enabled|default(false) %} <link rel="manifest" href="{{ url(site.page('manifest'), {canonical: true}) }}"> {%- endif %}Usage:
{{ include('partials/<name>.html.twig', {site}, with_context = false) }}.For an option accepting the
true|falseshorthand or anenabledkey (e.g.cache: falseiscache: {enabled: false}), use theenabledTwig test rather than testing the value:{%- if config.cache is enabled %}(custom options:site.<name>|default(false) is enabled).When a component needs code in several places, ship one partial per location (e.g.
partials/darkmodetoggle-head.html.twigin<head>+partials/darkmodetoggle.html.twigfor the button), and accept optional variables ({class: 'ml-2', label: 'Night mode'}).Make the component easy to restyle and to hook into:
- expose colors and sizes as CSS custom properties prefixed with the theme name (
--flexsearch-accent,--darkmodetoggle-size), with light and dark defaults - prefix every CSS class with the theme name (
.flexsearch-*,.darkmodetoggle) - offer
data-<name>-*attributes as JavaScript hooks (e.g.data-flexsearch-open), and a small global API or custom event when relevant - respect Cecil's color scheme conventions:
data-themeattribute and/ordarkclass on<html>, falling back toprefers-color-scheme - prefer vanilla JavaScript without dependencies; lazy-load heavy libraries and data on first use
- expose colors and sizes as CSS custom properties prefixed with the theme name (
Macros to import:
{# layouts/macros/icons.twig #} {%- macro svg(name, size = 24, class = '') -%} <svg width="{{ size }}" height="{{ size }}" class="{{ class }}">{{ site.data.icons[name]|raw }}</svg> {%- endmacro -%}Usage:
{% import 'macros/icons.twig' as icons %}{{ icons.svg('github', 16) }}.Section-specific layouts applied automatically when the site has a matching section (e.g.
layouts/episodes/list.rss.twigfor a podcast).Front matter blocks: a visual theme can render page blocks dynamically:
{% for block in page.blocks|default([]) %} {{ include('partials/blocks/' ~ block.name ~ '.html.twig', {block}, ignore_missing = true) }} {% endfor %}
Official Component Themes
Before writing a feature, check whether an official component theme already provides it. They are all installed with composer require cecil/theme-<name> (or by uncompressing the release archive in themes/<name>/), enabled in theme, then wired into layouts by including their partials.
| Theme | Feature |
|---|---|
flexsearch |
Client-side full-text search: index generated at build time, DocSearch-like modal |
pwa |
Web manifest, service worker, offline page (Progressive Web App) |
darkmodetoggle |
Icon button to switch between light and dark color schemes |
docsearch |
Algolia DocSearch |
fontawesome |
Font Awesome icons helpers |
octicons |
Twig macro for Primer Octicons icons |
podcast |
Templates to publish an audio show (RSS feed) |
redirects |
_redirects file generation |
headers |
_headers file generation |
netlify |
Netlify's _redirects and _headers |
netlifycms |
Netlify CMS support |
Ask the user before installing
Installing a component theme adds a Composer dependency, entries in theme and includes in layouts, so ask the user instead of deciding alone. Ask when:
- creating a visual theme or a site, and a feature could come from a component theme (search, dark mode, PWA, icons…): offer to install it, to pre-wire it with
ignore_missing = true(optional for the theme's users), or to skip it - the user requests a feature already covered by a component theme: offer to install it rather than re-implement it
- a feature has several options (e.g.
flexsearchvsdocsearch,darkmodetogglevs the built-intheme-selector): present the choice with the trade-offs
Suggested questions:
- "Should the theme use Cecil's built-in metatags partial for all SEO/social tags (recommended)?" (see Step 4)
- "Do you want a search box? FlexSearch (client-side, no service) or Algolia DocSearch?"
- "Do you want a light/dark mode toggle (
darkmodetoggle)?" - "Should the site work offline and be installable as an app (
pwa)?" - "Should these components be required by the theme, or only optional (included if installed)?"
Group the questions in a single message, recommend a default for each, and don't ask again for components already declared in theme or explicitly declined. Once answered, install (composer require cecil/theme-<name>), add the theme to theme, and include its partials and assets as documented below.
Combining themes
Several component themes can be combined, together with a visual theme:
theme:
- flexsearch
- darkmodetoggle
- pwa
- hyde
FlexSearch (flexsearch)
Adds a search box powered by FlexSearch. The index is a static JSON file generated at build time (/flexsearch.json, one per language), split into one record per <h2>/<h3> heading, with results grouped by section. Ctrl/⌘ + K opens the modal.
{# in <head> #}
{{ html(asset('flexsearch/flexsearch.css')) }}
{# where the button should be displayed (trigger + modal) #}
{{ include('partials/flexsearch.html.twig') }}
{# or separately: trigger in the header, dialog before </body> #}
{{ include('partials/flexsearch/trigger.html.twig') }}
{{ include('partials/flexsearch/dialog.html.twig') }}
Any element with a data-flexsearch-open attribute opens the modal. Main options:
flexsearch:
enabled: true
trigger: auto # auto (icon only on mobile), icon or full
hotkey: k # false to disable
sections: # indexed sections, in the order of result groups (all root sections by default)
docs:
limit: 5
blog:
title: Posts
limit: 3
split: false # one record per page instead of one per heading
date: true
Pages with exclude: true are not indexed. Styles are overridable with --flexsearch-* custom properties.
PWA (pwa)
Generates /manifest.webmanifest, /serviceworker.js and /offline.html. Include in <head>:
{{ include('partials/pwa.html.twig', {site}, with_context = false) }}
The service worker is disabled by default:
manifest:
background_color: '#FFFFFF'
theme_color: '#202020'
theme_color_dark: '#000000' # optional
shortcuts: true # main menu entries as app shortcuts
serviceworker:
enabled: true
install:
prompt: false
button: '#install-button' # custom install button (hidden by default)
precache:
pages:
limit: 10
update:
snackbar: true
offline:
snackbar: true
- Icons are generated from the site
assets/icon.pngifmanifest.iconsis not set. - Exclude a page from precaching with front matter
serviceworker: {precache: false}; other component themes should do the same for their technical generated pages. - Disabling the service worker afterwards unregisters it from visitors' browsers and clears their caches.
Dark mode toggle (darkmodetoggle)
Adds an icon button (moon/sun) that follows the system preference, remembers the user choice and avoids a flash on load.
{# in <head>, as early as possible #}
{{ include('partials/darkmodetoggle-head.html.twig', {site}, with_context = false) }}
{# where the button should be displayed (can be included several times) #}
{{ include('partials/darkmodetoggle.html.twig') }}
In dark mode, <html> gets class="dark" data-theme="dark" style="color-scheme: dark;", so it works with Tailwind CSS (dark: variant), Bootstrap/plain CSS ([data-theme="dark"]) and Pico CSS. Options:
darkmodetoggle:
storage_key: theme # localStorage key
class: dark # class added to <html> in dark mode (empty to disable)
style: true # minimal button style
JavaScript API: window.colorScheme.current(), .set('dark'), .toggle(), and the colorschemechange event. Any element with data-darkmodetoggle toggles the scheme.
Cecil's built-in
partials/theme-selector.html.twigis a basic toggle that only setsdata-theme(inline styles, no options, single instance per page). Preferdarkmodetogglefor a configurable, accessible button that also sets thedarkclass andcolor-scheme, and exposes a JavaScript API.
Pre-wiring component themes in a visual theme
A visual theme can include component partials with ignore_missing = true, so they are rendered only when the user installs the component theme:
<head>
{{- include('partials/darkmodetoggle-head.html.twig', {site}, with_context = false, ignore_missing = true) }}
{{- include('partials/pwa.html.twig', {site}, with_context = false, ignore_missing = true) }}
</head>
<header>
{{- include('partials/flexsearch.html.twig', ignore_missing = true) }}
{{- include('partials/darkmodetoggle.html.twig', ignore_missing = true) }}
</header>
Assets referenced by a component (e.g. flexsearch/flexsearch.css) must be guarded too, for example with asset('flexsearch/flexsearch.css', {ignore_missing: true}) and a missing check.
Packaging
composer.json
Themes are installed with Composer thanks to cecil/theme-installer, which copies the package to themes/<extra.name> (extra.name is required):
{
"name": "cecil/theme-<name>",
"type": "cecil-theme",
"description": "Cecil <Name> theme",
"keywords": ["Cecil", "theme", "<name>"],
"license": "MIT",
"require": {
"cecil/theme-installer": "^1.4||^2.0"
},
"extra": {
"name": "<name>"
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"allow-plugins": {
"cecil/theme-installer": true
}
}
}
Use
"description": "Cecil component theme <Name>"for a component theme.Add a branch alias for the development branch, e.g.
"extra": {"name": "<name>", "branch-alias": {"dev-main": "1.x-dev"}}.Declare dependent themes in
require(e.g."cecil/theme-fontawesome": "^1.5") and tell the user to add them totheme.Tailwind themes can add a builder:
"require-dev": { "aligny/tailwind-builder": "^1.0" }, "scripts": { "css:build": "tailwind-builder ./assets/css/tailwind.css --output=./assets/styles.css --minify", "css:watch": "tailwind-builder ./assets/css/tailwind.css --output=./assets/styles.css --watch" }
.editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.twig]
insert_final_newline = false
README.md
Follow this outline:
# <Name> theme
The _<Name>_ theme for [Cecil](https://cecil.app) is …

## Features
## Installation
```bash
composer require cecil/theme-<name>
```
> Or [download the latest archive](https://github.com/<vendor>/theme-<name>/releases/latest/) and uncompress its content in `themes/<name>`.
## Usage
Add `<name>` in the `theme` section of your `config.yml`:
```yaml
theme:
- <name>
```
### Configuration
(namespaced options with their default values)
### Internationalization
## License
_<Name>_ is a free software distributed under the terms of the MIT license.
For a component theme, also document the line to include in layouts (partial or macro import).
.gitattributes
Keep development files out of the release archive:
# Files to exclude when creating archive
.* export-ignore
/demo export-ignore
/docs export-ignore
# Auto detect text files and perform LF normalization
* text=auto
Demo site (recommended)
Recent official themes (e.g. flexsearch, darkmodetoggle) ship a demo/ website in the repository:
demo/config.ymlenables the theme (theme: [<name>]) and sets its options;demo/pages/and a minimaldemo/layouts/_default/page.html.twigshow the feature.demo/link.phplinks the repository asdemo/themes/<name>(symlink../.., ormklink /Jjunction on Windows), so the demo always uses the theme under development.demo/.gitignoreignores_site/,.cache/,.cecil/andthemes/.
Run it locally:
php demo/link.php
php cecil.phar serve demo
A .github/workflows/demo.yml workflow deploys it to GitHub Pages: set CECIL_BASEURL: https://<vendor>.github.io/theme-<name>/, download cecil.phar, run php demo/link.php, php cecil.phar build demo -vv, then upload demo/_site with actions/upload-pages-artifact and deploy with actions/deploy-pages. Link the demo from the README (**[Demo](https://<vendor>.github.io/theme-<name>/)**).
Conventions Checklist
-
config.yml(not.yaml) with theme options under a<name>:namespace - Layouts in
layouts/_default/, children extend['page.html.twig', '_default/page.html.twig'] -
partials/metatags.html.twigincluded in every HTML base layout<head>, with no duplicated<title>/meta tags (unless the user declined it) - Other built-in partials reused (
paginator,navigation,breadcrumb…) - All links built with
url(), all processed files withasset() - Every option guarded with
|default()or?? - Every UI string wrapped in
trans, at least onetranslations/messages.<locale>.yml - 2-space indentation in Twig/YAML/JSON/CSS, LF line endings
- Twig files do not end with a trailing newline
-
composer.jsonwith"type": "cecil-theme"andextra.name - README with screenshot, install, usage and configuration sections
- Component theme: assets in
assets/<name>/, CSS classes and custom properties prefixed with<name>, technical generated pages withexclude: trueandserviceworker.precache: false - Tested on a demo site (
demo/+link.php) withphp cecil.phar build -vandphp cecil.phar serve, without a sitelayouts/directory
Troubleshooting
Theme "<name>" not found: the folder must bethemes/<name>/(matchingextra.name) and containlayouts/orconfig.yml.- Theme layout ignored: a site
layouts/file with the same path (or a higher-priority lookup name) wins; check withphp cecil.phar build -vv, or remove the site layout. - Theme option ignored: the site config defines the same key; theme config never overrides site config. Lists of values are the exception: a list defined by the site (e.g.
output.pagetypeformats.homepage) is added to the theme list, so the values required by the theme are kept; the site can only remove one with a map of booleans (e.g.homepage: {atom: false}). - Dependent theme not loaded: list it explicitly in the site
themekey. - Asset not found: paths in
asset()are relative toassets/(orstatic/) of the site or theme, without the directory prefix. - Translations missing: check the file name locale matches
languages[].localeor code; the translations cache is refreshed when a file is updated or removed, but not when a new one is added: runphp cecil.phar cache:clear:translations. Deprecated configuration option(s) in …: the theme'sconfig.ymluses a renamed or removed option (seeconfig/deprecated.php), which makes the build fail for every site using the theme; apply the snippet given in the error message.- Run
php cecil.phar doctorto check theme configuration.
Useful Resources
- Official themes: https://github.com/orgs/Cecilapp/repositories?q=theme
- Themes showcase: https://cecil.app/themes/
- Templates documentation: https://cecil.app/documentation/templates/
- Configuration (
theme): https://cecil.app/documentation/configuration/site/#theme - Component themes: https://github.com/Cecilapp/theme-flexsearch, https://github.com/Cecilapp/theme-pwa, https://github.com/Cecilapp/theme-darkmodetoggle
- Minimal skeleton: https://github.com/Cecilapp/theme-example