Layouts and Blocks

One base template holds the page shell; every other template fills its named blocks with extends, block and parent().

Syntax{% 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

Example · php
// 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.

Example · twig
{# 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>&copy; 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.

Example · twig
{# 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.

Example · twig
{# 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

  • Be the first to comment on this lesson.