Grav Twig Inheritance — Extends, Blocks and Template Hierarchy
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
extendsandblockswork 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:
- Specific page template:
page--SLUG.html.twig(e.g.,page--home.html.twig) - Full route template:
page--FULL-ROUTE.html.twig - Page type template:
TYPE.html.twig(determined by the Markdown filename) - 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
Using
includewhereextendsis needed:includerenders a template in-place.extendsreplaces the current template's content. If you want to override parent blocks, useextends; if you want to insert a reusable fragment, useinclude.Multiple
extendsin 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.Forgetting
parent()when adding to blocks: If you override a block without{{ parent() }}, you completely replace the parent block's content. Useparent()when you want to add content instead of replacing it.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.Templates not found due to path issues: Template paths are relative to
templates/. Usepartials/base.html.twignottemplates/partials/base.html.twig.
Practice Questions
What is the difference between
{% extends %}and{% include %}? Answer:extendscreates a parent-child template relationship where the child can override parent blocks.includeinserts a template's output at the point of inclusion without block override capability.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.What is the purpose of
{% embed %}? Answer:embedcombines extends and include. It includes a template but allows you to override its blocks inline, without creating a separate child template file.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), thendefault.html.twig.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
Mini Project
Goal: Build a complete template inheritance system for a multi-section website.
- Create
partials/base.html.twigwith: 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 - Create
default.html.twigthat extends base, adds content block, sidebar block, and breadcrumb block - Create
blog.html.twigthat extends default, overrides sidebar with blog-specific widgets - Create
item.html.twigthat extends default, adds post-meta and author-bio blocks - Create
modular.html.twigthat extends base, overrides body to render modules - Create
page--home.html.twigthat extends modular with homepage-specific blocks - Create partials for header, footer, sidebar, navigation, and breadcrumbs
- Use
parent()in at least 3 blocks to add content while preserving parent output - Create an embed example in a listing template
- 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