Templates – the basics
Page HTML, Head, SEO title, description and Snippets are templates. The syntax largely corresponds to Twig or Django (pongo2).
Output
{{ page.title }}
{{ post.titel|upper }}
{{ preis|floatformat:2 }} €
{{ beschreibung|default:"Keine Beschreibung" }}
Output is automatically HTML-escaped – < becomes <. This protects against broken markup and injected code. For HTML that really should appear as HTML:
- HTML fields:
{{ post.inhalt|safe }} - Markdown fields:
{{ post.text|markdown }}
Conditions and loops
{% if page.slug == "kontakt" %}
…
{% elif route.query.seite %}
…
{% else %}
…
{% endif %}
{% for p in data("team") %}
<li class="{% if forloop.First %}erste{% endif %}">{{ forloop.Counter }}. {{ p.name }}</li>
{% empty %}
<li>Noch niemand da.</li>
{% endfor %}
Inside loops, forloop.Counter (from 1), forloop.Counter0 (from 0), forloop.First, forloop.Last and forloop.Revcounter are available.
Setting variables
{% with post=item("blog", "slug", route.slug) %}
<h1>{{ post.titel }}</h1>
{% endwith %}
Comments
{# Not output #}
Whitespace
Template tags don’t change spaces and line breaks – the text is output exactly as it stands (as with Twig). Hallo {% if name %}{{ name }}{% endif %}! keeps the space after “Hallo”.
Outputting template syntax: verbatim
If you need {{ … }} literally – for Vue, Alpine or Handlebars, for example –, wrap the area:
{% verbatim %}<div id="app">{{ nachricht }}</div>{% endverbatim %}
The importer sets verbatim automatically if the original contains such character sequences.
Available variables
| Variable | Content |
|---|---|
page.title, page.meta_title, page.slug, page.path, page.description, page.id, page.parent_id |
Current page (meta_title and description are already evaluated) |
page.noindex, page.nofollow |
Robots settings of the page (true/false) |
page.lang |
Effective language of the page – its own or inherited, otherwise de (see Multilingual websites) |
route.path |
Requested path |
route.slug |
Subpath on Catch-All pages, e.g. "mein-artikel" or "2025/rueckblick" |
route.rest |
Subpath as a list: route.rest.0, route.rest.1 … |
route.query.x |
Query parameter ?x=… |
today |
Today’s date as YYYY-MM-DD – matching date fields, e.g. data("events", "datum__gte", today) for upcoming events |
now |
Current point in time for the date filter: {{ now|date:"2006" }} (year in the copyright) |
site.title, site.id, site.domain, site.domains, site.favicon |
The current website (its home page) – site.domain is the first domain entered, site.favicon the URL of the website icon |
preview |
true in the admin preview |
Functions
| Function | Result |
|---|---|
nav() |
Published subpages of the current website’s home page (main menu) – without pages with Hide in menus; nav(true) with them |
children() / children(12) |
Published subpages of the current page or of page 12 – without hidden ones; children(true) / children(12, true) with them |
lang_root() / lang_root(12) |
Home page of the language tree of the current page or of page 12: the page itself or the nearest one above with its own language, otherwise the website’s home page – for menus per language: children(lang_root().id) |
data("slug", …) |
Entries of a Collection or result of a REST source (see Data) |
item("slug", "feld", wert) |
First entry with feld == wert – or nothing |
workflow("pfad", …) |
Result of a Workflow with an HTTP input at this path |
image(id, sizes, klasse) |
Responsive <img> with srcset, dimensions and alt text – for JPEG/PNG inside <picture> with WebP |
file(id) |
File info: .url, .name, .alt, .width, .height, .srcset, .webp_srcset, .mime, .size |
asset_url(id) |
Versioned URL of a CSS/JS file |
translations() / translations(entry) |
Published language versions of the page or of an entry, sorted by language code: each with lang, title, url, current – for language switchers |
language_name("fr") |
Name of a language in that language (“Français”, “Deutsch” …); unknown codes stay as they are |
not_found() |
Ends rendering with the 404 page (status 404) |
Every page from nav()/children() (and lang_root()) has title, path, slug, id, description and lang, plus hidden (Hide in menus). Example footer with legal notice and privacy policy, which are missing from the main menu:
<footer>{% for p in nav(true) %}{% if p.hidden %}<a href="{{ p.path }}">{{ p.title }}</a> {% endif %}{% endfor %}</footer>
Useful filters
| Filter | Example |
|---|---|
default |
{{ x|default:"–" }} |
upper, lower, title, capfirst |
{{ name|upper }} |
truncatechars, truncatewords |
{{ text|truncatechars:120 }} |
floatformat |
{{ preis|floatformat:2 }} |
length |
{{ liste|length }} |
join |
{{ tags|join:", " }} |
safe |
Output HTML unfiltered |
markdown |
Markdown to HTML (markdown:"html" lets embedded HTML through – see below) |
json |
Value as JSON, e.g. for JavaScript: <script>const cfg = {{ cfg|json }};</script> |
urlencode |
<a href="/suche?q={{ q|urlencode }}"> |
Markdown directly in HTML
Longer texts are more convenient to write in Markdown – even in the middle of a page’s or Snippet’s HTML. Wrap the area with {% filter markdown %}:
<main class="container">
{% filter markdown %}
# Über uns
Wir gestalten und entwickeln **Websites mit Haltung** – von der Idee bis zum Betrieb.
Mehr dazu unter [Leistungen](/leistungen) – das Team von {{ site.title }} freut sich auf dich.
- Beratung
- Umsetzung
{% endfilter %}
</main>
- Template variables and tags work inside the block.
- Headings get anchors for jump links, umlauts are transliterated: “Über uns” →
#ueber-uns, “Größe” →#groesse. - HTML in Markdown: The normal filter discards embedded HTML (
<div …>), because it also processes content from Collections and REST sources, which can come from outside. For texts you write yourself in the editor,{% filter markdown:"html" %}lets HTML through – for example for a<div class="hinweis">between paragraphs. For data from outside,|markdownremains the safe choice.
Pens – playgrounds
Under Pens you create small playgrounds – like on CodePen: HTML (with the full template feature set: data(), item(), workflow(), filters …), CSS and JS side by side, with the result below, live as you type. Good for trying out templates with real data, for prototypes or small microsites.
- Each Pen has its own address that can’t be guessed:
/cms-admin/pen/<uuid>. When signed in, it opens the editor (fullscreen); Ctrl+S saves. At the top you switch the layout – Code on top (side by side) or Code at side (stacked on the left, result on the right) –; Fullscreen shows only the result, filling the screen (Esc exits). - Public (switch at the top): Without signing in, the same address shows the result and the code next to it – read-only, in HTML/CSS/JS tabs. With Show code off, the address is a standalone page: only the result, with the Pen’s title – handy for quick single pages. Non-public Pens don’t exist for visitors (404). As an admin, you see the visitor view via Visitor view (new tab,
?view=1). - Pens are always
noindex, nofollow(meta tag andX-Robots-Tagheader) and don’t appear in any sitemap. - The result runs in a sandbox with its own origin – scripts in the Pen can’t get at your admin session.
- For visitors, only published Collection entries are used; in the editor, as in the preview, you also see drafts.
siteandnav()refer to the website of the requested domain. - Pens are part of bundles; they don’t have a version history.