- Home
- Learning paths
- Expert path
- Customize the theme templates
Customize the theme templates
Contents
Contents
The theme templates decide the HTML of every public page: header, menus, post, lists, product page. Editing them opens up everything custom CSS can't do, at the cost of maintenance on every update. This lesson details the files in app/theme, the variables they receive, the security constraints and this academy's own example.
What you'll be able to do
Choose the right level
Custom CSS, the theme's stylesheet or a PHP template.
Edit a template
With the variables in $v and Éclair's functions.
Stay compatible
Strict CSP, updates and the integrity check.
Custom CSS or theme templates?
Always start with the lightest solution. Custom CSS covers the vast majority of tweaks, and <body> carries classes that let you target a context without touching PHP: theme-<name>, hdr-center, cards-list, then is-post, is-page, is-wide, is-list, is-shop, is-product, is-search or is-404 depending on the page.
| Criterion | Custom CSS | Theme templates |
|---|---|---|
| Where | Settings › Appearance › Custom CSS | files in app/theme/ |
| Stored in | data/custom-css.php | app/, replaced on every update |
| After an update | kept | must be reapplied |
| Integrity check | nothing to report | “modified” or “unknown” files |
| Limits | 64 KB, no < or @import | free HTML, within the CSP |
| Best for | colors, spacing, a block's style | HTML structure, new elements |
The files in app/theme
| File | Role | Main $v variables |
|---|---|---|
layout.php | shared skeleton: <head>, header, menus, footer | lang, head, css, content, current, id, class |
single.php | page or post | item, html, toc, author, category, crumbs, related, translations, featured, words, h1_in_content |
list.php | blog, category, author, search, catalog | title, description, entries, crumbs, page, pages, prev, next, query, empty, chips |
cards.php, products.php | grids of post and product cards | entries, lang, level |
product.php | product page | item, entry, html, images, offers, crumbs, related |
404.php | page not found | lang |
theme.css | base styles, inlined in every page | /*dark*/ and /*shop*/ markers |
themes/*.css | the 5 optional themes | added after theme.css |
directions.css | art direction snippets | only those of the chosen direction |
The CSS variables in theme.css (--bg, --bg-soft, --card, --text, --muted, --line, --radius, --shadow, --text-w, --wide-w) and those computed from the settings (--accent, --on-accent, --link, --font, --font-head) remain your best handle: use them in your templates to follow the theme, dark mode and the chosen color.
How does a template receive its data?
Theme::render($view, $v) first runs the view's template (single, list…), puts its HTML in $v['content'], then runs layout.php with the same array. The skeleton therefore sees all the page's variables: $v['item'] or $v['translations'] are available in a post's header. For a page in French, Theme::typography() finally adds the non-breaking spaces.
Theme::partial($name, $v) includes app/theme/<name>.php in an isolated function, where only $v is visible. This lets you split your theme into small files (Theme::partial('lang-switch', $v)), knowing that each new file will show up among the integrity check's “Unknown files”.
| Tool | Use in a template |
|---|---|
e($value) | escape any text or attribute, without exception |
path_to('/path') | internal URL, compatible with a subfolder |
asset('site/site.js') | URL of a static file with its version fingerprint |
Theme::t('key', $lang) | theme strings in six languages (read_in, toc, menu…) |
Menus::items('main') | menu links: label, href, path and external |
Media::get(), Media::picture() | media library image, as responsive WebP |
Content::path($entry) | public URL of a piece of content |
Example: show a post's translations in the header, based on the links created in the editor.
<?php // layout.php, inside <header> (add "use Eclair\Content;" at the top of the file) ?>
<?php if (!empty($v['translations'])): ?>
<nav class="lang-switch" aria-label="<?= e(Theme::t('read_in', $lang)) ?>">
<?php foreach ($v['translations'] as $code => $t): ?>
<a href="<?= e(path_to(Content::path($t))) ?>" hreflang="<?= e($code) ?>" lang="<?= e($code) ?>"<?= $t['id'] === ($v['id'] ?? '') ? ' aria-current="true"' : '' ?>><?= e(strtoupper($code)) ?></a>
<?php endforeach ?>
</nav>
<?php endif ?>
The style for .lang-switch goes in custom CSS or in theme.css, never in a style attribute.
CSP constraints
Public pages are served with a strict Content-Security-Policy, computed by Security::headers(). It applies to your templates like everything else:
- Scripts:
script-src 'self'. No inline<script>, noonclickattribute, nojavascript:link. A.jsfile served by your domain is still allowed. - Styles:
style-src 'self'plus thesha256hash of Éclair's CSS. The only accepted<style>tag is the one inlayout.php, filled byTheme::css(). A second<style>block or astyle=""attribute is blocked. - Fonts and resources:
default-src 'self'. A font loaded from a third-party service is refused: host it on your domain, as art directions do inmedia/fonts/. - Frames: only the
youtube-nocookie.comandplayer.vimeo.complayers are allowed.
Template performance
For a cached page, a template only runs on the first display; after that, the static cache serves the page. That's no reason to make it heavier. Read lists from the index (Content::index(), Content::published()) rather than opening each piece of content, keep Media::picture() for images (dimensions, WebP srcset, lazy loading) and prefer native HTML elements, <details> or the popover attribute, to JavaScript. Every KB added to theme.css is sent with every page, since the CSS is inlined in the HTML.
Also keep the Blocks::ea() calls in single.php (data-ef attributes): they only appear in the admin preview and let Visual mode edit the title, the featured image and “Key takeaways” in place.
Updates, integrity and manifest.php
The update procedure deletes app/, then replaces it: your theme templates disappear with it. Get organized from day one.
- Keep your modified templates in a Git repository, outside the live site.
- After each update, compare the original templates of the old and new versions, to spot a new variable or a fix.
- Carry those changes over to your files, test locally, then upload them again.
- Run Security › File integrity › Scan now: only your files should appear among the modified or unknown ones.
# What changed in the original templates between two versions
diff -ru eclair-1.7.0/eclair/app/theme eclair-1.8.0/eclair/app/theme
The integrity check compares each file with its fingerprint in app/manifest.php. A list that matches your changes is normal; an unexpected file deserves an investigation. For a fleet of sites, a sturdier option is to maintain your own copy of the repository and produce your archive with php tools/build.php: its manifest carries your templates' fingerprints, and the check stays clean. The public rendering tests in tests/run.php will then flag your changes. That's expected, since they compare against the original rendering.
This academy's example
Éclair Academy is built with Éclair 1.8 and customized theme templates. The standard theme shares the same menus across all languages; our templates add four behaviors while respecting the constraints described above (no inline script, no style attribute, rendering cached as usual).
| File | Role |
|---|---|
app/theme/academy.php | Academy class: language of a URL, menu filtering, icons, paths and a lesson's position |
app/theme/layout.php | Header with a language switcher, full-screen mobile menu, footer with the four paths |
app/theme/single.php | Lesson header (badge, “Lesson 3 of 9”, line map), side table of contents, previous and next navigation |
app/theme/list.php and 404.php | Search and page not found in the requested language |
assets/site/academy.js | “Copy” button on code blocks and reading marker in the table of contents (deferred loading) |
- Menus filtered by language: a single main menu holds the links for all three languages;
Academy::menu()only keeps those whose URL belongs to the page's language (French at the root,/en/…and/es/…). An emoji at the start of a label, as in “🧭 Paths”, becomes a line-drawn icon in the mobile menu. - Language switcher: it relies on the translations linked in the editor (
$v['translations']) and leads to the same page in the other language, or to that language's home page if the page isn't translated. - Full-screen mobile menu without JavaScript: a
popovertarget="site-menu"button opens<nav id="site-menu" popover>, which the browser handles on its own (Escape key, click outside). On large screens, the CSS puts the menu back in the header. - Automatic lesson navigation:
Academy::stations()reads the first numbered list on the path's page. The order of its links gives each lesson's number, the line map and the “Previous lesson” and “Next lesson” links. To reorder a path, you simply edit that list in the editor.
The core of the filtering takes just a few lines:
<?php
// app/theme/academy.php (excerpt)
public static function langOf(string $path): string
{
foreach (['en', 'es'] as $code) {
if ($path === '/' . $code || str_starts_with($path, '/' . $code . '/')) {
return $code;
}
}
return 'fr';
}
/** Menu items in the page's language; external links show everywhere. */
public static function menu(array $items, string $lang): array
{
$out = [];
foreach ($items as $item) {
$path = (string) ($item['path'] ?? '');
if ($path !== '' && self::langOf($path) !== $lang) {
continue;
}
[$icon, $label] = self::splitIcon((string) $item['label']);
$out[] = $item + ['icon' => $icon, 'text' => $label];
}
return $out;
}
To keep the Security page green, the academy's build script recalculates the fingerprint of these six files in app/manifest.php (the first 32 characters of their SHA-256, like the original manifest). After an Éclair update, you just copy these files again from the academy's archive, then clear the cache.
Key points
- Try custom CSS first: it's stored in
data/and survives updates. - The theme templates receive everything in
$v, andlayout.phpsees the same variables as the view. - Escape every value with
e()and build URLs withpath_to(). - The CSP forbids inline scripts and styles: CSS goes in the theme's stylesheets, behavior in native HTML.
- A modified template is replaced on every update: keep it under version control and compare it with each new version.
Going further
- Custom CSS: everything you can do without touching PHP.
- Build a multilingual site: the linked translations the language switcher relies on.
- Performance: staying at 100 out of 100: measure the effect of your templates.
- Advanced security and maintenance: the update procedure and the integrity check.
- MDN: the Popover API, for a menu without JavaScript.
- OWASP: Content Security Policy Cheat Sheet.
Frequently asked questions
Do my templates survive an Éclair update?
No: the update replaces the whole app/ folder, templates included. Keep your files in a Git repository and reapply them after each release, checking what changed in the originals.
Can I add my own JavaScript file?
The CSP accepts a file served by your domain, never an inline script. Choose its location carefully: assets/ is replaced on every update, and the integrity check flags anything in media/ that is neither an image nor a font. Try a native HTML solution first.
How do I add a translated string that's missing from Theme::t?
Theme::t() returns the key itself when it doesn't exist. Declare a small array of strings per language in your template, indexed by $lang.
Does Visual mode work with my templates?
Yes, if you keep the blocks' output ($v['html']) and the Blocks::ea() attributes in single.php. They stay invisible on the public site.