Core dna renders server-side sites with Smarty or with Twig, one engine per site, and treats the two as peers. This guide is the one place both are documented: what they share, each engine's language and the platform's additions and limits, the two side by side, and translation.
L1–L2
Unit 03 · Twig
Twig syntax: output, attributes and control flow
TwigPage 15 of 25Updated
The Twig you write every day on Core dna: printing values and chaining filters, reaching a record's getters and areas as attributes, conditions, loops and tests, and the platform's own set tag, which also stores every value in the template context. Checked against the version the platform runs.
Who this is for. The developer writing templates on a Twig site. Afterwards you can print and filter values, read records the way the platform resolves them, write conditions and loops, and know what the platform's set does beyond stock Twig.
Output and filters
{{ … }} prints, {% … %} runs a tag, {# … #} is a comment. Filters follow a |, take their arguments in parentheses, and chain left to right.
Template codeTwig only
{{ page.getName() }} {# a method on a record #}
{{ page.name }} {# the same call, as an attribute #}
{{ item.title }} {{ item['data-id'] }} {# array keys #}
{{ page.getName()|e }} {# one filter #}
{{ page.getAreaText('summary')|striptags|u.truncate(160, '…')|e }} {# a chain #}
{{ "Results for #{query|e}" }} {# interpolation in double quotes #}
{{ 'Page ' ~ number }} {# joining strings #}
Escape last, after any filter that could change the text, and never escape a richtext area, which holds HTML on purpose. Nothing is escaped for you here (Twig in Core dna).
Records as attributes
A record's getters are reachable two ways: as a method call, page.getName(), and as an attribute, page.name. For an attribute, Twig asks the record for a property of that name, and the record answers through the platform's shorthand: the listed getter getName() if it has one, otherwise the layout area called name. So component.title is the title area of a component, and page.name is the page's name, not a layout area called name.
That is the same mechanism as Smarty's $component->title, and everything in The area shorthand applies unchanged here, including how each input type comes back, the live-preview behaviour and the limits. Three Twig specifics:
A name the record does not know prints nothing, where Smarty raises Property … does not exist. A misspelt area name and an empty area look the same.
record.anything is defined is always true, because a record accepts any call and decides afterwards. Test the value with is not empty, or a layout area with component.hasAreaWithName('title').
Methods with arguments need the call form: component.getAreaImage('hero').
Conditions, loops and tests
A loop over a page's component collection, in the shape production templates use:
Template codetemplates/pages/pages/landing.htmlTwig only
{% for element in page.getAreaCollectionElements('pageSections') %}
{% set section = element.isGlobal() ? element.getGlobalComponent() : element.getComponent() %}
{% if section %}
<section class="section{{ loop.first ? ' section--first' }}{{ loop.last ? ' section--last' }}"
data-position="{{ loop.index }}">
{{ section.getHtml() }}
</section>
{% endif %}
{% else %}
<p class="empty">No sections yet.</p>
{% endfor %}
{# keys and values, a filtered list, a range #}
{% for name, pixels in sizes %}{{ name }}: {{ pixels }}px {% endfor %}
{% for item in items|filter(i => i.isPublished()) %}…{% endfor %}
{% for i in 1..5 %}{{ i }}{% endfor %}
Loop variable
Value
loop.index, loop.index0
The pass number from 1, and from 0
loop.first, loop.last
True on the first and on the last pass
loop.length
The number of items
loop.revindex, loop.revindex0
Passes left, counting to 1, and to 0
loop.parent
The variables outside the loop
The {% else %} inside a for renders when the list is empty or null. Twig's ternary needs no parentheses ({{ count == 1 ? 'item' : 'items' }}), ?: falls back on a falsy value, and ?? on an undefined or null one. The tests used most are is defined, is null, is empty (the platform's own, here), is odd, is even, is divisible by(3), is iterable and is same as(…), plus the in and not in operators.
The platform's set tag
The platform replaces Twig's set with its own. It accepts exactly what stock Twig accepts, so the upstream manual's examples work:
Template codeTwig only
{% set columns = component.getAreaText('columns')|default(3) %}
{% set first, last = 'Ada', 'Lovelace' %} {# several targets #}
{% set intro %}
<p class="lede">{{ component.getAreaText('intro')|e }}</p>
{% endset %} {# the capture form #}
{# the difference: every value is also stored in the template context #}
{{ _templateContext.get('columns') }}
It does one thing more: in a page render, every value it sets is also written to _templateContext under the same name. (Template code typed into the admin has no template context, and there set is stock.) Three consequences:
A value can cross a boundary a Twig variable cannot. A variable set inside a block, an include or a macro is gone outside it; the stored copy can be read anywhere later in the render with _templateContext.get('name') (The template context in Twig).
Records rendered later with getHtml() receive the stored values, ahead of their own variables. Do not set the names records arrive as, such as page, post or record: a later page or post rendered with getHtml() would get yours instead of itself.
Names inside macros and partials are shared. A temporary {% set i = … %} in a macro overwrites the stored i of whoever else used that name. Give working names a prefix, such as _card_title.
Twig's {% do … %} runs an expression and prints nothing, which is the form for calls such as {% do _response.preventCaching() %}.