Jinja2 template engine (Part 2): a core tool for reuse and extension

📂 Stage: Stage 1 - Breaking the ice and setting sail (Basics) 🔗 Related chapters: Jinja2 模板引擎(上) · 静态文件管理


In the last article, we took advantage of the three key features of Jinja2: variable injection, loop control, and branch judgment, and were able to write dynamic rendering of a single page. But if you want to build a complete website with login status, footer, and sidebar, there is still one step left - reuse. If you copy and paste the navigation bar and footer to more than a dozen pages according to index.html, it will be a disaster if the copyright year is changed one day 😤.

This article will unlock the core gameplay of Reuse and Extension: template inheritance, fragment inclusion, custom filters and global functions, combined with three-layer inheritance techniques and built-in variables, which are enough to support the template architecture of a production environment.


1. Template inheritance: the most commonly used reuse solution ✨

1.1 Why use inheritance? No more copy-pasting!

Let’s first look at the comparison of the two project structures to feel the difference between “without inheritance” and “with inheritance”:

❶ No inheritance - nightmare mode
templates/
├── index.html      ← 复制了导航 + 页脚
├── article.html    ← 又复制了导航 + 页脚
├── profile.html    ← 还在复制导航 + 页脚
└── ...             (全站 10 个页面,改一次页脚就要改 10 个文件)

:::success ❷ Inheritance - silky mode

templates/
├── base.html       ← 只写一次导航 + 页脚 + 基础 CSS
├── index.html      ← 只写首页特有内容
├── article.html    ← 只写文章页特有内容
└── ...             (改一次页脚,base.html 里动一刀全站生效)

:::

The core of template inheritance is to extract the public skeleton of the website into a parent template, using blocks (block) marks areas where subpages can be replaced or filled.

1.2 First build the "parent template": define the shared skeleton

The parent template is usually calledbase.html, responsible for the HTML infrastructure, public navigation, footer, globally introduced CSS/JS, etc. of the entire website. we need to use{% block 块名 %}{% endblock %}Leave areas open for changes in the future.

naming tip

Block names are best made semantic, e.g.titleextra_headcontentscripts, you can know its function at a glance, making it easier for sub-templates to understand and cover it.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- ✅ 页面标题块,提供默认值,子模板可覆盖 -->
    <title>{% block title %}道满博客{% endblock %}</title>
    <!-- 全站通用样式 -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <!-- ✅ 页面专属 CSS / Meta 预留块 -->
    {% block extra_head %}{% endblock %}
</head>
<body>
    <!-- 固定导航栏,借助 Flask-Login 的 current_user 展示状态 -->
    <nav class="navbar">
        <a href="{{ url_for('index') }}">首页</a>
        <a href="{{ url_for('articles') }}">文章</a>
        <a href="{{ url_for('about') }}">关于</a>
        {% if current_user.is_authenticated %}
            <a href="{{ url_for('profile') }}">{{ current_user.name }}</a>
            <a href="{{ url_for('logout') }}">退出</a>
        {% else %}
            <a href="{{ url_for('login') }}">登录</a>
        {% endif %}
    </nav>

    <!-- 固定 Flash 消息区域,统一处理提示信息 -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flash-messages">
                {% for category, message in messages %}
                    <div class="alert alert-{{ category }}">{{ message }}</div>
                {% endfor %}
            </div>
        {% endif %}
    {% endwith %}

    <!-- ✅ 主内容块:子模板必须填充 -->
    <main class="container">
        {% block content %}{% endblock %}
    </main>

    <!-- 固定页脚 -->
    <footer>
        <p>&copy; 2026 道满博客 | <a href="https://www.daomanpy.com">道满 Python AI</a></p>
    </footer>

    <!-- ✅ 页面专属 JS 预留块 -->
    {% block scripts %}{% endblock %}
</body>
</html>

in parent templateblockThe content inside is the default value. If the subtemplate is not overridden, the default value will be displayed; if it is overridden, the content of the subtemplate will be used.

1.3 Then write the "subtemplate": fill/cover specific blocks

Subtemplates must be used in the first line{% extends "父模板路径" %}Declare the inheritance relationship and only cover what you care aboutblock. Don't write any extra HTML outside the block, otherwise Jinja2 will complain.

How to preserve parent block content?

If you want to append your own stuff to the original content of the parent block, you can call it at the beginning of the child block.{{ super() }}. For example, if a subtemplate wants to add a search box after the general navigation, it can overwrite the navigation block and first write{{ super() }}, and then append your own search form.

