Skip to content

Grav Twig Inheritance — Extends, Blocks and Template Hierarchy

DodaTech Updated 2026-06-27 9 min read

In this tutorial, you'll learn Grav Twig inheritance — building template hierarchies with extends and blocks, multi-level inheritance chains, embeds for reusable sections, and parent theme fallback strategies.

What You'll Learn

  • Template inheritance: how extends and blocks work together
  • Multi-level inheritance: base → layout → page type → specific page
  • Block overrides and the parent() function
  • Embed: combining extends with include flexibility
  • Template resolution order in Grav themes
  • Child theme inheritance and override strategies

Why It Matters

In WordPress, the template hierarchy is built into the system (single.php, page.php, archive.php). In Grav, you build the hierarchy yourself using Twig inheritance. This gives you complete control. A typical Grav site has 4 levels of inheritance: a base HTML template, a layout template for page structure, a content type template, and a specific page override. Understanding inheritance means you can make site-wide changes in one place and page-specific changes without duplicating entire templates.

Real-World Use

A documentation site needs a consistent layout across 500 pages — same header, footer, sidebar, and navigation. But the blog section needs a different layout (no sidebar, wider content area), and the homepage needs a completely custom layout. Using Twig inheritance, the team defines the base layout once, extends it for the blog, and creates a completely custom homepage template — all without duplicating shared HTML.

Learning Path

flowchart LR
    A["Twig Macros"] --> B["Twig Inheritance
← You are here"]:::current B --> C["Twig Debugging"] C --> D["Custom Twig Extensions"] D --> E["Theme Configuration"] E --> F["Theme Assets"] F --> G["Theme Inheritance"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Blocks — The Building Blocks of Inheritance

A block is a named section of a template that child templates can override:

{% block content %}
    <p>Default content goes here.</p>
{% endblock %}

Child templates override blocks by redefining them:

{% extends 'partials/base.html.twig' %}

{% block content %}
    <h1>{{ page.title }}</h1>
    {{ page.content|raw }}
{% endblock %}

Three-Level Inheritance

Level 1: Base Template

templates/partials/base.html.twig:

<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: 'en' }}">
<head>
    {% block head %}
    <meta charset="utf-8" />
    <title>{% block title %}{{ site.title }}{% endblock %}</title>
    {% block stylesheets %}
        {% do assets.addCss('theme://css/styles.css') %}
    {% endblock %}
    {{ assets.css() }}
    {% endblock head %}
</head>
<body class="{% block body_classes %}{% endblock %}">
    {% block header %}
        {% include 'partials/header.html.twig' %}
    {% endblock %}

    {% block body %}
    <main class="container">
        {% block content %}{% endblock %}
    </main>
    {% endblock %}

    {% block footer %}
        {% include 'partials/footer.html.twig' %}
    {% endblock %}

    {% block javascripts %}
        {% do assets.addJs('theme://js/main.js') %}
    {% endblock %}
    {{ assets.js() }}
</body>
</html>

Level 2: Layout Template

templates/default.html.twig — extends the base and adds page structure:

{% extends 'partials/base.html.twig' %}

{% block content %}
    <article class="content-wrapper">
        <h1>{{ page.title }}</h1>
        <div class="entry-content">
            {{ page.content|raw }}
        </div>
    </article>
{% endblock %}

{% block body_classes %}default-page{% endblock %}

Level 3: Content Type Template

templates/blog.html.twig — extends the layout and adds blog-specific structure:

{% extends 'default.html.twig' %}

{% block content %}
    <div class="blog-listing">
        <h1>{{ page.title }}</h1>
        {% for child in page.collection %}
            <article class="blog-post">
                <h2><a href="{{ child.url }}">{{ child.title }}</a></h2>
                <time>{{ child.date|date('F j, Y') }}</time>
                <p>{{ child.summary }}</p>
            </article>
        {% endfor %}
    </div>
{% endblock %}

{% block body_classes %}blog-page{% endblock %}

Level 4: Page-Specific Template

templates/blog.html.twig handles all blog pages. For a specific blog post, Grav uses item.html.twig:

{% extends 'default.html.twig' %}

{% block content %}
    <article class="blog-post-full">
        <h1>{{ page.title }}</h1>
        <div class="post-meta">
            <time>{{ page.date|date('F j, Y') }}</time>
            {% if page.taxonomy.tag %}
            <div class="tags">
                {% for tag in page.taxonomy.tag %}
                <span class="tag">{{ tag }}</span>
                {% endfor %}
            </div>
            {% endif %}
        </div>
        <div class="post-content">
            {{ page.content|raw }}
        </div>
    </article>
{% endblock %}

{% block body_classes %}single-post{% endblock %}

The parent() Function

The parent() function includes the parent block's content inside an override:

{% block stylesheets %}
    {{ parent() }}
    {% do assets.addCss('theme://css/blog.css') %}
{% endblock %}

This keeps the base stylesheets and adds blog-specific ones. Without parent(), the child block completely replaces the parent's content.

Multiple Inheritance

Templates can extend templates that extend other templates:

base.html.twig  (Level 1: HTML structure)
    └── default.html.twig  (Level 2: Page layout)
        └── blog.html.twig (Level 3: Blog listing)
            └── page--blog-category.html.twig (Level 4: Specific blog type)

Each level adds more specific content. The key rule: a template can only extend one parent, but that parent can extend another parent.

Embed

The {% embed %} tag combines extends and include — it lets you override blocks of an included template:

{% embed 'partials/card.html.twig' %}
    {% block card_title %}
        {{ page.title }}
    {% endblock %}
    {% block card_body %}
        {{ page.summary }}
    {% endblock %}
    {% block card_footer %}
        <a href="{{ page.url }}">Read more</a>
    {% endblock %}
{% endembed %}

