Layouts and Blocks
One base template holds the page shell; every other template fills its named blocks with extends, block and parent().
{% extends 'base.html.twig' %} ... {% block content %}{% endblock %}Every page shares a doctype, a <head>, a header and a footer. Copying them into each template means changing every file when the navigation changes. Twig solves this with inheritance, which works in the opposite direction from including a header and a footer: the shell is a complete template with holes in it, and each page fills the holes.
extends and block
A block is a named region declared with {% block name %}...{% endblock %}. Its contents are the default. A template that declares {% extends 'base.html.twig' %} is rendering the base template, but with its own versions of any blocks it redeclares. A template may extend only one parent, and {% extends %} belongs at the top.
The shorthand {% block title 'Articles' %} is the same as {% block title %}Articles{% endblock %}. Naming the end tag, {% endblock title %}, is optional and helps in a long file.
parent() and block()
{{ parent() }} renders what the parent's version of this block contained, so a page can add to a block instead of replacing it. That is how a page adds one stylesheet without dropping the site's own. {{ block('title') }} prints another block of the current render, which is useful for showing the page title in both the <title> and an <h1>.
Three levels
A common arrangement is base.html.twig for the shell, a layout.html.twig per section for structure such as a sidebar, and one template per page. Each level only overrides what it changes.
Example
// src/Controller/PageController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PageController extends AbstractController
{
// GET /about returns 200. The body is base.html.twig in full, with the
// blocks that page/about.html.twig overrides substituted in.
#[Route('/about', name: 'app_about', methods: ['GET'])]
public function about(): Response
{
return $this->render('page/about.html.twig', [
'team_size' => 4,
]);
}
// GET /contact returns 200. Both templates extend the same base, so the
// <head>, header and footer exist in exactly one file.
#[Route('/contact', name: 'app_contact', methods: ['GET'])]
public function contact(): Response
{
// The second argument defaults to an empty array.
return $this->render('page/contact.html.twig');
}
}When to use it
- Change the site navigation in one file instead of in every template.
- Add a page-specific stylesheet without losing the global one.
- Give a section of the site a shared sidebar that individual pages can override.
More examples
The base template
This is the only file that knows what a page looks like. Note that stylesheets has a default so a page that ignores it still gets the site CSS, while content is empty because a page with no content is a mistake. Create this file first in a new project and extend it from then on.
{# templates/base.html.twig #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{# The content between the tags is the default if nobody overrides it. #}
<title>{% block title %}Acme{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}
</head>
<body>
<header>
<a href="{{ path('app_about') }}">About</a>
<a href="{{ path('app_contact') }}">Contact</a>
</header>
{# Declared empty on purpose: every page is expected to fill it. #}
<main>{% block content %}{% endblock %}</main>
<footer>© Acme</footer>
</body>
</html>A page that adds to a block instead of replacing it
Omitting parent() in the stylesheets block is the usual cause of a page that loses the site's styling. block('title') keeps the <title> and the <h1> in step with one change. Write a page this way whenever it needs an extra asset or a heading that matches its title.
{# templates/page/about.html.twig #}
{% extends 'base.html.twig' %}
{# Shorthand: the same as {% block title %}About us{% endblock %} #}
{% block title 'About us' %}
{% block stylesheets %}
{# Keep the parent's stylesheet, then add one for this page. #}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('styles/about.css') }}">
{% endblock %}
{% block content %}
{# Reuse the title block rather than repeating the string. #}
<h1>{{ block('title') }}</h1>
<p>We are a team of {{ team_size }}.</p>
{% endblock content %}A middle layout for one section
The middle template fills base's content block and opens its own, so every article page gets the sidebar without repeating it. Add a level like this when a group of pages shares structure that the rest of the site does not, and stop at three levels - deeper chains get hard to follow.
{# templates/article/layout.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
<div>
<aside>
{% block sidebar %}
<h2>Categories</h2>
{% endblock %}
</aside>
{# A new hole, for pages that extend this layout rather than base. #}
<section>{% block article_content %}{% endblock %}</section>
</div>
{% endblock %}
{# templates/article/show.html.twig #}
{% extends 'article/layout.html.twig' %}
{% block title %}{{ article.title }}{% endblock %}
{% block article_content %}
<h1>{{ article.title }}</h1>
{% endblock %}
Discussion