Cecil logo Cecil
What's on this page

Dynamic content

By default, the body of a page is static: Twig syntax written in a Markdown file is output as is.

You can create dynamic content in a page by using the template_from_string Twig function, which renders the page content as a Twig template.

{{ include(template_from_string(page.content, "dynamic content for page " ~ page.id)) }}

With this, you can use any page variable in the body of the page.

---
var: 'value'
---
The value of `var` is {{ page.var }}.

Enable dynamic content

The recommended way is to create a dedicated layout that overrides the content block, and to use it only for pages that need it.

{# 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 the layout variable in the front matter of the page:

---
title: "My dynamic page"
layout: dynamic
---

Available variables

The page content is rendered with the same context as the layout, so you can use:

  • page variables: page.title, page.date, page.<custom variable>, etc.
  • site variables: site.title, site.pages, site.data, etc.
  • all functions, filters and sorts available in templates;
  • Twig tags: {% set %}, {% if %}, {% for %}, {% include %}, etc.

How it works

The Markdown body is converted to HTML first, then the result (page.content) is rendered by Twig. This order has some consequences:

  • Markdown escapes the < and > characters, so comparison operators (>, <=) and arrow functions (=>) can’t be used in the body: the build fails with an Unexpected character "&" error.
  • A Twig expression used in an HTML attribute (e.g. href="{{ url(post) }}") is URL encoded by the Markdown converter and won’t be evaluated.
  • A Twig expression alone on its own line is wrapped in a <p> element.
  • Twig syntax written inside a code span or a code block is evaluated too.

To display Twig syntax as is, wrap it in a verbatim tag:

{% verbatim %}`{{ page.title }}`{% endverbatim %}

To avoid an extra <p> element around a block of HTML, wrap the expression in an HTML element, separated by line breaks:

<div>
{{ include('partials/latest-posts.html.twig') }}
</div>

Example: variables, data and partial template

This example shows a page displaying site and page variables, data from a data file, and the latest blog posts through a reusable partial template.

Data file:

# data/team.yml
- name: Alice
  role: Developer
- name: Bob
  role: Designer

Partial template, which receives section and limit as parameters:

{# layouts/partials/latest-posts.html.twig #}
{% set posts = site.pages.showable|filter_by('section', section|default('blog'))|sort_by_date|slice(0, limit|default(5)) %}
{% if posts|length > 0 %}
<ul class="latest-posts">
  {% for post in posts %}
  <li><a href="{{ url(post) }}">{{ post.title }}</a> <time datetime="{{ post.date|date('Y-m-d') }}">{{ post.date|format_date('long') }}</time></li>
  {% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}

Page:

---
title: About
layout: dynamic
---
Welcome to **{{ site.title }}**! This page was published on {{ page.date|format_date('long') }}.

Our team has {{ site.data.team|length }} members: {{ site.data.team|column('name')|join(', ') }}.

## Latest posts

<div>
{{ include('partials/latest-posts.html.twig', {section: 'blog', limit: 3}) }}
</div>

Output:

<p>Welcome to <strong>My site</strong>! This page was published on October 9, 2026.</p>
<p>Our team has 2 members: Alice, Bob.</p>
<h2 id="latest-posts">Latest posts</h2>
<div>
<ul class="latest-posts">
  <li><a href="/blog/post-3/">Post 3</a> <time datetime="2026-03-01">March 1, 2026</time></li>
  <li><a href="/blog/post-2/">Post 2</a> <time datetime="2026-02-01">February 1, 2026</time></li>
  <li><a href="/blog/post-1/">Post 1</a> <time datetime="2026-01-01">January 1, 2026</time></li>
</ul>
</div>

Example: shortcodes with macros

Twig macros can be used as shortcodes to insert rich HTML snippets in the body of a page.

Create the macros:

{# layouts/macros/shortcodes.html.twig #}
{% macro youtube(id, title = 'YouTube video') %}
<iframe src="https://www.youtube-nocookie.com/embed/{{ id }}" title="{{ title }}" width="560" height="315" loading="lazy" allowfullscreen></iframe>
{% endmacro %}

{% macro alert(message, type = 'info') %}
<div class="alert alert-{{ type }}" role="alert">{{ message }}</div>
{% endmacro %}

Create a layout that imports the macros before rendering the content:

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

{% block content %}
{% set imports = "{% import 'macros/shortcodes.html.twig' as sc %}" %}
{{ include(template_from_string(imports ~ page.content, "dynamic content for page " ~ page.id)) }}
{% endblock content %}

Use the shortcodes in a page:

---
title: "Demo video"
layout: shortcodes
---
<div>
  {{ sc.alert('This video is in English.', 'warning') }}
</div>

## Demo

<div>
{{ sc.youtube('NaB8JBfE7DY', 'Cecil demo') }}
</div>

Output:

<div>
  <div class="alert alert-warning" role="alert">This video is in English.</div>
</div>
<h2 id="demo">Demo</h2>
<div>
  <iframe src="https://www.youtube-nocookie.com/embed/NaB8JBfE7DY" title="Cecil demo" width="560" height="315" loading="lazy" allowfullscreen></iframe>
</div>