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 anUnexpected 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>