Drupalwoo ☉

Advanced Drupal templating with Twig extensions

Twig gives Drupal a template layer that separates content processing from presentation. In Drupal 10 and 11, templates can use built-in filters, Twig functions and contributed extensions to turn field data and render arrays into reliable markup. That separation keeps PHP out of theme files and gives developers a consistent syntax for reusable behaviour across nodes, views, blocks and custom entities.

Drupal’s Twig extensions are most valuable when they solve a recurring presentation problem without hiding business logic. The core Twig extension supplies filters and functions, while modules such as TwigTweak can add further helpers. Check a contributed module’s compatibility with your Drupal and PHP versions before adopting it, and keep custom work in a dedicated module rather than patching core.

Extensions are useful for conditional markup, consistent field formatting, translated labels and calculations such as reading time. They can also conceal accidental complexity: a filter that silently accepts an array, produces unsafe markup or performs an expensive query makes every template harder to reason about. Treat an extension as public API with defined inputs and outputs, not as a convenient place to hide project-specific rules.

Australian projects add useful constraints. Teams in Melbourne and Sydney commonly format dates in Australian English and handle AEST and AEDT daylight-saving transitions, while Canberra agencies publishing on .gov.au domains need Drupal implementations that support WCAG 2.2 AA. Multilingual briefs may combine English with Japanese, Mandarin or Arabic, so an extension must respect translation, locale and right-to-left behaviour rather than hard-code English assumptions.

Approach Where it belongs Control over output Main limitation
Core Twig filters Escaping, dates, numbers and text cleanup Low The raw filter disables auto-escaping
Drupal template functions URLs, attributes, blocks and rendered content Medium Some results already contain trusted markup
Custom Twig filters Repeated domain-specific formatting or calculations High Input, security and performance need explicit rules
JavaScript and CSS Browser-only interactions and visual layout Low in the template Neither should supply canonical content or permission decisions

Core filters and Drupal-ready output

Twig’s standard filters already cover many routine requirements. Use escape when inserting values into HTML or attributes, striptags when producing plain-text summaries, and number_format for controlled numeric presentation. Drupal also provides template functions for constructing URLs, rendering blocks and creating safe HTML attributes. These helpers provide a useful baseline before a project adds its own extension.

Dates illustrate why the correct Drupal helper matters. A machine-readable timestamp should remain consistent, while the visible value should follow the active language and configured time zone. For example:

<time datetime="{{ node.created|date('c') }}">
  {{ format_date(node.created, 'short') }}
</time>

A Melbourne publisher should verify how this resolves during daylight-saving changes rather than hard-coding an offset. A fixed English date format can also be replaced with a translated label supplied by a field formatter.

Auto-escaping remains active unless a filter explicitly bypasses it. Values returned by escape and most ordinary Twig functions are safe for their declared context, but a raw filter removes that protection. Treat raw output as an exception for content you have already validated, such as trusted library markup. When preparing structured data for JavaScript, use proper JSON encoding and context-appropriate escaping instead of concatenating user-controlled strings.

Working with render arrays and field markup

Drupal supplies many templates with a prepared content render array. Rendering one of its fields preserves the formatter, visibility settings and field-level markup configured by site builders. This is usually preferable to reconstructing HTML from raw field values:

{% set body = content.body|render %}
{% if body %}
  <div class="node__body">
    {{ body }}
  </div>
{% endif %}

The render filter handles Drupal’s render arrays and returns markup that Twig recognises as safe within the relevant context. Rendering the same field repeatedly can still add unnecessary work, so prepare the result once when several parts of a template need it. If the field is already displayed elsewhere in the node template, hide the original display or render it in only one location to avoid duplicated wrappers.

Entity methods remain available for values that display settings deliberately omit. Machine identifiers such as node.bundle and node.id are useful for classes, links and conditional behaviour, but they should not become visible labels. For example, a template can derive stable styling from a content type:

{% set classes = ['card', 'card--' ~ node.bundle] %}
<article{{ create_attribute({'class': classes|join(' ')}) }}>
  {{ content }}
</article>

Translated labels should come from translated fields or an access-aware service. Twig is a presentation layer, not a substitute for Drupal’s permission system. If an operation depends on editorial status, account roles or unpublished data, make that decision through the appropriate service or preprocessing layer before the value reaches the template.

Building a custom Twig filter

A small extension gives repeated template logic a clear home. Create the service in the module’s services.yml file and tag it so Drupal registers it with Twig:

services:
  example.twig_extension:
    class: Drupal\example\Twig\ExampleExtension
    tags:
      - { name: twig.extension }

The extension class can define filters with explicit names and methods:

<?php

namespace Drupal\example\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

final class ExampleExtension extends AbstractExtension
{
  public function getFilters(): array
  {
    return [
      new TwigFilter('reading_time', [$this, 'readingTime']),
    ];
  }

  public function readingTime(mixed $value): string
  {
    $text = html_entity_decode(strip_tags((string) $value), ENT_QUOTES, 'UTF-8');
    $count = preg_match_all("/[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*/u", $text, $matches) ?: 0;

    return max(1, (int) ceil($count / 220)) . ' min read';
  }
}

The filter can then be used alongside Drupal’s render filter:

{{ content.body|render|reading_time }}

This implementation counts words in the rendered body after removing markup. Its Unicode-aware pattern also handles multilingual content better than an English-only word counter. The assumed reading speed can become a configuration value, and the unit should come from Drupal’s translation service when localisation matters.

Returning a plain string leaves the result subject to Twig’s auto-escaping. That is a useful default for calculated output. A filter should never be marked as HTML-safe merely to remove an escaping warning, because its input may later change. Inject configuration, entity access or translation services through the constructor rather than creating dependencies inside the filter method.

Reusable patterns for Australian projects

The most useful project toolkit is usually small. A reading-time filter, a phone-number formatter and a helper that maps stored option values to translated labels solve recurring problems without giving every template a general-purpose programming interface. Each helper should have a narrow purpose and predictable behaviour.

Privacy and consent rules remain outside that presentation boundary. For Australian organisations, the Privacy Act 1988 governs personal information in many commercial projects, while the Spam Act 2003 sets consent requirements for commercial electronic messages. Formatting a customer’s telephone number or email address in Twig does not make that information publishable. Access decisions belong in properly scoped services and Drupal modules; templates should receive content already approved for display.

For a small Drupal team, a compact extension library might include:

Every extension also needs a clear usage contract:

Testing and maintaining template logic

A custom extension should arrive with automated coverage. Unit tests can exercise pure calculations, including empty text, multilingual text and boundary values. A browser or kernel-level rendering test should then prove that the filter appears correctly in a real node, view or block. This combination catches both faulty calculations and template integration errors.

During development, enable Twig debugging in a protected non-production environment and inspect suspect values with Twig’s dump() function. Clear Drupal’s caches after changing a tagged service, then check the rendered render arrays rather than guessing which template supplied the output. Debugging information may contain unpublished content or personal information, so it should never be enabled on a public production site.

Before release, review filters that perform database queries, remote requests or repeated processing of large strings. Cache results where the expected behaviour allows it, impose sensible input limits and avoid unnecessary calls across multiple fields. Twig’s auto-escaping protects output at the final render; it does not protect permissions, query safety or runtime performance.

A documented extension with narrow inputs, predictable outputs and automated tests remains easier to upgrade, audit and reuse across Australian Drupal estates as core, Twig and contributed modules evolve.