cecil · git:20261022.0eb9b62 · 2026-10-22 · sha256 8d216b5c3481ffb7

cecil git:20261022.0eb9b62B

Immutable. This exact content never changes and is served at /api/v1/blob/8d216b5c3481ffb7.

---
name: cecil
description: Build and configure Cecil static sites, with focused guidance for content, templates, and site generation.
license: EUPL-1.2
---

# Cecil Site Builder

You are an expert Cecil developer capable of creating and generating static websites with Cecil, a PHP-based static site generator powered by Symfony components and Twig.

## When to Use This Skill

Use this skill when:

- Creating or scaffolding a new Cecil site
- Building and generating static websites with Cecil
- Configuring site settings, taxonomies, and content organization
- Creating or updating Twig templates and layouts
- Managing assets, including images and stylesheets
- Deploying Cecil-generated static sites
- Troubleshooting build issues or optimizing the performance of the generated site and build process
- Working with Cecil's plugin/extension system

## Project Structure

### Directory Layout

```
my-site/
├── cecil.yml  # Main configuration file (or config.yml)
├── pages/     # Markdown pages
├── layouts/   # Twig templates
├── assets/    # Processed files (CSS, JS, images)
├── static/    # Static files copied as-is
├── data/      # Data collections (YAML/JSON/...)
└── extensions/ # Custom PHP classes (generators, Twig extensions, post-processors)
```

### Key Directories

