4 Expert pathLesson 3 of 8 7 min read

Customize the theme templates

  1. 1. Éclair's architecture
  2. 2. Local development and testing
  3. 3. Customize the theme templates
  4. 4. Nginx, VPS, proxy and CDN: deploy Éclair
  5. 5. Éclair performance: staying at 100 out of 100
  6. 6. Migrate from WordPress or Wix
  7. 7. Advanced security and maintenance
  8. 8. Scaling Éclair in an agency
Contents
  1. Custom CSS or theme templates?
  2. The files in app/theme
  3. How does a template receive its data?
  4. CSP constraints
  5. Template performance
  6. Updates, integrity and manifest.php
  7. This academy's example
  8. Key points
  9. Going further
  10. Frequently asked questions

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.

CriterionCustom CSSTheme templates
WhereSettings › Appearance › Custom CSSfiles in app/theme/
Stored indata/custom-css.phpapp/, replaced on every update
After an updatekeptmust be reapplied
Integrity checknothing to report“modified” or “unknown” files
Limits64 KB, no < or @importfree HTML, within the CSP
Best forcolors, spacing, a block's styleHTML structure, new elements

The files in app/theme

FileRoleMain $v variables
layout.phpshared skeleton: <head>, header, menus, footerlang, head, css, content, current, id, class
single.phppage or postitem, html, toc, author, category, crumbs, related, translations, featured, words, h1_in_content
list.phpblog, category, author, search, catalogtitle, description, entries, crumbs, page, pages, prev, next, query, empty, chips
cards.php, products.phpgrids of post and product cardsentries, lang, level
product.phpproduct pageitem, entry, html, images, offers, crumbs, related
404.phppage not foundlang
theme.cssbase styles, inlined in every page/*dark*/ and /*shop*/ markers
themes/*.cssthe 5 optional themesadded after theme.css
directions.cssart direction snippetsonly 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”.

ToolUse 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:

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.

  1. Keep your modified templates in a Git repository, outside the live site.
  2. After each update, compare the original templates of the old and new versions, to spot a new variable or a fix.
  3. Carry those changes over to your files, test locally, then upload them again.
  4. 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).

FileRole
app/theme/academy.phpAcademy class: language of a URL, menu filtering, icons, paths and a lesson's position
app/theme/layout.phpHeader with a language switcher, full-screen mobile menu, footer with the four paths
app/theme/single.phpLesson header (badge, “Lesson 3 of 9”, line map), side table of contents, previous and next navigation
app/theme/list.php and 404.phpSearch 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)

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

Going further

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.

All the lessons of this path