This is useful when you need to use a template in a context where you want to customize its blocks without creating a new child template file.

Template Resolution Order

When Grav renders a page, it looks for templates in this order:

  1. Specific page template: page--SLUG.html.twig (e.g., page--home.html.twig)
  2. Full route template: page--FULL-ROUTE.html.twig
  3. Page type template: TYPE.html.twig (determined by the Markdown filename)
  4. Default template: default.html.twig

Use the specific template for one-off pages:

templates/page--home.html.twig:

{% extends 'partials/base.html.twig' %}

{% block content %}
    <div class="custom-homepage">
        {{ page.content|raw }}
    </div>
{% endblock %}

{% block body_classes %}homepage{% endblock %}

Block Naming Conventions

Use descriptive, hierarchical block names:

{# Good block names #}
{% block head_title %}{% endblock %}
{% block head_meta %}{% endblock %}
{% block head_stylesheets %}{% endblock %}
{% block header_logo %}{% endblock %}
{% block header_navigation %}{% endblock %}
{% block content_main %}{% endblock %}
{% block content_sidebar %}{% endblock %}
{% block footer_copyright %}{% endblock %}

{# Avoid generic names #}
{% block section1 %}{% endblock %}
{% block stuff %}{% endblock %}

Partial Templates

Partials are reusable template chunks included with {% include %}:

templates/partials/
├── header.html.twig
├── footer.html.twig
├── sidebar.html.twig
├── navigation.html.twig
└── pagination.html.twig

Partials are included, not extended:

{% include 'partials/header.html.twig' with { 'custom_var': 'value' } %}

Learning Path

flowchart LR
    A["Twig Macros"] --> B["Twig Inheritance
← You are here"]:::current B --> C["Twig Debugging"] C --> D["Custom Twig Extensions"] D --> E["Theme Configuration"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Using include where extends is needed: include renders a template in-place. extends replaces the current template's content. If you want to override parent blocks, use extends; if you want to insert a reusable fragment, use include.

  2. Multiple extends in one template: A template can only extend one parent. You cannot do {% extends 'base.html.twig' %}{% extends 'layout.html.twig' %}. Chain inheritance through multiple files instead.

  3. Forgetting parent() when adding to blocks: If you override a block without {{ parent() }}, you completely replace the parent block's content. Use parent() when you want to add content instead of replacing it.

  4. Wrong block names: Block names are case-sensitive. {% block Content %} is different from {% block content %}. Always match the exact block name from the parent template.

  5. Templates not found due to path issues: Template paths are relative to templates/. Use partials/base.html.twig not templates/partials/base.html.twig.

Practice Questions

  1. What is the difference between {% extends %} and {% include %}? Answer: extends creates a parent-child template relationship where the child can override parent blocks. include inserts a template's output at the point of inclusion without block override capability.

  2. How do you keep a parent block's content when overriding it? Answer: Call {{ parent() }} inside the child block. This outputs the parent block's content, allowing you to add content on top of it.

  3. What is the purpose of {% embed %}? Answer: embed combines extends and include. It includes a template but allows you to override its blocks inline, without creating a separate child template file.

  4. What is the template resolution order in Grav? Answer: Grav looks for: page-specific template (page--SLUG.html.twig), then full route template, then page type template (from Markdown filename), then default.html.twig.

  5. Challenge: Build a complete 4-level template hierarchy for a documentation site. Level 1: base.html.twig with head, header, body, footer blocks. Level 2: default.html.twig with content and sidebar blocks. Level 3: doc.html.twig with table of contents sidebar and breadcrumb navigation. Level 4: page--install.html.twig for the installation page with custom code blocks. Use parent() at each level. Create at least 3 partials (header, footer, sidebar). Verify that changes to the base template propagate to all child templates.

FAQ

Can a Twig template extend itself?

No. A template cannot extend itself directly. You can, however, create a circular chain (A extends B extends A) which Twig detects and throws a runtime error.

How many levels of inheritance should I use?

3-4 levels is typical. More than 5 levels becomes hard to debug. If you need deep inheritance, consider using embed or include patterns instead.

What happens if a block in the child template does not match any block in the parent?

The content in the child's unmatched block is ignored. Twig only processes blocks that correspond to named blocks in the parent template or its ancestors.

Can I define a block inside a macro?

No. Macros cannot contain blocks. Blocks are for template inheritance. If you need overrideable sections within a reusable component, use embed.

How does inheritance work with child themes?

A child theme template overrides the parent theme template at the same path. The child template can extend the parent theme's template using {% extends 'partials/base.html.twig' %}.

Mini Project

Goal: Build a complete template inheritance system for a multi-section website.

  1. Create partials/base.html.twig with: head block (title, meta, stylesheets, scripts), header block with logo and navigation, body block with main content wrapper, footer block with copyright and links, and javascripts block
  2. Create default.html.twig that extends base, adds content block, sidebar block, and breadcrumb block
  3. Create blog.html.twig that extends default, overrides sidebar with blog-specific widgets
  4. Create item.html.twig that extends default, adds post-meta and author-bio blocks
  5. Create modular.html.twig that extends base, overrides body to render modules
  6. Create page--home.html.twig that extends modular with homepage-specific blocks
  7. Create partials for header, footer, sidebar, navigation, and breadcrumbs
  8. Use parent() in at least 3 blocks to add content while preserving parent output
  9. Create an embed example in a listing template
  10. Test that all templates render correctly and verify the inheritance chain

What's Next

Now you understand Twig inheritance. Next, learn how to debug templates when things go wrong:

Continue to Lesson 18: Twig Debugging — Dump, debug bar, variables inspector, and troubleshooting techniques.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro