Introduction
Drupal is a flexible content management system (CMS) used to build websites ranging from simple blogs to complex, high-traffic platforms. Template customization is how you control the display of content and create consistent, accessible, and performant user experiences.
This tutorial walks through essential steps for customizing Drupal templates: setting up a local environment, understanding the theme layer and template hierarchy, working with Twig, creating a custom theme, and following performance and security best practices. The examples target Drupal 9 and Drupal 10 and include Composer and Drush commands commonly used in modern Drupal workflows.
Introduction to Drupal and Template Customization
Overview of Drupal
Drupal is an open-source CMS with a modular architecture and a robust theming layer. It separates content, presentation, and behavior so designers and developers can deliver tailored front-end experiences without changing core code.
Template customization in Drupal lets you adjust how content types, views, blocks, and pages render. You typically work in a theme directory, provide Twig templates and asset libraries, and use preprocess hooks for structured variable manipulation.
- Open-source platform
- Highly customizable templates and theme inheritance
- Active community and contributed modules
- Scales from small sites to enterprise deployments
Prerequisites
Before diving into Drupal theming, ensure you have the following foundational knowledge and tools. These make the learning curve smoother and reduce time spent on environment setup issues.
- HTML and CSS: comfortable creating accessible, semantic markup and working with responsive layouts.
- Basic PHP: familiarity with arrays, objects, and namespaces; useful for understanding preprocess hooks and small theme functions.
- Twig basics: variable interpolation, filters, and control structures — you can learn Twig quickly if you're already comfortable with template languages.
- Command-line experience: Composer and basic CLI usage for dependency management and Drush commands.
- Git: version control for theme and site configuration changes.
- Local development tooling: experience with Docker, DDEV, Lando, or similar helps create reproducible environments.
- Environment versions: confirm PHP and tooling match your Drupal target. (Drupal 10 requires PHP 8.1+; Drupal 9 typically runs on PHP 7.4+ or PHP 8.0+ depending on contributed modules.)
If you lack some items above, invest a few hours in a quick HTML/CSS refresher and basic PHP tutorials — they pay off when debugging Twig output and preprocess logic.
Understanding Drupal's Theming System
The Basics of Theming
A Drupal theme contains templates (Twig files), .info.yml metadata, libraries (.libraries.yml), and optional PHP for hook implementations. The theme registry resolves which template file to use based on naming conventions and suggestions. You can override core or contrib templates by placing files in your active theme.
Below is a compact visual of the request/render flow to help you reason about where templating and caching fit into the full stack.
What is Twig?
Twig is the templating engine Drupal uses to separate presentation from logic. It provides a clear syntax for output, control structures, filters, and functions while enforcing escaping by default to reduce XSS risks.
For code highlighting in documentation and editors, use class="language-twig" or class="language-markup" on <code> blocks. These classes let syntax highlighters recognize Twig syntax (curly braces, filters, tags) and markup appropriately. Use language-markup when the snippet is pure HTML; use language-twig when it contains Twig tags or expressions.
Note on versions: Drupal 9 runs with Twig 2 while Drupal 10 uses Twig 3 — template syntax is largely compatible, but check breaking changes when migrating major versions.
Twig Examples & Useful Functions
Extending a base template
Use extends and block to customize base templates. The following example preserves the base page structure and replaces the content block.
{% extends 'page.html.twig' %}
{% block content %}
<h1>{{ node.title }}</h1>
{{ content }}
{% endblock %}
Common Twig filters and functions in Drupal
|escape(implicit in Drupal): prevents XSS by escaping output|raw: outputs unescaped content (use sparingly and with validated data)|without: render a render array excluding keys (useful in templates)attributes.addClass()andattributes.removeClass(): manipulate HTML attributeslink(): builds a link from a render array or URL object
Example: using without and attributes
{# Render the node body without the links element #}
{{ content.body }}
{{ content|without('links') }}
{# Add a CSS class safely to attributes #}
<article{{ attributes.addClass('node--with-highlight') }}>
{{ content.field_custom }}
</article>
{# Remove a CSS class from attributes safely #}
<article{{ attributes.removeClass('node--teaser') }}>
{{ content.field_custom }}
</article>
Example: using the link() function
Build links in templates using link(). The examples below demonstrate a route-based link and a simple external link. Depending on the context, url() can produce a Url object for route-based links.
{# Link to the current node's canonical route (route-based Url object) #}
{{ link('Read full article', url('entity.node.canonical', {'node': node.id()})) }}
{# Simple external link (plain URL string accepted in many contexts) #}
{{ link('External docs', 'https://www.drupal.org/') }}
Extending a Base Theme (Example)
This example shows creating a minimal custom subtheme. Note: the Classy theme was commonly used in older examples but is deprecated for modern workflows; for Drupal 9/10 prefer using stable as a base or adopt the Starterkit workflow (copying starter templates into a new theme) to keep up with best practices.
Create mytheme.info.yml in themes/custom/mytheme/ (using stable as the base theme):
name: My Theme
type: theme
base theme: stable
core_version_requirement: ^9 || ^10
libraries:
- mytheme/global-styling
regions:
header: 'Header'
content: 'Content'
footer: 'Footer'
Define an asset library in mytheme.libraries.yml:
global-styling:
css:
theme:
css/style.css: {}
js:
js/script.js: {}
Place a template override at templates/node--article.html.twig:
{% extends 'node.html.twig' %}
{% block content %}
<div class="article-header">
<h2>{{ label }}</h2>
</div>
<div class="article-body">
{{ content.body }}
</div>
{% endblock %}
After adding files, clear caches and enable the theme via the admin UI or Drush.
Notes on modern alternatives:
- stable — a minimal, stable base theme suitable for building on top of; compatible with Drupal 9 and 10.
- Starterkit workflow — recommended if you want a copy-based starter that you customize; it avoids tightly coupling to an upstream base theme and is the direction many projects use for Drupal 9/10 themes.
Setting Up Your Drupal Environment for Customization
Preparing Your Development Environment
Use Composer 2 to create and manage Drupal projects. Composer manages dependencies for Drupal core and contributed modules. For local development, tools such as DDEV, Lando, or Docker-based environments are widely used in professional workflows; XAMPP/MAMP can be acceptable for quick experimentation.
Run this command to set up a new Drupal project using Composer:
composer create-project drupal/recommended-project my_site_name
Common utilities:
- Composer 2 for dependency management
- Drush 10 or 11 for command-line site management
- Docker / DDEV / Lando for reproducible local environments
Tip: confirm your environment's PHP version and extensions match Drupal's requirements for the target major version. Also verify Twig major version differences when migrating (Drupal 9 & Twig 2 vs Drupal 10 & Twig 3).
Basic Template Structure and Hierarchy in Drupal
Understanding the Template Hierarchy
The template hierarchy picks the most specific template available. Examples:
page.html.twig— base page templatenode.html.twigandnode--article.html.twig— node-level templatesblock.html.twig— block renderingviews-view.html.twig— Views output
Example of extending a page-level template:
{% extends 'page.html.twig' %}
{% block content %}
<h1>{{ node.title }}</h1>
{{ content }}
{% endblock %}
Making Your First Custom Template: A Step-by-Step Guide
Creating a Custom Template
Steps to create a custom node template for a content type named article:
- In your active theme, create
templates/node--article.html.twig. - Copy and modify markup from
node.html.twigor create a lightweight version. - Clear Drupal caches to register the new template.
Example override that adds a custom field output (this extends the base node.html.twig so the relationship is clear to beginners):
{% extends 'node.html.twig' %}
{% block content %}
<div class='custom-content'>
{{ content.field_volunteer_highlight }}
</div>
{% endblock %}
To clear caches from the command line use Drush (installed separately):
drush cr
Preprocess Hook Example
Preprocess hooks let you prepare and sanitize variables before they reach Twig templates. Below is a practical hook_preprocess_node example suitable for a theme's .theme file (or a module): it prepares boolean flags, a safe trimmed summary, and demonstrates adding cache metadata to avoid leaking personalized content.
bundle() === 'article') {
// Prepare a boolean flag for use in Twig templates.
$variables['is_featured'] = (bool) ($node->get('field_featured')->value ?? FALSE);
// Create a sanitized, trimmed summary for presentation.
$body = $node->get('body')->value ?? '';
$safe = strip_tags($body);
// Very small helper: truncate to ~200 characters safely.
if (strlen($safe) > 200) {
$safe = substr($safe, 0, 197) . '...';
}
// Use XSS filtering as a defensive measure (keeps a minimal set of tags if needed).
$variables['trimmed_summary'] = \Drupal\Component\Utility\Xss::filter($safe);
// Add cache metadata on the template renderable to avoid leaking personalized data.
$variables['#cache']['tags'][] = 'node:' . $node->id();
$variables['#cache']['contexts'][] = 'user.roles:authenticated';
// Set a conservative max-age for this example (1 hour).
$variables['#cache']['max-age'] = 3600;
}
}
Best practices demonstrated:
- Prepare typed variables (booleans/strings) so templates avoid complex logic.
- Sanitize and trim server-side to reduce template-side responsibilities.
- Always add appropriate cache metadata (tags, contexts, max-age) when exposing per-entity data.
Security & Troubleshooting
Security considerations
- Trust Twig's automatic escaping. Prefer
{{ variable }}over{{ variable|raw }}unless you have validated the content. - Avoid embedding PHP in templates — use preprocess hooks to prepare variables securely.
- Sanitize user-provided values before rendering if you must use
|raw. - Use cache contexts, tags, and max-age correctly to prevent leaking personalized content to other users.
Troubleshooting tips
- Enable Twig debugging in
sites/default/services.ymlwhen developing to see template suggestions and active template paths. - If a change doesn't appear, clear caches (
drush cr) and flush any full-page caches (e.g., Varnish) if present. - Use Xdebug (https://xdebug.org/) to step through PHP preprocess functions and spot logic errors.
- Profile template rendering and PHP execution time to identify slow preprocess functions; reduce heavy computations or move them to services.
Common errors and fixes
- Blank page after template edit: check for syntax errors in Twig — a missing tag can prevent rendering.
- Changes not applied: ensure the file name matches Drupal's naming suggestions exactly and clear caches.
- Unexpected markup or missing fields: confirm the field is present in the render array and not hidden by a module or view mode.
Best Practices and Resources for Continued Learning
Practical approaches
Work on focused, real projects (landing pages, content lists, or small brochure sites) to gain familiarity with template overrides, libraries, and preprocess hooks. Review contributed themes (base or starter themes) to learn common patterns.
- Follow DRY: extract repeated markup into smaller includes or components.
- Prefer render arrays and preprocess hooks for complex data assembly, keeping templates for presentation.
- Use theme suggestions and Twig includes to keep templates small and composable.
- Profile and cache: move heavy logic out of preprocess hooks where possible and leverage Drupal cache systems.
Authoritative resources
| Resource | Description | Link |
|---|---|---|
| Drupal.org - Theming documentation | Official theming guide, examples, and best practices maintained by the project. | https://www.drupal.org/ |
| Drupal.org - Twig and templating | Documentation and references for Twig usage within Drupal templates. | https://www.drupal.org/ |
| Drupal.org - Preprocess & theme hooks | Guidance for preprocess hooks, theme suggestions, and cache metadata. | https://www.drupal.org/ |
| Drupal Stack Exchange | Community Q&A for specific issues. | https://drupal.stackexchange.com |
| Meetup | Find local Drupal meetups and events. | https://www.meetup.com/ |
Key Takeaways
- Understand Drupal's theme layer and template hierarchy to override templates effectively.
- Use Twig for safe, readable templates. Apply filters and functions to control output and attributes.
- Prefer preprocess hooks and render arrays for data preparation, keeping Twig templates focused on presentation.
- Test templates across devices and clear caches after changes to verify results.
- Engage community resources on drupal.org and Drupal Stack Exchange for help and examples.
Frequently Asked Questions
- What is the best way to start customizing Drupal templates?
- Begin by inspecting the active theme's templates and enabling Twig debugging to see suggestion names. Modify a copy of a template (in your custom theme) and clear caches to observe changes. Start with small updates—adding CSS classes or rendering an extra field—before moving to larger structural changes.
- How do I troubleshoot issues with my Drupal template?
- If changes don't appear, clear caches first. Enable Twig debug and check the template suggestions to confirm Drupal is using the file you edited. Look for syntax errors in Twig or unrendered fields in your render arrays. Use Xdebug to trace preprocess functions if variables are missing or incorrect.
- Can I create custom templates for specific content types in Drupal?
- Yes. Create template files using naming patterns like
node--[content-type].html.twig, replacing[content-type]with the machine name. For example,node--article.html.twigtargets article content.
Conclusion
Template customization is a practical skill that improves how content is presented and how users interact with a Drupal site. By using Twig, theme inheritance, preprocess hooks, and Drupal's caching features, you can deliver clean, maintainable, and performant front-end code.
Start by building a small theme for practice, keep templates focused on presentation, and use the community and official resources to troubleshoot specific issues. Over time, these practices will make theme development faster and more predictable.