Let's write a sub-template for the article list page to feel the refreshingness of inheritance:

<!-- templates/articles/index.html -->
{% extends "base.html" %}

<!-- 1. 覆盖标题 -->
{% block title %}文章列表 - 道满博客{% endblock %}

<!-- 2. 补充文章页专属样式 -->
{% block extra_head %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/articles.css') }}">
{% endblock %}

<!-- 3. 填充主内容区 -->
{% block content %}
    <h1>文章列表</h1>

    {% for article in articles %}
        <article class="article-card">
            <h2><a href="{{ url_for('article', id=article.id) }}">{{ article.title }}</a></h2>
            <div class="meta">
                <span>作者:{{ article.author.name }}</span>
                <span>发布时间:{{ article.created_at.strftime('%Y-%m-%d') }}</span>
                <span>阅读:{{ article.views }}</span>
            </div>
            <p>{{ article.summary }}</p>
        </article>
    {% else %}
        <p>暂无文章,去 <a href="{{ url_for('create_article') }}">发布</a> 第一篇吧!</p>
    {% endfor %}

    <!-- 简单分页 -->
    {% if pagination %}
        <div class="pagination">
            {% if pagination.has_prev %}
                <a href="{{ url_for('articles', page=pagination.prev_num) }}">上一页</a>
            {% endif %}
            <span>第 {{ pagination.page }} / {{ pagination.pages }} 页</span>
            {% if pagination.has_next %}
                <a href="{{ url_for('articles', page=pagination.next_num) }}">下一页</a>
            {% endif %}
        </div>
    {% endif %}
{% endblock %}

Now you only have to write the really different parts of the page. Navigation, footers, error messages, and other chores are all done bybase.htmlcontract.

1.4 Advanced: Three-level inheritance (the savior of complex projects)

When the website develops to have multiple modules (such as "article module" and "user module"), and each module has its own shared sidebar or sub-navigation, a singlebase.htmlIt's not enough. At this time, you can introduce middle-level subtemplate to form a three-layer inheritance structure:

templates/
├── base.html                ← 顶层:整个站点的 HTML 骨架
├── _layout_articles.html   ← 中层:文章模块专属骨架(统一侧边栏、搜索框)
├── _layout_users.html      ← 中层:用户模块专属骨架(统一个人卡片)
├── articles/
│   ├── index.html          ← 继承 _layout_articles
│   ├── detail.html         ← 继承 _layout_articles
│   └── editor.html         ← 继承 _layout_articles
└── users/
    └── profile.html         ← 继承 _layout_users

The same is used for middle-level templates{% extends "base.html" %}, in one's ownblockIt is divided again into finer-grained blocks for continued coverage by the final page template.

Pitfall avoidance reminder

It is recommended that the inheritance level should not exceed 3 levels, otherwise it will become a headache to track where the block comes from and who it covers.


2. include: "Inserter" for independent fragments 📎

2.1 When to use include?

{% include %}It has a complementary relationship with inheritance. It is used to insert an independent small template fragment intact, suitable for:

  • Small functional components reused on multiple pages: popular article sidebar, comment area, share button;
  • Pure content block that does not need to be overridden and has no inheritance relationship.

2.2 Usage of include

First extract the independent fragments, and the file name is usually underlined._prefix, indicating that this is a "part" rather than a complete page:

<!-- templates/_hot_sidebar.html -->
<div class="sidebar">
    <h3>🔥 热门文章</h3>
    <ul>
        {% for article in hot_articles %}
            <li><a href="{{ url_for('article', id=article.id) }}">{{ article.title }}</a></li>
        {% endfor %}
    </ul>
</div>

Then import it directly where needed:

<!-- templates/index.html -->
{% extends "base.html" %}

{% block content %}
    <div class="row">
        <div class="col-8">
            <h1>最新动态</h1>
            <!-- 主要内容 -->
        </div>
        <div class="col-4">
            <!-- 直接插入热门侧边栏 -->
            {% include "_hot_sidebar.html" %}
        </div>
    </div>
{% endblock %}

The advantage of this is that if you change the style or logic of the popular article component, all pages that reference it will be updated simultaneously.


3. Custom filter: "Gadget" for processing data 🔧

Jinja2 has many built-in filters (such asuppertruncatelength), but in business it is often necessary to customize the data display format - for example, convert time into "year, month, day", or truncate text according to the number of Chinese characters. At this time, you need to write the filter yourself.

3.1 Register custom filter

Taking Flask as an example, just register it in the application factory or module file. Two methods are recommended:

# app.py
from flask import Flask
from datetime import datetime

app = Flask(__name__)

# 方式一:装饰器注册(推荐,简洁直观)
@app.template_filter("custom_date")
def format_custom_date(dt, fmt="%Y年%m月%d日 %H:%M"):
    """自定义日期显示格式"""
    if not dt:
        return ""
    # 兼容字符串格式的时间(视业务需要)
    if isinstance(dt, str):
        try:
            dt = datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
        except:
            return ""
    return dt.strftime(fmt)

@app.template_filter("truncate_chinese")
def truncate_chinese(text, max_len=50, suffix="..."):
    """中文友好的截断(按字符长度,英文单词兼容)"""
    if not text:
        return ""
    if len(text) <= max_len:
        return text
    return text[:max_len] + suffix

# 方式二:add_template_filter(适合批量注册)
def double(value):
    return value * 2
app.add_template_filter(double, "double")

3.2 Use in templates

Like the built-in filter, pass|For pipeline calls, add parentheses when passing parameters:

<p>发布时间:{{ article.created_at|custom_date }}</p>
<p>摘要:{{ article.content|truncate_chinese(100, " [查看全文]") }}</p>
<p>双倍数字:{{ 5|double }}</p>

Filters are "small tools" that should maintain a single responsibility and only convert the data display layer. Don't write complex business logic in them.


4. Custom global function: "Global Assistant" that can be called at any time 🛠️

If you want to directly call a function in the template that requires dynamic calculation and accepts parameters (such as getting the number of unread messages from the current user, getting popular tags), but you don’t want to manually query and pass it in in each view function, then it’s time to customize the global function.

4.1 Register global function

Global functions are registered toapp.jinja_env.globalsIn the dictionary, the template can be called directly as an ordinary function:

# app.py
from app.models import Tag

def get_hot_tags(limit=5):
    """返回指定数量的热门标签(假设按文章数量排序)"""
    return Tag.query.order_by(Tag.article_count.desc()).limit(limit).all()

def get_site_name():
    """从配置中返回网站名"""
    return app.config.get("SITE_NAME", "道满博客")

# 注册到全局命名空间
app.jinja_env.globals["hot_tags"] = get_hot_tags
app.jinja_env.globals["site_name"] = get_site_name

4.2 Use in templates

Just call it directly in the template, no need to pass it through the view:

<!-- 页面标题可直接引用全局函数 -->
<title>{% block title %}{{ site_name() }}{% endblock %}</title>

<!-- 在侧边栏动态获取热门标签 -->
<div class="sidebar">
    <h3>🏷️ 热门标签</h3>
    <div class="tags">
        {% for tag in hot_tags(8) %}
            <a href="{{ url_for('tag_articles', tag_id=tag.id) }}">{{ tag.name }}</a>
        {% endfor %}
    </div>
</div>

Be careful when using global functions:

  • Don't do complex calculations or check databases in global functions to avoid performance problems, try to use cache or reasonable queries.
  • Small data that can be directly provided by the view should not be made global to avoid coupling business logic at the template layer.

5. Summary and best practices

Review of core knowledge points

FunctionApplicable scenariosKey syntax/methods
Template inheritanceSharing large skeleton at the whole site/module level{% extends %}, {% block %}, {{ super() }}
include includesindependent small functional components (sidebar, etc.){% include "_xxx.html" %}
Custom filtersConvert display format to a single piece of data@app.template_filter(), {{ value|filter(arg) }}
Custom global functionDynamically calculate global data (labels, configuration)app.jinja_env.globals["name"] = func

Avoiding Pitfalls and Best Practices

  1. Inheritance level ≤ 3 levels: Maintenance costs rise sharply beyond three levels;
  2. Block name semantics: usecontentextra_headscriptsrather thanblock1block2
  3. Independent fragments are prefixed with an underscore: On the one hand, the semantics are clear, and on the other hand, Flask does not directly serve files starting with an underscore by default, which is safer;
  4. Do not abuse global variables/functions: small data that the view can pass in through the context is handed over to the view, keeping the template pure;
  5. Prefer using filters for date and string processing: Avoid stuffing a long string of Python code into the template, which will make the readability worse.

Now you have mastered the four magic weapons of template inheritance, component inclusion, custom filters and global functions, which are enough to meet the template architecture needs of most web projects. In the next article, we will go deep into static file management and front-end resource integration to completely open up the collaboration channel between the front and back ends, so stay tuned.


🔗 Extended reading