Drupal Twig Templating — Complete Guide to Twig in Drupal
In this tutorial, you'll learn how to use Twig in Drupal theming — from basic syntax and variables to filters, functions, template inheritance, and debugging — so you can build custom templates with confidence.
What You'll Learn
- Twig syntax fundamentals: variables, blocks, comments, and control structures
- How Drupal exposes content, page, node, and user variables to templates
- Using Twig filters and functions for text formatting and URL generation
- Extending and including templates with inheritance
- Enabling Twig debugging and performance optimization
Why It Matters
Drupal 8 replaced PHP-based templates with Twig, a modern template engine from the Symfony framework. Twig is faster, more secure, and easier to read than raw PHP. It automatically escapes output, prevents XSS Attacks, and enforces a clean separation between logic and presentation. For anyone building Drupal themes, Twig is the language you will use every day. Mastering it means you can build any layout, override any template, and customize every aspect of your site's appearance.
Real-World Use
A development team builds a custom publishing platform on Drupal. The designers create mockups in Figma, and the frontend developers translate them into Twig templates. They use template inheritance to define a base page layout, then extend it for articles, landing pages, and search results. Twig filters format dates and truncate text. The autoescape feature ensures user-generated content never breaks the layout or executes malicious scripts.
Learning Path
flowchart LR A[Installing Themes] --> B[Twig Templating] B --> C[Sub-themes] C --> D[Template Suggestions] D --> E[Asset Libraries] E --> F[Module Management] F --> G[Essential Modules]
Twig Overview
Twig is a template engine written in PHP and used by the Symfony framework. Drupal adopted Twig starting with Drupal 8, replacing the old PHPtemplate system. Twig compiles templates to PHP classes for performance.
Key principles of Twig:
Twig separates template logic from PHP business logic. You cannot run arbitrary PHP code inside a Twig template. This makes templates safer and easier to maintain.
Twig automatically escapes all output unless explicitly marked as safe. This prevents XSS vulnerabilities by default.
Twig template files use the .html.twig extension and live in the theme's templates/ directory.
Basic Syntax
Twig has three main delimiters:
{{ variable }} {# Outputs the value of a variable #}
{% block name %} {# Executes a control structure or block #}
{# This is a comment #} {# Comments are stripped from output #}
Variables
Variables are accessed using dot notation:
<h1>{{ node.title.value }}</h1>
<p>{{ node.body.value }}</p>
<span>Author: {{ node.getOwner().getDisplayName() }}</span>
Control Structures
{# If statement #}
{% if node.isPublished() %}
<p>This content is published.</p>
{% else %}
<p>This content is not published.</p>
{% endif %}
{# For loop #}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% else %}
<li>No items found.</li>
{% endfor %}
</ul>
{# Set variable #}
{% set custom_class = 'highlight' %}
<div class="{{ custom_class }}">Content</div>
Variables in Drupal
Drupal provides a rich set of variables to Twig templates.
Global Variables
site— site information (name, slogan, url)user— current user object (uid, roles, display name)front_page— the front page URLlanguage— current language objectlogged_in— boolean, true if user is authenticatedis_admin— boolean, true if user has admin rolebase_path— Drupal root path (usually/)directory— theme directory path
Page Variables
Available in page.html.twig:
page.content— main content areapage.sidebar— sidebar region contentpage.header— header region contentpage.footer— footer region contentpage.primary_menu— main navigationpage.breadcrumb— breadcrumb trailpage.title— page titlepage.tabs— local task tabs (edit, view, etc.)
Node Variables
Available in node.html.twig:
node— the full node objectlabel— the node title (string)content— rendered content arraydate— node creation dateauthor_name— author display namenode_type— content type machine namestatus— published statusin_preview— whether in preview modeview_mode— current view mode (full, teaser, etc.)
Filters
Filters transform variable values. They are applied with the pipe | character.
{{ 'hello'|upper }} {# HELLO #}
{{ 'HELLO'|lower }} {# hello #}
{{ 'hello world'|capitalize }} {# Hello world #}
{{ 'Hello World'|replace({'World': 'Drupal'}) }} {# Hello Drupal #}
{{ node.body.value|striptags }} {# Remove HTML tags #}
{{ node.body.value|striptags|slice(0, 150) ~ '...' }} {# Truncate to 150 chars #}
Drupal-Specific Filters
{# Translate string #}
{{ 'Submit'|t }}
{# Clean ID for HTML attributes #}
{{ 'My Custom ID'|clean_id }}
{# Output: my-custom-id #}
{# Remove specific keys from array #}
{{ content|without('comment', 'links') }}
{# Safe join of array values #}
{{ items|safe_join(', ') }}
{# Format date #}
{{ node.created.value|format_date('medium') }}
{{ node.created.value|date('Y-m-d') }}
{# Placeholder replacement #}
{{ 'Hello @name'|t({'@name': user.displayname}) }}
Chaining Filters
{{ node.title.value|striptags|upper|slice(0, 50) }}
Functions
Functions generate dynamic values in templates.
{# Generate internal path #}
<a href="{{ path('entity.node.canonical', {'node': node.id}) }}">{{ label }}</a>
{# Generate full URL #}
<a href="{{ url('entity.node.canonical', {'node': node.id}, {'absolute': true}) }}">{{ label }}</a>
{# Get active theme path #}
<img src="{{ active_theme_path() }}/images/logo.png" alt="Logo">
{# File URL from URI #}
<img src="{{ file_url(node.field_image.entity.uri.value) }}" alt="{{ node.field_image.alt }}">
{# Attach a library #}
{{ attach_library('mytheme/custom') }}
{# Link generation #}
{{ link('Read more', 'entity.node.canonical', {'node': node.id}, {'class': ['read-more']}) }}
Extends and Includes
Template inheritance is one of Twig's most powerful features.
Extends
{# templates/page.html.twig - base layout #}
<!DOCTYPE html>
<html>
<head>
<title>{{ head_title }}</title>
{{ stylesheets }}
</head>
<body>
<header>{{ page.header }}</header>
<main>{% block content %}{% endblock %}</main>
<footer>{{ page.footer }}</footer>
{{ scripts }}
</body>
</html>
{# templates/page--article.html.twig - extends the base #}
{% extends 'page.html.twig' %}
{% block content %}
<article class="article">
<h1>{{ node.title.value }}</h1>
<div class="meta">
{{ node.created.value|format_date('long') }}
</div>
<div class="body">
{{ node.body.value }}
</div>
</article>
{% endblock %}
Includes
{# Include a reusable component #}
{{ include('@mytheme/templates/header.html.twig') }}
{# Include with variables #}
{{ include('@mytheme/templates/card.html.twig', {
title: 'Card Title',
body: 'Card body content'
}) }}
Blocks
Blocks define sections of content that child templates can override.
{# Base template with blocks #}
{% block page_title %}
<h1>{{ title }}</h1>
{% endblock %}
{% block page_content %}
{{ content }}
{% endblock %}
{% block page_sidebar %}
{{ sidebar }}
{% endblock %}
A child template can override any block:
{% extends 'base.html.twig' %}
{% block page_title %}
<h1 class="custom-title">{{ title }}</h1>
{% endblock %}
{# Omit page_sidebar — it uses the parent's default #}
{% block page_content %}
{{ parent() }} {# Include parent block content #}
<section class="additional">Extra content</section>
{% endblock %}
Debug Output
Twig debugging shows which template is being used and available variables.
Enabling Twig Debug
# sites/default/services.yml
parameters:
twig.config:
debug: true
cache: false # Disable Twig cache during development
After enabling, reload the page and view the HTML source. You will see HTML comments showing which templates were used:
<!-- THEME HOOK: 'node' -->
<!-- FILE NAME SUGGESTIONS:
* node--article--full.html.twig
* node--article.html.twig
* node.html.twig
-->
<!-- BEGIN OUTPUT from 'themes/custom/mytheme/templates/node.html.twig' -->
Dumping Variables
{# Dump all available variables (requires Devel module) #}
{{ kint() }}
{{ dpm() }}
{# Dump a specific variable #}
{{ kint(node) }}
{{ dpm(node.title.value) }}
Autoescape
Twig automatically escapes output as HTML by default. Use the raw filter to output unescaped HTML:
{# Autoescaped - safe but HTML is escaped #}
{{ node.body.value }}
{# Output: <p>Hello</p> #}
{# Raw - outputs HTML as-is #}
{{ node.body.value|raw }}
{# Output: <p>Hello</p> #}
Always use |raw carefully. Only apply it to trusted content from administrators. User-generated content should never use |raw.
Drupal Twig Extensions
The twig_tweak module provides additional Twig functions and filters:
composer require drupal/twig_tweak
{# Load a view #}
{{ drupal_view('my_view', 'block_1', arg1, arg2) }}
{# Load entity content #}
{{ drupal_entity('node', 123, 'teaser') }}
{# Load a block #}
{{ drupal_block('system_branding_block') }}
{# Get a field from a specific entity #}
{{ drupal_field('body', 'node', 123) }}
{# Get a region #}
{{ drupal_region('sidebar') }}
{# Get a token value #}
{{ drupal_token('site:name') }}
Template Caching
Twig caches compiled templates to PHP classes. During development, disable the cache:
parameters:
twig.config:
cache: false
In production, Twig cache is enabled and improves performance. Clear the cache after template changes:
drush cr
The compiled templates are stored at sites/default/files/php/twig/.
Common Mistakes
Not using autoescape: Disabling autoescape or applying
|rawto untrusted content exposes the site to XSS attacks. Only use raw on content authored by trusted administrators.Forgetting
|rawfor rendered content: Some Drupal render arrays need|rawto render properly. Check whether the variable is already a string or a render array.Hardcoding URLs: Using absolute URLs like
/node/123instead of thepath()function breaks when the URL alias changes. Always use Twig functions for URLs.Not clearing Twig cache: After modifying templates, the old cached version may still be served. Run
drush crto clear all caches.Putting business logic in templates: Twig is for presentation only. Complex logic belongs in preprocess functions or custom modules, not in
{% if %}blocks in templates.
Practice Questions
How does Twig's autoescape feature improve security compared to raw PHP templates?
What is the difference between
{% extends %}and{% include %}in Twig?How would you truncate a long text field to 200 characters and append an ellipsis using Twig filters?
Challenge: Create a Twig template structure for a blog site. Write a base
page.html.twigwith header, content, and footer blocks. Create anode--article.html.twigthat extends the base, overrides the content block, and displays the article title, author, date, body, and tags. Add a sidebar include that shows recent posts using thedrupal_view()function from twig_tweak.
FAQ
Mini Project
Goal: Create a custom Twig template for an article content type.
- Create
templates/node--article.html.twigin your theme - Display the article title wrapped in an
<h1>with a link - Show the author name and publication date using the
format_datefilter - Render the body field with
|rawto output HTML - Display all taxonomy tags as a comma-separated list
- Add an "Estimated reading time" calculated from word count
- Enable Twig debugging and verify your template is being used
- Disable Twig cache in services.yml during development
What's Next
Now that you understand Twig templating, proceed to creating sub-themes to customize base themes. After that, explore template suggestions for fine-grained template control.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro