Guide 02 · Developer docs

Template languages: Smarty and Twig

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 inheritance, includes, embed and macros

How Twig templates are composed on Core dna: a base template and the blocks a child fills, includes with with and only, embed for parameterised wrappers, macros and import, the template context for state that crosses templates, and whitespace control, with the Smarty equivalent where one exists.

Who this is for. The developer building a Twig site's page templates, partials and component library. Afterwards you can choose between an include, an embed and a macro for each reusable piece, and carry state across templates that do not share variables.

A base template and a child

Template code templates/includes/base.html Twig only
<!doctype html>
<html lang="en">
<head>
  <title>{% block title %}{{ page.getName()|e }}{% endblock %} | Example</title>
  {% block head %}{% endblock %}
</head>
<body class="{{ bodyClass|default('') }}">
  {{ include('includes/nav.html') }}
  {% if block('promo') is defined %}<aside class="promo">{{ block('promo') }}</aside>{% endif %}
  <main>{% block content %}{% endblock %}</main>
  <footer>{% block footer %}<nav>{% block footer_links %}{% endblock %}</nav>{% endblock %}</footer>
</body>
</html>
Template code templates/pages/pages/article.html Twig only
{% extends 'includes/base.html' %}
{% set bodyClass = 'article' %}                  {# outside any block: reaches the base everywhere #}

{% block title %}Article: {{ parent() }}{% endblock %}
{% block promo %}Read the full series{% endblock %}
{% block content %}
  <h1>{{ page.getName()|e }}</h1>
  {{ page.body }}
{% endblock %}
{% block footer_links %}{{ parent() }}<a href="/archive">Archive</a>{% endblock %}
  • {{ parent() }} prints the base's content of the block, wherever you place it: before your content for Smarty's append, after it for prepend.
  • Nested blocks can be overridden one at a time: the child above replaces footer_links and keeps the rest of footer.
  • block('promo') is defined renders a wrapper only when a child fills the block, the job Smarty's hide does.
  • Anything but blocks and set at a child's top level is an error: A template that extends another one cannot include content outside Twig blocks. Smarty discards the same content.
  • A set outside any block reaches the base everywhere; a set inside a block stays in that block. The platform's set also stores the value in the template context, which is how a value set inside a block reaches the rest of the page (below).

include, with and only

An include renders another template in place. By default it sees all of the caller's variables, plus any you pass with with; only gives it nothing but what you pass. That isolation is the main thing Twig's include has over Smarty's.

Template code Twig only
{% include 'elements/card.html' with {title: post.getName(), url: post.getFullPath()} %}
{% include 'elements/card.html' with {title: post.getName(), url: post.getFullPath()} only %}

{# the function form, which can be filtered or assigned #}
{{ include('elements/card.html', {title: post.getName()}, with_context = false) }}
{% set cardHtml = include('elements/card.html', {title: post.getName()}) %}

{# an optional partial #}
{% include 'elements/' ~ variant ~ '.html' ignore missing %}
  • Use only for shared partials. A partial that receives only its parameters behaves the same wherever it is included, and cannot pick up an unrelated variable of the same name from its caller.
  • The underscore helpers survive only. For a page render they are Twig globals, so an isolated partial still has _request, _url, __ and the rest.
  • Variables set inside the include stay there, as in Smarty; their stored copies are in the template context.
  • ignore missing turns a missing file into nothing rather than an error, which Smarty cannot do.

Shared partials go in includes/ for chrome, elements/ for small reusable pieces, and a partials/ folder beside the templates that share them, always named in full from templates/: Twig has no relative names.

embed: an include whose blocks you fill

embed includes a template and overrides its blocks in one step, which suits wrappers such as cards, panels and modals: the wrapper owns the markup, the caller supplies the parts. Smarty has no equivalent; there you pass captured markup into an include.

Template code Twig only
{# templates/elements/panel.html #}
<div class="panel panel--{{ tone|default('plain') }}">
  <h3 class="panel__title">{% block title %}{% endblock %}</h3>
  <div class="panel__body">{% block body %}{% endblock %}</div>
</div>

{# a page template #}
{% embed 'elements/panel.html' with {tone: 'info'} %}
  {% block title %}Opening hours{% endblock %}
  {% block body %}{{ component.hours }}{% endblock %}
{% endembed %}

embed takes with and only like an include, and the blocks inside it see the caller's variables.

Macros and import

A macro is a named fragment that takes arguments and returns markup. Macros live in a library file and are imported under a name, or used from the same file through _self.

Template code Twig only
{# templates/includes/ui.twig #}
{% macro badge(label, tone = 'info') %}
  <span class="badge badge--{{ tone }}">{{ label|e }}</span>
{% endmacro %}

{# any template #}
{% import 'includes/ui.twig' as ui %}
{{ ui.badge('New') }}
{{ ui.badge('Sold out', 'warning') }}

{% from 'includes/ui.twig' import badge %}
{{ badge('Last few') }}
  • A macro does not see the calling template's variables. Everything it needs comes in as arguments. The exceptions are the underscore helpers, which are globals, and the template context.
  • A macro beats an include when the piece is a function of a few values and is used many times on a page, such as a badge, a price or a formatted date: the arguments are explicit and the call is short. Prefer an include for a larger piece with its own logic, and an embed when the caller fills slots.
  • Every set inside a macro is stored in the template context, so give working names a prefix (The platform's set tag).

Macros are also where a Twig site keeps reusable transformations it would otherwise write as a filter, since a site cannot register extensions.

The template context in Twig

_templateContext works in Twig as in Smarty: one store for the whole render, which components and records rendered with getHtml() share, and which crosses every block, include and macro boundary. Read with _templateContext.get('key', default); write with the platform's set, which stores automatically, or explicitly with {% do _templateContext.set('key', value) %}. The heading-level pattern, in Twig:

Template code templates/components/section.html Twig only
{% set _section_level = min(_templateContext.get('headingLevel', 2), 6) %}
<section>
  <h{{ _section_level }}>{{ component.getAreaText('heading')|e }}</h{{ _section_level }}>
  {% do _templateContext.set('headingLevel', _section_level + 1) %}
  {% for element in component.getAreaCollectionElements('items') %}
    {% set _section_child = element.isGlobal() ? element.getGlobalComponent() : element.getComponent() %}
    {{ _section_child ? _section_child.getHtml() }}
  {% endfor %}
  {% do _templateContext.set('headingLevel', _section_level) %}
</section>

The explicit set() calls matter here: the key, headingLevel, is not the name of any variable, and the last call restores the level for the next section. Pass a variable across an inheritance boundary covers the inheritance case in both engines.

Whitespace control

A - inside a delimiter removes all whitespace on that side: {%- if x %}, {{- value -}}. A ~ removes spaces and tabs but keeps line breaks. This is the Twig way to keep generated markup tight, the job Smarty does with {strip} around a whole block. The spaceless filter, used as {% apply spaceless %}, still works but is deprecated since Twig 3.12; prefer the modifiers. As with {strip}, removing whitespace between inline elements or words joins them on the page.

Go deeper

Was this page helpful?

Your answer goes to the docs team.