cecil · diff

git:20261022.0eb9b62 to git:20261023.da7f9ed

91 added, 521 removed. Audit B to B.

---
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
+ ## Reference Files
+
+ This file covers what most tasks need. Read the matching reference **before** working on a topic it covers:
+
+ | Read | Before |
+ |--------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
+ | [references/content.md](references/content.md) | Writing page content: Markdown syntax (links, images, embeds, notes, table of contents, excerpt), menus and taxonomies in front matter, permalinks (`pages.paths`) |
+ | [references/configuration.md](references/configuration.md) | Editing `cecil.yml`: menus, taxonomies, dates, environment variables and `.env`, default pages, data and static files, output formats, cache, server headers, optimization |
+ | [references/templates.md](references/templates.md) | Creating or editing a Twig template: lookup rules, built-in templates and partials, metatags, variables (page, site, taxonomies, pagination), functions, filters, components, fragments cache |
+ | [references/multilingual.md](references/multilingual.md) | Adding languages, translating pages, translating templates UI strings (`trans`, `translations/` files) or localizing dates |
+ | [references/assets.md](references/assets.md) | Handling assets in templates (`asset()`, `html()`, images, resize, responsive, CDN), Sass, code highlighting, or optimizing the build |
+ | [references/ai.md](references/ai.md) | Making the site AI-friendly: `llms.txt`, Markdown output of pages, HTTP headers, blocking AI crawlers (`robots.txt`) |
+ | [references/extending.md](references/extending.md) | Writing PHP extensions (custom generators, Twig extensions, post-processors) or using Cecil as a PHP library |
+ | [references/deployment.md](references/deployment.md) | Deploying the generated site (GitHub Pages, GitLab Pages, Netlify, Vercel, Cloudflare Pages, etc.) |
+ | [references/workflows.md](references/workflows.md) | Creating a blog, custom pages, dynamic content (Twig in a page body), shortcodes or a search page |
+
## 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)
+ ├── translations/ # UI strings translations (messages.<locale>.yaml)
+ ├── extensions/ # Custom PHP classes (generators, Twig extensions, post-processors)
+ ├── .cache/ # Build cache (generated, `cache.dir`)
+ └── _site/ # Generated site (`output.dir` or `--output`)
```
+ Add `.cache/` and `_site/` to the site `.gitignore`.
+
### 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`
+ - **assets/** - Files handled by Cecil (Sass compilation, minification, image handling), published **only when used** with `asset()` in a template (or as an image in Markdown): a file linked directly (`url()` or a raw path) must be in `static/`
+ - **static/** - Files copied to output without transformation (`static.mounts` copies specific files or directories of `static/` to a chosen output path)
+ - **data/** - Data files (YAML, JSON, XML, CSV) exposed in templates via `site.data` (`data/authors.yml` → `site.data.authors`)
+ - **translations/** - Translation files of the templates UI strings (`messages.<locale>.<ext>`, used by the `trans` tag/filter)
- **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
+ - **Pages**: Markdown files (`.md`, also `.markdown`, `.mdown`, `.mkdn`, `.mkd`, `.text`, `.txt`: `pages.ext`) composed of front matter and body (see [references/content.md](references/content.md) for the Markdown syntax)
- **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
+ - **Section**: Root folder in `pages/` (e.g. `pages/blog/post-1.md` -> section `blog`); its `index.md` (or `README.md`, handled as `index.md`) holds the section front matter
+ - **Homepage**: `pages/index.md`; it accepts `sortby` and `pagination` like a section, and `pagesfrom: blog` to list the pages of the `blog` section instead of all pages
+ - **File-based routing**: Files under `pages/` define generated paths, slugified (`pages/My Projects/Project A.md` → `/my-projects/project-a/`, rendered as `my-projects/project-a/index.html`); permalinks per section are set with `pages.paths` (see [references/content.md](references/content.md#permalinks))
+ - **Page ID**: the slugified path without extension and language (e.g. `blog/post-1`), used by `url()`, `site.page()`, `[link](page:blog/post-1)` and menus
- **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/`)
+ - Is a full _Section_ (same `type`, variables, and [layout](references/templates.md#lookup-rules-how-cecil-chooses-a-template) 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)) |
+ | `date`, `updated` | Creation date (default: file modification date) and update date (no default; `lastmod` is an alias) |
+ | `layout` | Template to use (see [Lookup Rules](references/templates.md#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 |
+ | `menu` | Add the page to a menu (e.g. `menu: main`, `menu: [main, footer]` or `menu: {main: {weight: 10}, footer: {name: "Short"}}`; entry name defaults to `title`) |
+ | `<vocabulary>` | Taxonomy terms, with the plural key declared in `taxonomies` (e.g. `tags: [PHP, Twig]` → `/tags/` and `/tags/php/` pages) |
+ | `redirect`, `alias` | Redirect the page to a URL; generate redirections from old paths to the page (e.g. `alias: [contact]`) |
+ | `external` | URL of a remote file used as the page body (e.g. a GitHub `README.md`) |
| `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 |
+ Every predefined variable can be overridden, except `section` (derived from the folder). Any other variable is a custom one, available in templates as `page.<variable>`.
+
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)
+ - `cascade` (variables propagated to the section pages, without overriding the variables they already set) and `circular` (circular prev/next navigation; with sub-sections, only the top-level section value applies)
### Configuration
- Configuration is defined in `cecil.yml` or `config.yml` at project root:
+ Configuration is defined in `cecil.yml` or `config.yml` at project root (read [references/configuration.md](references/configuration.md) for the options structure):
- - Core options are top-level keys such as `title`, `baseurl`, `description`, `taxonomies`, `menus`
+ - Core options are top-level keys such as `title`, `baseline`, `baseurl` (must end with `/`), `description`, `author`, `social`, `taxonomies`, `menus`, `date`, `theme`
- 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_*`)
+ - Defaults (including generators, default pages and output formats) are defined in `config/default.php`; priority is defaults < themes `config.yml` (the first theme wins) < site config (plus `--config` files) < env vars (`CECIL_*`, also read from a `.env` file) and CLI options (`--baseurl`, `--output`, `--drafts`…)
- 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`
+ - 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`. The built-in default pages (`index`, `404`, `robots`, `sitemap`, `xsl/*`) are declared there too: disable one with `pages: {default: {sitemap: {published: false}}}` rather than creating a duplicate
- 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).
+ Requirements: PHP 8.3+ with the `fileinfo`, `gd` and `mbstring` extensions (optional: `intl` to localize dates in a locale other than `en`, `curl` for `doctor:links` external checks, `imagick` for better image processing). If the CLI reports a PHP version or extension error, check `php -v` and `php --ini` (the CLI may use another `php.ini`).
+ If PHP is not available, use the standalone binary for the platform instead (`cecil-linux-x64`, `cecil-linux-arm64`, `cecil-macos-x64`, `cecil-macos-arm64`, `cecil-windows-x64.exe`), 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).
+
+ ```bash
+ curl -Lo cecil https://github.com/Cecilapp/Cecil/releases/latest/download/cecil-linux-x64 && chmod +x cecil
+ xattr -d com.apple.quarantine cecil # macOS only
+ ```
+
+ If Cecil is already installed globally (Homebrew `brew install cecilapp/tap/cecil`, Scoop, PHIVE or `composer global require cecil/cecil`), run `cecil <command>` instead of `php cecil.phar <command>` (check with `cecil --version`); it is updated with the same tool, not with `self-update`.
+
### Step 2: Create a New Site
- Use the `new:site` command to scaffold a new website:
+ Use the `new:site` command to scaffold a new website. It asks questions (title, baseline, base URL, etc.): in a non-interactive shell, add `-n` to use the defaults, then edit `cecil.yml`:
```bash
- php cecil.phar new:site
+ php cecil.phar new:site -n # `--demo` adds demo content, `--force` overrides an existing site
+ php cecil.phar new:site my-site -n # in the `my-site` directory
```
+ Starter kits are an alternative for a full site (`Cecilapp/the-butler` blog, `Cecilapp/statidocs` documentation, etc.): see https://cecil.app/starter-kits/.
+
### Step 3: Configure the Site
Edit **cecil.yml**:
```yaml
title: My Site
- baseurl: https://example.com/
+ baseline: A short tagline # shown after the title in the homepage <title>
+ baseurl: https://example.com/ # must end with /
description: My awesome static site
taxonomies:
categories: category
tags: tag
```
### Step 4: Create Content
- Create a page with:
+ Create a page with (`-n` and `--name` in a non-interactive shell):
```bash
- php cecil.phar new:page
+ php cecil.phar new:page --name=blog/my-post.md -n # `--prefix` adds a date prefix, `--slugify` slugifies the name
```
+ `new:page` uses a page model when it exists: `models/<section>.md` (e.g. `models/blog.md` for pages in `blog/`) or `models/default.md`, with the `%title%` and `%date%` placeholders. Pages can also be created directly as files in `pages/`.
+
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 new:site` | Create a new website (`-n` non-interactive, `--demo` to add demo content, `--force`) |
+ | `php cecil.phar new:page` | Create a new page (`--name`, `--prefix`, `--slugify`, `--force`, `--open`, `--editor`) |
+ | `php cecil.phar build` | Build the static site (`--drafts`, `--optimize` / `--no-optimize`, `--baseurl`, `--output`, `--config`, `--page`, `--dry-run`, `--clear-cache[=<regex>]`, `--metrics`, `--show-pages`, `--render-subset`, `--notify`) |
+ | `php cecil.phar serve` | Start local server on `http://localhost:8000` with live reload (`--drafts`, `--optimize`, `--config`, `--page`, `--clear-cache`, `--metrics`, `--open`/`-o`, `--host`, `--port`, `--no-watch`, `--timeout`, `--notify`, `--no-ignore-vcs`); no `--baseurl`, `--output` or `--dry-run` |
| `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 --background` | Run the server in the background, **without** watching changes (`serve:log -l 100` shows `.cecil/server.log` and `.cecil/errors.log`) |
| `php cecil.phar serve:stop` | Stop the local server |
- | `php cecil.phar edit` | Open pages directory with the configured editor |
+ | `php cecil.phar edit` | Open pages directory with the configured editor (`--editor` or `editor` config key) |
| `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 cache:clear` | Clear all cache files (or only `cache:clear:assets`, `cache:clear:templates` (`--fragments`), `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
- ```
+ | `php cecil.phar self-update` | Update Cecil to the latest version (Phar only; `--stable`, `--preview`, `--rollback`) |
- 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.
+ - Every command accepts the site directory as argument (e.g. `php cecil.phar build path/to/site`); `php cecil.phar help <command>` lists its options.
+ - `--config=config/dev.yml[,other.yml]` **adds** configuration files, merged over `cecil.yml` from left to right: it does not replace it.
+ - `build --metrics` shows the duration and memory of each step (to profile a slow build); `--clear-cache=<regex>` only clears the matching cache keys.
+ - `serve` is for local preview only: never use it to host the site, deploy the `_site/` output.
+ - `doctor:links` and `doctor:seo` return a failure exit code according to `--fail-on`: `bad` (default), `ok` (redirections, missing trailing slash), `feedback` or `never`. `doctor:links` also accepts `--page`, `--exclude`, `--format=json`, `--include-generated`, `--timeout`, `--concurrency`, `--no-cache` and `--feedback`; its defaults can be set under the `doctor.links` configuration key. External links checks need the `curl` PHP extension, and successful results are cached for 24 hours.
+ - `doctor:seo` runs a dry-run build and checks the generated pages; thresholds and checks are set under `doctor.seo` (e.g. `{title: {min: 30, max: 60}, description: {min: 120, max: 160}, content: {min_words: 300}}`, and `checks` booleans: `title`, `description`, `canonical`, `h1`, `og_tags`, `img_alt`, `content_length`, `lang_attribute`).
- In practice, you usually need only:
+ ## Templates
- - `layouts/page.html.twig`
- - `layouts/list.html.twig`
- - optional overrides per section
+ Twig templates live in `layouts/` and are named `layouts/(<section>/)<type>|<layout>.<format>(.<language>).twig` (e.g. `layouts/page.html.twig`, `layouts/blog/list.html.twig`). For each page, Cecil picks the first existing template in a lookup order, searching the site `layouts/`, then the theme, then the built-in templates. Read [references/templates.md](references/templates.md) for the full lookup rules and helpers.
### 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
+ Cecil embeds default templates in [`resources/layouts/`](https://github.com/Cecilapp/Cecil/tree/main/resources/layouts) (`_default/` layouts, `partials/`, feeds, sitemap, robots.txt, 404, redirects, JSON outputs, etc.). They are always available to Twig, so they can be rendered, included or extended **without being copied** into `layouts/`.
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
+ 5. **Always use `partials/metatags.html.twig`** in the `<head>` of HTML layouts (already included by `_default/page.html.twig`): it generates `<title>`, description, canonical, Open Graph, Twitter/X Card, favicons, feeds and `hreflang` links, etc. **Never add a separate `<title>`, `<meta name="description">`, canonical or `og:*` tags** next to it: they would be duplicated.
+ 6. **Make UI strings translatable** on multilingual sites: wrap them with `{% trans %}…{% endtrans %}` or `|trans` and provide `translations/messages.<locale>.yaml` files (see [references/multilingual.md](references/multilingual.md#ui-strings-translation)).
+ 7. **Build URLs with `url()`** (pages, page IDs, menu entries, assets), never by concatenating `site.baseurl`: `<a href="{{ url(page) }}">`, `{{ url('blog/post-1') }}`.
+ 8. **Escape text in attributes yourself**: Twig autoescape is **disabled** by default (`layouts.autoescape: false`), so `{{ page.content }}` needs no `|raw`, but a value printed in an attribute must be escaped (`title="{{ page.title|e('html_attr') }}"`).
## 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`
+ - **Custom extension ignored**: a Twig extension or post-processor class that can't be loaded only logs an error (`Unable to add Twig extension`, `Unable to load output post processor`), the build doesn't fail: check the `-v` output
+ - **Cache issues**: Clear cache with `php cecil.phar cache:clear`; a **new** translation file is not detected (`cache:clear:translations`), and remote assets are cached for 7 days (`cache.assets.remote.ttl`); while developing templates, `cache: {templates: false}` disables the templates cache
- **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
```
+
+ `debug: true` in the configuration (or `CECIL_DEBUG=true`) enables the Twig `dump()` / `d()` functions and the Sass source maps.