Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Twig Inheritance and Layouts

Twig Inheritance and Layouts

~13 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Without inheritance, EVERY template would have to repeat navigation, footer, and the HTML skeleton. Twig's inheritance system solves this elegantly – EXACTLY the pattern we already anticipated for flash messages in chapter 12.

The base template: base.html.twig

templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Task Manager{% endblock %}</title>
    {% block stylesheets %}{% endblock %}
</head>
<body>
    <nav>
        <a href="{{ path('project_index') }}">Projects</a>
    </nav>

    {% for label, messages in app.flashes %}
        {% for message in messages %}
            <div class="alert alert-{{ label }}">{{ message }}</div>
        {% endfor %}
    {% endfor %}

    <main>
        {% block body %}{% endblock %}
    </main>

    {% block javascripts %}{% endblock %}
</body>
</html>

{% block name %}...{% endblock %} defines a NAMED spot that child templates can OVERRIDE. path('project_index') resolves a route name (chapter 7) to the actual URL – EXACTLY like redirectToRoute() in the controller, never hardcode a URL.

A child template: extends

templates/project/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Projects – {{ parent() }}{% endblock %}

{% block body %}
    <h1>Projects</h1>

    <ul>
        {% for project in projects %}
            <li>{{ project.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

{% extends %} MUST be the template's first line. {{ parent() }} inside a block inserts the PARENT block's CONTENT – here: Projects – Task Manager instead of completely replacing the base title.

Reusable partial templates: include

For building blocks that appear on SEVERAL pages independent of the inheritance hierarchy (e.g. a project card), {% include %} fits better than inheritance:

templates/project/_card.html.twig
<div class="project-card">
    <h3>{{ project.name }}</h3>
    <a href="{{ path('project_show', {id: project.id}) }}">Details</a>
</div>
{% for project in projects %}
    {{ include('project/_card.html.twig', {project: project}) }}
{% endfor %}

The leading underscore (_card.html.twig) is pure convention (not a Symfony requirement), but signals "this template only gets INCLUDED, not rendered directly from a controller".

path('project_show', {id: project.id}) shows how route parameters (chapter 9) get passed from Twig – as a second argument, an object literal with the placeholder names as keys.

Inheritance vs. include: a rule of thumb

MechanismUse case
extendsFor the ENTIRE page structure – ONE base, many pages fill its blocks.
includeFor REUSABLE building blocks WITHIN a page, independent of the block hierarchy – e.g. a card, a form fragment.

Including CSS and JavaScript: the asset() function

{% block stylesheets %}
    <link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}

Tipp: asset('styles/app.css') resolves the path relative to public/ and automatically respects configured "asset versioning" (cache-busting via query string) – more robust than a hardcoded /styles/app.css path.