- **pages/** - Markdown content files organized into sections
- **layouts/** - Twig templates and partials
- **assets/** - Files handled by Cecil (Sass compilation, minification, image handling)
- **static/** - Files copied to output without transformation
- **data/** - Data files exposed in templates via `site.data`
- **extensions/** - Custom PHP classes autoloaded by Cecil (file path must match the class namespace, e.g. `extensions/MyProject/Generator/CustomGenerator.php`)

## Cecil Fundamentals

### Architecture

Cecil follows a build pipeline:

```
Builder → Steps → Generators → Renderer → Output
```

- **Steps** (`Step/`): Sequential build phases, in this order
  - Load: pages, data files and static files
  - Pages Create / Convert: create pages collection, convert front matter and Markdown body
  - Taxonomies Create: build vocabularies and terms
  - Pages Generate: run generators (see below)
  - Menus Create: build navigation structures
  - StaticFiles Copy: copy static files
  - Pages Render / Save: render with Twig and write output files
  - Assets Save: save processed assets
  - Optimize: HTML, CSS, JS and images

- **Generators** (`Generator/`): Page generators executed via priority queue
  - Generators are ordered by numeric weight; lower numbers execute first (e.g., DefaultPages at weight 10 runs before Alias at weight 80).
  - DefaultPages (10) → ExternalBody (30) → Section (40) → Taxonomy (50) → Homepage (60) → Pagination (70) → Alias (80) → Redirect (90)

- **Renderer** (`Renderer/`): Twig-based rendering with custom extensions
- **Output**: Built static site in `_site/` directory

### Content Model

- **Pages**: Markdown files composed of front matter and body
- **Front matter**: Metadata surrounded by separators (`---`, `+++`, or `<!-- -->`)
- **Section**: Root folder in `pages/` (e.g. `pages/blog/post-1.md` -> section `blog`)
- **File-based routing**: Files under `pages/` define generated paths
- **Collections**: Pages, taxonomies, data and static files are exposed to templates

### Nested Sections (Sub-sections)

A nested folder that explicitly contains an `index.md` file becomes a _sub-section_ of its parent _Section_. A nested folder **without** an `index.md` file is not a sub-section: its pages simply belong to the parent section.

```plaintext
pages/
└─ blog                 # Section "blog"
   ├─ index.md
   ├─ post-1.md         # Page in "blog"
   └─ 2024              # Sub-section (contains an "index.md")
      ├─ index.md
      └─ post-2.md      # Page in "blog" AND "blog/2024"
```

A sub-section:

- Is a full _Section_ (same `type`, variables, and [layout](../../docs/templates/1-lookup-rules.md) resolution) available at its own URL (e.g. `/blog/2024/`)
- Falls back to the templates of its parent sections (e.g. `blog/list.html.twig` for `blog/2024` if `blog/2024/list.html.twig` doesn't exist)
- Can be nested at any depth (e.g. `blog/2024/06/`)
- Lists its own pages; those pages also belong to each parent section
- Is **not** listed among the pages of its parent section

Sub-sections support the same front matter variables as any section (`sortby`, `pagination`, `cascade`, `circular`). Use `cascade` on a parent `index.md` to propagate variables down to sub-sections and their pages.

In templates, use `page.parent`, `page.ancestors`, `page.sections` and `page.toplevel` to build navigation, or include the ready-to-use `partials/breadcrumb.html.twig` partial.

Previous/next navigation (`page.prev` / `page.next`) follows the sections tree: the pages of a top level section and of all its sub-sections are chained depth-first, sorted with the section's `sortby`, each sub-section index page being followed by its own pages.

### Front Matter

Front matter is YAML by default (`pages.frontmatter` also accepts `ini`, `toml` and `json`). Common variables:

| Variable                     | Purpose                                                                                                   |
|------------------------------|-----------------------------------------------------------------------------------------------------------|
| `title`, `description`       | Title (default: file name without prefix and language suffix) and description                            |
| `date`, `updated`            | Creation and update dates                                                                                 |
| `layout`                     | Template to use (see [Lookup Rules](#lookup-rules-how-cecil-chooses-a-template))                         |
| `slug`, `path`               | Override the URL (values must already be slugified)                                                       |
| `weight`                     | Sort order                                                                                                |
| `published` / `draft`        | `draft: true` is the same as `published: false` (preview with `build --drafts` / `serve --drafts`)        |
| `schedule`                   | `{publish: <date>, expiry: <date>}`: published if `publish` is reached **and** `expiry` is not passed     |
| `menu`                       | Add the page to a menu (e.g. `menu: main` or `menu: {main: {weight: 10}}`)                                |
| `redirect`, `alias`          | Redirect the page to a URL; generate redirections from old paths to the page                             |
| `output`                     | Output format(s) of the page (e.g. `[html, json]`)                                                        |
| `uglyurl`                    | `true` to output `page.html` instead of `page/index.html`                                                 |
| `excluded`                   | `true` to hide the page from lists, sitemap and feeds (formerly `exclude`)                                |
| `language`, `langref`        | Language code (default: file suffix) and translations reference                                          |

A file name prefix sets the `date` (`2019-04-23_My post.md`) or the `weight` (`1_First project.md`) of the page; separators are `-` and `_` by default (`pages.prefix.separator`).

A section (`<section>/index.md`) also accepts:

- `sortby`: `date` (default), `updated`, `title` or `weight`, or a map `{variable: date, desc_title: false, reverse: false}` (`desc_title` sorts pages with the same date by descending title); the site default is `pages.sortby`
- `pagination`: `{max: 5, path: page}` to override `pages.pagination`, `false` to disable it, or `{enabled: true}` to enable it when it is disabled globally (`pages.pagination: false`)
- `cascade` (variables propagated to the section pages) and `circular` (circular prev/next navigation)

### Configuration

Configuration is defined in `cecil.yml` or `config.yml` at project root:

- Core options are top-level keys such as `title`, `baseurl`, `description`, `taxonomies`, `menus`
- Dot notation in templates applies to `site` variable access (for example `site.title`)
- Defaults (including generators, default pages and output formats) are defined in `config/default.php`; priority is defaults < themes `config.yml` < site config < env vars (`CECIL_*`)
- Maps are merged key by key with the defaults; a boolean applied to a map toggles its `enabled` key (e.g. `cache: false`); in templates, check it with the `enabled` test (`{% if config.cache is enabled %}`)
- A list of values (`pages.ext`, `output.pagetypeformats.<type>`, `assets.images.responsive.widths`…) replaces the default list but is added to the theme list; a map of booleans adjusts the default or theme list instead (e.g. `homepage: {rss: true, atom: false}`)
- `output.formats` entries are merged by `name`: override a default format with only its `name` and the properties to change
- Deprecated options (renamed, moved or removed, listed in `config/deprecated.php`) make the build fail, with a message giving the source (site, theme or env vars) and the new YAML snippet to use
- Pages without content file (front matter only) are declared in `pages.default`, indexed by ID (`pages.virtual` is removed); like any page they are listed (`showable`, sitemap, homepage) unless they set `excluded: true`
- A page is "generated" (`page.generated`, formerly `page.virtual`) when it has no content file (section, term, page declared in configuration, etc.) or is a paginated page

## Building a Cecil Site

### Step 1: Download Cecil

Download Cecil using curl:

```bash
curl -LO https://cecil.app/cecil.phar
chmod +x cecil.phar
```

If PHP is not available, use the standalone binary for the platform instead (e.g.: `cecil-linux-x64`, `cecil-macos-arm64`, `cecil-windows-x64.exe` from the latest GitHub release), and replace `php cecil.phar` with `./cecil` in the commands below. The binary has no `self-update` command, only supports GD for image processing, and its `serve` command requires a local PHP executable (for the PHP built-in web server).

### Step 2: Create a New Site

Use the `new:site` command to scaffold a new website:

```bash
php cecil.phar new:site
```

### Step 3: Configure the Site

Edit **cecil.yml**:

```yaml
title: My Site
baseurl: https://example.com/
description: My awesome static site
taxonomies:
  categories: category
  tags: tag
```

### Step 4: Create Content

Create a page with:

```bash
php cecil.phar new:page
```

Then edit the generated file in `pages/`:

```markdown
---
title: My First Post
description: Welcome to my blog
date: 2024-05-14
tags: [Welcome, "First post"]
---
# My First Post

This is my first post content.
```

### Step 5: Create Templates (Optional)

Cecil ships with [built-in templates](#built-in-templates) (`resources/layouts/`), so a site builds **without any template** in `layouts/`. Only create templates to customize the rendering, and prefer extending the built-in ones (see [Built-in Templates](#built-in-templates)).

Create Twig templates in `layouts/` (for example `layouts/page.html.twig`):

```twig
<!DOCTYPE html>
<html lang="{{ site.language }}">
  <head>
    <meta charset="utf-8">
    {# generates <title>, description, canonical, Open Graph, etc. #}
    {{ include('partials/metatags.html.twig') }}
  </head>
  <body>
    <header>
      <h1>{{ site.title }}</h1>
    </header>
    <main>
      {{ page.content }}
    </main>
    <footer>
      <p>&copy; {{ site.title }}</p>
    </footer>
  </body>
</html>
```

### Step 6: Build the Site

```bash
php cecil.phar build
```

Output is generated in `_site/` directory.

## CLI Commands

| Command                                    | Purpose                                                                                                     |
|--------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| `php cecil.phar new:site`                  | Create a new website (`--demo` to add demo content)                                                         |
| `php cecil.phar new:page`                  | Create a new page                                                                                           |
| `php cecil.phar build`                     | Build the static site (`--drafts`, `--optimize`, `--baseurl`, `--output`, `--config`, `--page`, `--dry-run`, `--clear-cache`) |
| `php cecil.phar serve`                     | Start local server with live reload (same options as `build`, plus `--open`, `--host`, `--port`, `--no-watch`) |
| `php cecil.phar serve --incremental`       | Serve with incremental builds (rebuild only changed pages)                                                  |
| `php cecil.phar serve --background`        | Run the server in the background (`serve:log` to show its log)                                              |
| `php cecil.phar serve:stop`                | Stop the local server                                                                                       |
| `php cecil.phar edit`                      | Open pages directory with the configured editor                                                             |
| `php cecil.phar show:config`               | Display effective configuration (`--key=pages.dir` to show a single key)                                    |
| `php cecil.phar show:content`              | Display content tree (pages, data, static files)                                                            |
| `php cecil.phar doctor`                    | Diagnose site and environment (see also `doctor:frontmatter`, `doctor:seo`, `doctor:links`, `doctor:cache`) |
| `php cecil.phar cache:clear`               | Clear all cache files (or only `cache:clear:assets`, `cache:clear:templates`, `cache:clear:translations`)   |
| `php cecil.phar clear`                     | Remove generated files (or only `clear:output`, `clear:temporary` / `clear:tmp`)                            |
| `php cecil.phar util:templates:extract`    | Extract built-in templates into `layouts/` (Phar and standalone binary only)                                |
| `php cecil.phar util:translations:extract` | Extract translation strings from templates                                                                  |
| `php cecil.phar about`                     | Display Cecil version and environment information                                                           |
| `php cecil.phar self-update`               | Update Cecil to the latest version (Phar only)                                                              |

`doctor:links` and `doctor:seo` return a failure exit code according to `--fail-on`: `bad` (default), `ok`, `feedback` or `never`. `doctor:links` also accepts `--page`, `--exclude`, `--format=json` and `--include-generated`; its defaults can be set under the `doctor.links` configuration key.

## Template Development

Twig templates live in `layouts/` and follow Cecil naming conventions.

### Naming Convention

Use this pattern:

```plaintext
layouts/(<section>/)<type>|<layout>.<format>(.<language>).twig
```

Examples:

- `layouts/page.html.twig` - default page template
- `layouts/list.html.twig` - section/home listing template
- `layouts/blog/list.atom.twig` - Atom feed template for `blog` section (RSS is not output by default: enable it with `output.pagetypeformats.section: {rss: true}`)
- `layouts/page.html.fr.twig` - French page template
- `layouts/_default/page.html.twig` - fallback template

### Lookup Rules (How Cecil Chooses a Template)

Cecil uses the first existing template, in priority order, for each page type. `<layout>` is the front matter `layout` variable, and each entry resolves to `<name>.<format>.twig` (e.g. `blog/list.html.twig`):

| Page type  | Lookup order                                                                                                                                                                      |
|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Homepage   | `<layout>` → `index` → `home` → `list` → `_default/<layout>` → `_default/index` → `_default/home` → `_default/list` → `_default/page`                                             |
| Page       | `<section>/<layout>` → `<layout>` → `<section>/page` → `_default/<layout>` → `page` → `_default/page`                                                                             |
| Section    | `<layout>` → `<section>/index` → `<section>/list` → `section/<section>` → `<parent>/index` → `<parent>/list` → `section/<parent>` → `_default/section` → `list` → `_default/list` |
| Vocabulary | `taxonomy/<plural>` → `vocabulary` → `_default/vocabulary`                                                                                                                        |
| Term       | `taxonomy/<term>` → `taxonomy/<singular>` → `term` → `_default/term` → `_default/list`                                                                                            |

For a sub-section, `<section>` is its full path (e.g. `blog/2024`) and the `<parent>` entries are repeated for each parent section, nearest first.

To render a section with the templates of another section, map it with `layouts.sections` (applies to the section and its pages):

```yaml
layouts:
  sections:
    news: blog # "news" uses blog/list.html.twig and blog/page.html.twig
```

Each candidate is searched in `layouts/` (site), then in `themes/<theme>/layouts/`, then in Cecil's built-in templates (`resources/layouts/`). Most `_default/*` templates exist built-in, which is why a site renders without any custom layout.

In practice, you usually need only:

- `layouts/page.html.twig`
- `layouts/list.html.twig`
- optional overrides per section

### Built-in Templates

Cecil embeds default templates in [`resources/layouts/`](https://github.com/Cecilapp/Cecil/tree/main/resources/layouts). They are always available to Twig (lowest priority, after site and theme layouts), so they can be rendered, included or extended **without being copied** into `layouts/`.

- `_default/` - fallback layouts: `page.html.twig`, `list.html.twig`, `home.html.twig`, `vocabulary.html.twig`, `term.html.twig`, `404.html.twig`, `404.json.twig`, `redirect.html.twig`, feeds (`list.atom.twig`, `list.rss.twig`, `list.jsonfeed.twig`), embeds (`page.embed.twig`, `page.oembed.twig`), JSON/Markdown/LLMs outputs, `sitemap.xml.twig`, `robots.txt.twig`, etc.
- `partials/` - reusable fragments (see [Built-in Partials](#built-in-partials-and-utilities))
- `extended/` - base templates extended by other built-ins (`feed.twig`, extended by the Atom, RSS and JSON Feed templates)
- `components/` - none built-in, but site and theme [components](#components) live in `layouts/components/`
- `shortcodes.twig` - built-in shortcodes

Rules to follow:

1. **Don't recreate what already exists**: before writing a template, check whether a built-in one covers the need (feeds, sitemap, robots.txt, 404, redirects, JSON outputs are already provided).
2. **Extend rather than copy**: `_default/page.html.twig` exposes the blocks `head`, `head_metatags`, `head_css`, `header`, `content` and `footer`.

   ```twig
   {# layouts/page.html.twig #}
   {% extends '_default/page.html.twig' %}
   {% block content %}
     <article>{{ page.content }}</article>
   {% endblock %}
   ```

3. **Don't shadow a built-in template by accident**: a site file with the same path (e.g. `layouts/_default/page.html.twig` or `layouts/partials/metatags.html.twig`) fully replaces the built-in one for the whole site, and can't `extends` itself.
4. **Extract only as a last resort**: `php cecil.phar util:templates:extract` copies all built-in templates into `layouts/`; the copies then no longer receive Cecil updates.

### Metatags (`partials/metatags.html.twig`)

Always use the built-in `partials/metatags.html.twig` partial in the `<head>` of HTML layouts instead of hand-writing SEO/social tags. It is already included by `_default/page.html.twig` (block `head_metatags`).

```twig
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {{ include('partials/metatags.html.twig') }}
</head>
```

It generates:

- `<title>` (page title + divider + site title; site title + baseline on the homepage; page number on paginated lists)
- `description`, `keywords` (from `tags`), `author`, `robots` (`noindex` on paginated pages; pages with a `noindex` `metatags.robots` are also excluded from the sitemap)
- favicons (from `favicon.ico`, `favicon.svg`, `favicon.png` assets, resized)
- `prev`/`next`/`first`/`last` links, canonical and alternate formats, feeds, `hreflang` alternates
- `rel=me` links, Open Graph, Facebook, Twitter/X Card, Fediverse creator
- optional Dublin Core and JSON-LD structured data

Important:

- **Never add a separate `<title>`, `<meta name="description">`, canonical or `og:*` tags** next to this partial: they would be duplicated.
- Feed it through front matter (page) or configuration (site fallback): `title`, `description`, `tags`, `author`, `image`, `canonical.url`, `social.*`.
- Tune it with the `metatags` configuration (per page with front matter `metatags`):

  ```yaml
  metatags:
    title:
      divider: " &middot; "
      only: false        # page title only
    robots: "index,follow"
    image: true          # default social image (`false` to disable)
    favicon: true        # or `{enabled: true, image: favicon.png, sizes: {...}}`
    og: true
    twitter: true
    mastodon: true
    articles: "blog"     # site config only: section(s) rendered as Open Graph "article" and JSON-LD `BlogPosting`
    dc: false            # Dublin Core
    data: false          # JSON-LD structured data (`true` or a map of options)
  ```

- JSON-LD structured data (`metatags.data`) is a `@graph` of linked entities (`WebSite`, publisher, `WebPage`/`CollectionPage`, `BreadcrumbList`, `BlogPosting`, `VideoObject`, main menu `ItemList`). For a personal site, set the publisher as a `Person`; per page, set a specific type (`AboutPage`, `ContactPage`, etc.):

  ```yaml
  metatags:
    data:
      article: BlogPosting   # or NewsArticle
      publisher:
        type: Person         # Organization by default
        name: "Jane Doe"     # site title (or site author for a Person) by default
        logo: logo.png       # favicon image by default
  ```

  ```yaml
  # page front matter
  metatags:
    data:
      type: AboutPage
  ```

- Override `title` or `image` for a specific template:

  ```twig
  {{ include('partials/metatags.html.twig', {title: 'Custom title', image: og_image}) }}
  ```

- Customize one part with `embed` and its blocks (`title`, `description`, `metatags_favicon`, `metatags_alternates`, `metatags_og`, `metatags_twitter`, `metatags_dc`, `metatags_structured_data`), instead of copying the whole file:

  ```twig
  {% embed 'partials/metatags.html.twig' %}
    {% block metatags_twitter %}{% endblock %}
  {% endembed %}
  ```

- Run `php cecil.phar doctor:seo` to check the generated metatags.

See the [metatags documentation](https://cecil.app/documentation/configuration/site/#metatags) for all options.

### Template Variables

Most useful variables in Twig:

- `site.title`, `site.baseurl`, `site.description`
- `site.pages` - pages collection (current language)
- `site.allpages` - pages in all languages
- `site.taxonomies` - vocabularies and terms
- `site.menus.<name>` - menu entries (a `main` menu is created automatically with the homepage and the top-level sections, unless they define their own `menu` variable)
- `page.title`, `page.date`, `page.content`, `page.path`, `page.type`, `page.section`

### Multilingual Sites

Configure languages in `cecil.yml`:

```yaml
language: en
languages:
  - code: en
    name: English
    locale: en_US
  - code: fr
    name: Français
    locale: fr_FR
```

Use suffixed filenames for translations:

```plaintext
pages/about.md
pages/about.fr.md
```

You can render a language switcher in templates with:

```twig
{% include 'partials/languages.html.twig' %}
```

Useful collection helpers:

- `site.pages.showable` to skip draft/paginated/redirect/excluded pages (generated sections and taxonomies are included)
- `sort_by_weight` filter for menu entries

### Example Template

```twig
{# layouts/page.html.twig #}
<!DOCTYPE html>
<html lang="{{ site.language }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {# no <title> here: metatags.html.twig generates it #}
    {{ include('partials/metatags.html.twig') }}
  </head>
  <body>
    <header>
      <h1><a href="{{ url('/') }}">{{ site.title }}</a></h1>
      {% if site.menus.main is defined %}
      <nav>
        <ul>
        {% for entry in site.menus.main|sort_by_weight %}
          <li><a href="{{ url(entry.url) }}">{{ entry.name }}</a></li>
        {% endfor %}
        </ul>
      </nav>
      {% endif %}
    </header>
    <main>
      <article>
        <h2>{{ page.title }}</h2>
        {% if page.date %}
          <time datetime="{{ page.date|date('c') }}">{{ page.date|date('Y-m-d') }}</time>
        {% endif %}
        {{ page.content }}
      </article>
    </main>
  </body>
</html>
```

### Built-in Partials and Utilities

Available in every site, without extraction (include them rather than rewriting them):

- `partials/metatags.html.twig` - all `<head>` SEO/social tags, including `<title>` (see [Metatags](#metatags-partialsmetatagshtmltwig))
- `partials/alternates.html.twig` - canonical and alternate formats links (included by metatags)
- `partials/alternates-languages.html.twig` - `hreflang` links (included by metatags)
- `partials/feeds-from-section.html.twig` - section feeds links (included by metatags)
- `partials/jsonld.js.twig` - JSON-LD structured data (included by metatags when `metatags.data` is enabled)
- `partials/navigation.html.twig` - main menu navigation
- `partials/page-navigation.html.twig` - previous/next page links (follows the sections tree)
- `partials/paginator.html.twig` - pagination links
- `partials/languages.html.twig` - language switcher
- `partials/breadcrumb.html.twig` - breadcrumb (nested sections aware)
- `partials/terms-list.html.twig` - taxonomy terms list
- `partials/theme-selector.html.twig` - light/dark theme toggle
- `partials/googleanalytics.js.twig` - Google Analytics snippet
- `partials/data.json.twig` - page data serialized as JSON (used by JSON outputs)
- `partials/pico.css.twig`, `partials/highlight.css.twig` - CSS used by the default layouts
- `partials/notes/<type>.html.twig` - rendering of the Markdown `:::<type>` notes (`tabs`, `code-group`, `steps`, `accordion`, `cards`), rendered during conversion with a `note` variable only (no `page` or `site`); add one to render another note type. To nest notes, open the parent with a longer fence (`::::`, then `:::::`…): a note is closed by a line with at least as many `:` as its opening line

If a built-in template really needs to be modified, extract them all into `layouts/` (last resort, see [Built-in Templates](#built-in-templates)):

```bash
php cecil.phar util:templates:extract
```

### Pagination

Pagination is configured globally under `pages.pagination` (defaults: `max: 5`, `path: page`), and can be overridden in section front matter (see [Front Matter](#front-matter)).

```yaml
pages:
  pagination:
    max: 10
```

`pages.pagination: false` disables it globally; a section can then enable it again with `pagination: {enabled: true}`.

In templates, paginated lists expose `page.paginator` (`pages`, `count`, `current`, `links`).

In list templates, include paginator links with:

```twig
{% include 'partials/paginator.html.twig' %}
```

### Custom Filters and Functions

Cecil Twig helpers commonly used in templates (full list in the [templates reference](https://cecil.app/documentation/templates/reference/)):

- Functions: `url()` (internal/absolute URLs), `asset()` (reference and process assets), `html()` (HTML element of an asset: `<img>`, `<picture>`, `<link>`, `<script>`), `image()`, `css()`, `js()`, `integrity()`, `readtime()`
- Asset filters: `resize`, `webp`, `avif`, `lqip`, `dataurl`, `inline`, `minify`, `fingerprint`, `to_css`
- Content filters: `markdown_to_html`, `toc`, `excerpt`, `excerpt_html`, `slugify`, `highlight`
- Collection filters: `filter_by`, `sort_by_date`, `sort_by_title`, `sort_by_weight`
- Tests: `enabled` (config options), `external` (URL or menu entry outside the site), `asset`, `image_large`, `image_square`

```twig
{{ html(asset(page.image), {alt: page.title}, {responsive: true, formats: ['webp']}) }}
```

The `html` **filter** (`asset|html`) is deprecated: use the `html()` function.

### Components

Reusable Twig components are stored in `layouts/components/` (e.g. `layouts/components/button.twig`) and used with the `x` tag or an HTML-like syntax; the `slot` variable holds the inner content and `attributes` the passed attributes:

```twig
{% x:button with {class: 'primary'} %}Click me{% endx %}
<x-button class="primary">Click me</x-button>
```

## Build Optimization

### Asset Processing

Configure asset optimization:

```yaml
assets:
  minify: true
  fingerprint: true
  compile:
    style: compressed
  images:
    optimize: true
```

### Image Variants (Dark and Mobile)

Cecil can serve alternative versions of an image in a `<picture>` element, when files with a configured suffix exist next to the original image (e.g. `photo.dark.jpg`, `photo.mobile.jpg`, `photo.mobile.dark.jpg`):

```yaml
layouts:
  images:                          # images rendered with the `html` Twig function
    dark_suffix: .dark             # adds <source media="(prefers-color-scheme: dark)">
    mobile_suffix: .mobile         # adds <source> with the mobile media query
    mobile_media_query: "(max-width: 767px)"
pages:
  body:
    images:                        # same options for images in Markdown content
      dark_suffix: .dark
      mobile_suffix: .mobile
```

Both are disabled by default (`null`).

### Code Syntax Highlighting

Fenced code blocks are highlighted at build time (`pages.body.highlight: true` by default). The engine can be changed:

```yaml
pages:
  body:
    highlight:
      engine: tempest  # `highlightjs` (default, `hljs-*` CSS classes) or `tempest` (fewer languages, `hl-*` CSS classes)
```

The theme CSS must match the engine's classes (the built-in `partials/highlight.css.twig` handles both).

In templates, the `highlight` filter does not use this option: its engine is set by `layouts.highlight.engine` (`highlightjs` by default) or by its `engine` argument: `{{ code|highlight('php', 'tempest') }}`.

### Performance Tips

1. Use `draft: true` to exclude non-published content from builds
2. Enable output optimization in production (`optimize: true` or `build --optimize`); asset minification and fingerprinting are already enabled by default
3. Use output and format settings adapted to your pages types
4. Use responsive image options and image optimization when needed

## Extension & Plugins

### Custom Generators

Extend Cecil by creating custom generators:

```php
<?php

namespace MyProject\Generator;

use Cecil\Generator\AbstractGenerator;

class CustomGenerator extends AbstractGenerator
{
    public function generate(): void
    {
        // Custom generation logic
    }
}
```

Save it as `extensions/MyProject/Generator/CustomGenerator.php`, then register it in configuration with `pages.generators`.

```yaml
pages:
  generators:
    100: MyProject\Generator\CustomGenerator
```

> Note: use single backslashes in YAML. Double backslashes (`\\`) are only needed inside JSON or PHP strings.

### Twig Extensions and Post-processors

You can also extend Twig (via `layouts.extensions`) and post-process output (via `output.postprocessors`).

```yaml
layouts:
  extensions:
    MyExtension: MyProject\Twig\MyExtension
```

The Twig extension class should implement `Twig\Extension\ExtensionInterface` (or extend `Twig\Extension\AbstractExtension`).

```yaml
output:
  postprocessors:
    MyProcessor: MyProject\Renderer\PostProcessor\MyProcessor
```

Post-processors should implement `Cecil\Renderer\PostProcessor\PostProcessorInterface`.

## Deployment

### Static Site Hosting

Cecil generates pure static HTML, compatible with:

- GitHub Pages
- Netlify
- Vercel
- AWS S3
- Any web server

### Build & Deploy Workflow

```bash
# Check internal links (add --internal --external to also check external links); fails on broken links
php cecil.phar doctor:links

# Build
php cecil.phar build

# Deploy output directory (_site/)
# to your hosting platform
```

### GitHub Pages Example

```bash
php cecil.phar build
# Commit _site/ directory and push to gh-pages branch
```

## Code Quality Standards

When extending or contributing to Cecil:

- Follow PSR-12 coding standards
- Use `declare(strict_types=1);` in all PHP files
- Include proper PHPDoc blocks for all classes and methods
- Use 4-space indentation for PHP, 2-space for YAML/Twig

## Useful Resources

- **Official website**: https://cecil.app
- **GitHub Repository**: https://github.com/Cecilapp/Cecil
- **Issue Tracker**: https://github.com/Cecilapp/Cecil/issues
- **Documentation**: https://cecil.app/documentation/

## Common Workflows

### Create a Blog

1. Create `pages/blog/index.md` for blog section
2. Add individual posts in `pages/blog/post-*.md`
3. Configure taxonomy for tags/categories
4. Rely on built-in `_default/list.html.twig` and `_default/page.html.twig`, or extend them in `layouts/blog/`
5. Build with `php cecil.phar build`

### Add Custom Pages

1. Create markdown files in `pages/` directory
2. Add front matter with `title` and, if needed, `layout`
3. If needed, create a template in `layouts/` (preferably extending a built-in one, with `partials/metatags.html.twig` in `<head>`)
4. Let lookup rules pick the template, or set `layout: <name>` in front matter
5. Build to generate output

### Use Dynamic Content (Twig in Page Body)

By default, Twig syntax in a Markdown body is output as is. To evaluate it, create a dedicated layout and use it only for pages that need it:

```twig
{# layouts/dynamic.html.twig #}
{% extends '_default/page.html.twig' %}

{% block content %}
{{ include(template_from_string(page.content, "dynamic content for page " ~ page.id)) }}
{% endblock content %}
```

Then set `layout: dynamic` in the page front matter. The body can use `page.*`, `site.*`, functions, filters and Twig tags.

Caveats (Markdown is converted to HTML **before** Twig rendering):

- `<`, `>` and `=>` are escaped: comparisons and arrow functions fail (`Unexpected character "&"`)
- Twig expressions inside HTML attributes (e.g. `href="{{ url(post) }}"`) are URL-encoded and not evaluated
- An expression alone on its line is wrapped in `<p>`: wrap it in an HTML element separated by line breaks
- Twig in code spans/blocks **is** evaluated: use `{% verbatim %}` to display it as is

Keep the body simple and move complex logic into a partial template or a macro. See the [dynamic content documentation](https://cecil.app/documentation/content/dynamic-content/).

### Implement Search

1. Create `pages/search.md` with front matter `layout: search` and `output: json`
2. Use JavaScript library (e.g., Lunr.js) on frontend
3. Create `layouts/search.json.twig` that iterates `site.pages.showable` and emits a JSON array of `{title, url, content}` objects
4. Add search functionality to templates

## Troubleshooting

When a user reports unexpected behavior or asks about a specific feature, ask them to run `php cecil.phar doctor` and include the output. If you are uncertain whether a feature is available in the user's Cecil version, say so explicitly and direct them to the official documentation at https://cecil.app/documentation/ rather than guessing version ranges.

### Common Issues

- **Site not generating**: Check `cecil.yml` syntax and configuration
- **`Deprecated configuration option(s)`**: move or remove the listed options as shown in the error message
- **Missing pages**: Ensure content files are in `pages/` directory
- **Template not loading**: Verify the `layout` front matter variable, template naming and lookup rules
- **Build errors**: Run `php cecil.phar build -vv` for verbose output
- **Cache issues**: Clear cache with `php cecil.phar cache:clear`
- **Broken links or anchors**: Run `php cecil.phar doctor:links` (internal links) or `php cecil.phar doctor:links --internal --external`

### Debug Output

Get detailed build information:

```bash
php cecil.phar build -v    # Verbose
php cecil.phar build -vv   # Very verbose
php cecil.phar build -vvv  # Debug
```