Twig Standards
A Twig template's job is narrow on purpose: take data that's already been prepared, and display it. Almost every Twig standard exists to keep that boundary from blurring.
1. Keep Logic Out of Templates
It's tempting to reach for a bit of PHP-like logic directly inside a template, but that logic belongs in a preprocess function instead. Moving it out keeps the template readable at a glance, and makes the logic itself something that can actually be unit tested.
{# ❌ Incorrect: business logic computed inline in the template. #}
{% if node.getType() == 'article' and node.get('field_featured').value == 1 %}
<span class="badge">Featured</span>
{% endif %}
{# ✅ Correct: the preprocess function already decided this. #}
{% if is_featured %}
<span class="badge">Featured</span>
{% endif %}A template can still hold simple presentational conditionals — whether a value is empty, which of two classes to apply — that's not business logic, just display logic.
2. Translation
Every piece of text shown to a user passes through Drupal's translation system, using the |t filter, even in a project that only ever ships in one language — because there's no telling when that stops being true.
{# ❌ Incorrect: hard-coded text, never translated. #}
Read more
{# ✅ Correct: passed through the translation system. #}
{{ 'Read more'|t }}A translatable string that needs a dynamic value gets a placeholder rather than string concatenation, so the translation itself stays one coherent sentence instead of fragments glued together.
{{ 'Welcome back, @name!'|t({'@name': username}) }}3. Autoescaping and |raw
Twig escapes output automatically, so |raw is rarely needed — reaching for it on anything that came from a user or an editor is how an XSS vulnerability gets into a theme.
{# ❌ Incorrect: |raw on user-supplied content opens the door to XSS. #}
{{ comment.body|raw }}
{# ✅ Correct: let Twig's autoescaping do its job. #}
{{ comment.body }}Content that's already been through a trusted sanitization step — a render array marked safe, output from check_markup() — is the only kind of content |raw is meant for.
4. Whitespace Control
The {%- and -%} tag variants strip the surrounding whitespace, which matters more than it sounds like it should in templates that generate markup sensitive to stray whitespace, like inline elements or <pre> blocks.
{%- for item in items -%}
<li>{{ item.label }}</li>
{%- endfor -%}5. The Attributes Object
An element's classes and other HTML attributes are built up through the attributes object rather than written out as a literal string, which is what lets other modules add their own classes and attributes to that same element without a template override.
{# ❌ Incorrect: a hard-coded class attribute. #}
<div class="card card--featured">
{# ✅ Correct: built through the attributes object. #}
<div{{ attributes.addClass('card', 'card--featured') }}>