Build your first theme 2: Twig templates and suggestions
In part 2 of the theme tutorial you take control of markup: you override the node template, then use Twig’s debug output to discover the more specific template names Drupal looks for, and target node teasers with one of them.
What you’ll build
Section titled “What you’ll build”Two templates in First theme:
templates/node.html.twig, your own override of the node template that ships with the Node module,templates/node--teaser.html.twig, a template suggestion that Drupal picks automatically whenever a node renders in the teaser view mode.
You start from the theme as part 1 left it: installed, default, with your own regions.
Prerequisites
Section titled “Prerequisites”- First theme installed and set as default per part 1.
- A couple of article nodes, so the front page shows teasers.
Step 1: Turn on Twig development mode
Section titled “Step 1: Turn on Twig development mode”Drupal compiles Twig templates to PHP and caches the result aggressively, which is exactly wrong while you are editing them. Core ships a development mode that recompiles templates on every change and annotates the page source with HTML comments naming each template and its alternatives.
Go to Administer → Configuration → Development → Development settings/admin/config/development/settings, check Twig development mode, leave the two sub-options it reveals (Twig debug mode and Disable Twig cache) checked, and select Save settings. The debugging guide covers this page in depth, including how to toggle the same settings from the command line.
Step 2: Override the node template
Section titled “Step 2: Override the node template”Every theme hook a module defines comes with a default template; a theme overrides one by shipping a template file with the same name. Drupal finds template files anywhere below templates/, by name alone.
The starterkit copy of the theme already contains one at templates/content/node.html.twig. Delete that file, so the theme has exactly one node template, and create templates/node.html.twig:
{#/** * @file * First theme's override of the node template. * * Drupal finds this file by name: node.html.twig in the active theme wins * over the copy that ships with the node module. * * Available variables include: * - node: The node entity. * - label: The (possibly hidden) node title. * - content: The render array of all visible fields. Render it whole with * {{ content }} or pick fields with {{ content.field_name }}. * - attributes / title_attributes / content_attributes: HTML attribute * objects for the wrapper, title and content elements. * - page: TRUE when the node is shown as a full page. * * @see template_preprocess_node() */#}<article{{ attributes.addClass('node', 'node--' ~ node.bundle|clean_class) }}> {{ title_prefix }} {% if label and not page %} <h2{{ title_attributes }}> <a href="{{ url }}" rel="bookmark">{{ label }}</a> </h2> {% endif %} {{ title_suffix }}
<div{{ content_attributes.addClass('node__content') }}> {{ content }} </div></article>Two things in this template repay attention:
contentis a render array holding every visible field.{{ content }}renders all of it;{{ content.body }}would render one field. Render arrays, not raw values, are what templates receive, and printing them is what triggers rendering. The render pipeline explains why.attributes,title_attributes, andcontent_attributesareAttributeobjects. Methods likeaddClass()merge your classes with whatever modules added during preprocessing, instead of overwriting it. The|clean_classfilter (one of Drupal’s Twig additions inTwigExtension) turns a bundle name likeblog_postinto a safe CSS class.
New template files are one of the few things Twig development mode does not pick up, because the list of discovered templates lives in the theme registry cache:
ddev drush cache:rebuildStep 3: Read the suggestions and target teasers
Section titled “Step 3: Read the suggestions and target teasers”Look closer at the FILE NAME SUGGESTIONS comment above an article teaser:
<!-- FILE NAME SUGGESTIONS: ▪️ node--1--teaser.html.twig ▪️ node--1.html.twig ▪️ node--article--teaser.html.twig ▪️ node--article.html.twig ▪️ node--teaser.html.twig ✅ node.html.twig-->This is the suggestion list: alternative template names Drupal checked, most specific first, with ✅ marking the one it used. The Node module provides these alternatives, and the pattern is readable directly off the list: by view mode, by bundle, by ID, and combinations. Create the file matching “any node in the teaser view mode”, templates/node--teaser.html.twig:
{# First pass. Part 4 rebuilds this around the card component. #}<article{{ attributes.addClass('node', 'node--teaser') }}> <h3><a href="{{ url }}" rel="bookmark">{{ label }}</a></h3> <div{{ content_attributes }}>{{ content }}</div></article>Rebuild caches once more so the registry sees the new file. Full article pages keep using node.html.twig; teasers switch to the new template. You changed nothing else: picking the most specific existing template is the theme system’s job.
Step 4: Know where suggestions come from
Section titled “Step 4: Know where suggestions come from”You will eventually want a suggestion that core does not offer, and there is a split in how the two sides of Drupal provide them:
Modules define suggestions by implementing hook_theme_suggestions_HOOK() as a #[Hook] attribute method in a class under src/Hook/. That is exactly where your teaser suggestion came from; the Node module implements it in its NodeThemeHooks class:
// Fragment: how a module provides suggestions (src/Hook class).#[Hook('theme_suggestions_node')]public function themeSuggestionsNode(array $variables): array { return ['node__' . $variables['elements']['#node']->bundle()];}Themes cannot use #[Hook] classes: core’s hook attribute documentation states that hooks implemented by themes must remain procedural. A theme adds or reorders suggestions with hook_theme_suggestions_HOOK_alter() as a plain function in its .theme file:
// Fragment: first_theme.theme. Theme hooks stay procedural functions.function first_theme_theme_suggestions_node_alter(array &$suggestions, array $variables): void { $suggestions[] = 'node__highlighted';}Suggestions are machine names with double underscores (node__teaser); the matching file names use double hyphens (node--teaser.html.twig). A suggestion must start with the base hook name plus __, or Drupal discards it; Twig development mode lists discarded ones under INVALID FILE NAME SUGGESTIONS. First theme does not need custom suggestions, so its generated first_theme.theme keeps only the starterkit’s preprocess function for now (part 6 adds one of its own); object-oriented hooks covers the module side in full.
Troubleshooting
Section titled “Troubleshooting”My new template is ignored
Section titled “My new template is ignored”The theme registry caches the list of template files per theme hook. Twig development mode recompiles changed templates but does not rescan for new files: run ddev drush cache:rebuild after adding or renaming any template.
No debug comments in the page source
Section titled “No debug comments in the page source”Either Twig development mode is off, or you are looking at a cached page. The page cache for anonymous visitors stores pages rendered earlier; rebuild caches or check as a logged-in user.
Twig\Error\SyntaxError after editing a template
Section titled “Twig\Error\SyntaxError after editing a template”The error page names the template file and line. Typical causes: an unclosed {% if %} or {% for %} block, or {{ }} around a tag that needs {% %}. Fix the line; with the Twig cache disabled the next reload recompiles immediately.
The same template appears twice in my theme
Section titled “The same template appears twice in my theme”Two files with the same name in different subdirectories of templates/ are ambiguous; which one wins depends on scan order, not on your intent. Keep exactly one, as step 2 did by deleting the starterkit copy.
Next steps
Section titled “Next steps”- Part 3: libraries, CSS, and breakpoints gives the theme its own stylesheet.
- Twig in Drupal is the reference for template variables, filters, and preprocessing.
- The render pipeline explains theme hooks and the registry underneath all of this.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.