Right, so you’ve built a base template. It’s a beautiful thing, full of your site’s <head> boilerplate, a <header> with your logo, a <footer> with your copyright (which you’ll forget to update next year). But how do you get unique content from your child templates into that rigid, beautiful frame? You yell “Hey, base template, over here!” and you throw it a block.

Think of a block as a named placeholder. You’re telling your base template, “I’m reserving this spot. Later, a child template is going to swoop in and fill it with its own specific content.” It’s the designated area where the child gets to be its own person.

You define a block in Jinja2 with the {% block block_name %} and {% endblock %} tags. Everything inside them is the default content. If a child template doesn’t override this block, this default is what gets rendered. It’s your template’s safety net.

<!-- base_template.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Amazing Site - Default Title{% endblock %}</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
    <nav>... your navigation ...</nav>

    <main>
        {% block content %}
        <!-- This is the big one. The main event. -->
        <h1>Welcome to the Default Page</h1>
        <p>If you're seeing this, a child template didn't do its job.</p>
        {% endblock %}
    </main>

    <footer>
        {% block footer %}
        &copy; 2023 My Amazing Site. All rights reserved... probably.
        {% endblock %}
    </footer>
</body>
</html>

The Naming of Blocks is a Serious Business

You can call a block almost anything (content, main_article, scripts, my_crazy_block_42), but for the love of all that is holy, be descriptive. content is classic. page_scripts is excellent for JavaScript you only want on certain pages. title is obvious but brilliant.

Here’s the kicker, and it’s a design choice by the Jinja folks that I actually adore: blocks are scoped by name, not by hierarchy. This means you can define the same block name multiple times in an inheritance chain, and the last definition wins. It’s not a nest; it’s a replacement. We’ll get to how that plays out with super() in a moment.

Providing the Default Content

The content you put inside your block definition in the base template isn’t just a sad fallback. It’s genuinely useful. That <title> default ensures every page has some title, preventing SEO-friendly “Untitled Document” nonsense. The footer default saves you from having to repeat your copyright on every single template. Use defaults for everything that’s, well, default.

The Pitfall of the Empty Block

Notice I put actual HTML inside my {% block content %} default. This is good practice. Now look at this, which is a common rookie mistake:

<!-- Don't do this -->
{% block content %}
{% endblock %}

What’s the default content here? Nothing. A whole lot of nothing. If a child template forgets to override this block, that section of the page will render… blank. Silent failure. It’s horrifying. Always provide a sensible, even if basic, default. It’s like a unit test for your template structure.

Overriding Blocks in the Child Template

This is where the magic happens. In your child template, you declare which base template you’re extending, and then you redefine any blocks you want to change. The syntax is identical.

<!-- child_template.html -->
{% extends "base_template.html" %}

{% block title %}The Actual Page Title - My Amazing Site{% endblock %}

{% block content %}
<h1>Here is my actual, page-specific content!</h1>
<p>It's so much better than that default nonsense.</p>
{% endblock %}

When Jinja renders child_template.html, it says, “Okay, you’re extending base_template.html. I’ll load that entire structure. Now, for every block you’ve defined, I’m going to ignore the base template’s version and use yours instead.” The nav, the footer block (which we didn’t override, so the default is used), the CSS—all that stays. We just swapped out the parts we wanted.

Appending, Not Replacing: Using {{ super() }}

Sometimes you don’t want to replace the base content; you want to add to it. This is most common in blocks like title or scripts. You want the site name in the <title>, but you also want the page name. You want the global JS, but also some page-specific JS.

Enter {{ super() }}. This function magic trick pulls in the content from the parent block.

{% block title %}
{{ super() }} - The Actual Page Title
{% endblock %}

This would render: My Amazing Site - Default Title - The Actual Page Title. Okay, that’s messy because our base default was verbose. Let’s fix our base to be more conducive to this:

<!-- A smarter base_template.html -->
<title>{% block title %}My Amazing Site{% endblock %}</title>

<!-- Now in the child -->
{% block title %}
{{ super() }} - The Actual Page Title
{% endblock %}

Rendered: My Amazing Site - The Actual Page Title. Perfect.

It’s equally vital for scripts. You’d hate to override your base scripts block and accidentally forget to include jQuery that the whole site relies on. {{ super() }} ensures you’re building on top of the foundation, not tearing it down and hoping you remember all the pipes.

{% block scripts %}
{{ super() }} <!-- This includes the base scripts -->
<script src="/static/js/specific-page-wizardry.js"></script>
{% endblock %}

This is the way. It makes your templates robust, modular, and far less likely to break in spectacularly silent ways. Always use {{ super() }} in blocks where the base content is necessary, not optional.