Django Templates 進階:模板繼承、自訂標籤與靜態檔案 | Django 教學

2026/06/05 2026/05/25
Django Templates 進階:模板繼承、自訂標籤與靜態檔案 | Django 教學

當你的 Django 專案頁面越來越多,你會發現每個模板都重複著相同的 HTML 結構 – 導航列、頁尾、CSS 引入。模板繼承(Template Inheritance)正是 Django 解決這個問題的核心機制,它讓你用 DRY 原則(Don’t Repeat Yourself)組織模板架構。本文將深入探討 {% block %}{% extends %} 的繼承機制、{% include %} 引入可重用片段、自訂 Template TagsTemplate Filters,以及 靜態檔案(Static Files)的完整處理流程,帶你打造一套結構清晰、易於維護的模板系統。

模板繼承概念:DRY 原則

在沒有模板繼承的情況下,每個 HTML 頁面都需要重複撰寫 <head>、導航列、頁尾等共用結構。當你需要修改導航列的一個連結時,就必須逐一修改所有頁面 – 這不僅費時,更容易遺漏。

Django 的模板繼承機制採用「骨架 + 可覆蓋區塊」的設計:先建立一個包含完整 HTML 結構的 基礎模板(Base Template),在其中用 {% block %} 標籤定義可覆蓋的區域,子模板再透過 {% extends %} 繼承父模板並覆寫需要變動的區塊。

base.html(基礎骨架)
├── {% block title %}{% endblock %}
├── {% block extra_css %}{% endblock %}
├── {% block content %}{% endblock %}
└── {% block extra_js %}{% endblock %}
    ↓ extends
page.html(子模板)
├── {% extends "base.html" %}
├── {% block title %}文章列表{% endblock %}
└── {% block content %}...{% endblock %}

這樣一來,所有頁面共用的 HTML 結構只需要維護一份,而每個頁面只需要關注自己特有的內容。


base.html 基礎模板設計

一個設計良好的 base.html 應該涵蓋完整的 HTML 骨架,並預留足夠的 {% block %} 讓子模板可以彈性覆寫:

{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的部落格{% endblock %} | MyBlog</title>
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    {# 導航列 #}
    <nav class="navbar">
        <a href="{% url 'home' %}" class="logo">MyBlog</a>
        <ul>
            <li><a href="{% url 'post_list' %}">文章</a></li>
            <li><a href="{% url 'about' %}">關於</a></li>
            {% if user.is_authenticated %}
                <li><a href="{% url 'logout' %}">登出({{ user.username }})</a></li>
            {% else %}
                <li><a href="{% url 'login' %}">登入</a></li>
            {% endif %}
        </ul>
    </nav>

    {# 訊息提示 #}
    {% if messages %}
    <div class="messages">
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">{{ message }}</div>
        {% endfor %}
    </div>
    {% endif %}

    {# 主要內容區 #}
    <main class="container">
        {% block content %}{% endblock %}
    </main>

    {# 頁尾 #}
    <footer>
        <p>&copy; {% now "Y" %} MyBlog. All rights reserved.</p>
    </footer>

    <script src="{% static 'js/main.js' %}"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

{% block %} 定義可覆蓋區域

{% block %} 標籤定義了一個具名區塊,子模板可以選擇性地覆寫它。幾個設計要點:

  • block 命名要語義化:使用 titlecontentextra_csssidebar 等有意義的名稱
  • 提供預設內容{% block title %}我的部落格{% endblock %} 讓未覆寫的子模板也能正常顯示
  • 預留擴充點extra_cssextra_js 讓子模板可以載入額外的資源

{% extends %} 繼承父模板

子模板使用 {% extends %} 宣告繼承關係,並只覆寫需要變動的 block:

{# templates/blog/post_list.html #}
{% extends "base.html" %}
{% load static %}

{% block title %}文章列表{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
{% endblock %}

{% block content %}
<h1>所有文章</h1>
<div class="post-grid">
    {% for post in posts %}
    <article class="post-card">
        <h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h2>
        <p class="meta">{{ post.created_at|date:"Y年m月d日" }} | {{ post.author }}</p>
        <p>{{ post.content|truncatewords:30 }}</p>
    </article>
    {% empty %}
    <p>目前沒有文章。</p>
    {% endfor %}
</div>
{% endblock %}

繼承規則提醒

  • {% extends %} 必須是模板的第一個標籤,前面不能有任何內容(包括空白行)
  • 子模板中只有 {% block %} 內的內容會被渲染,block 之外的內容會被忽略
  • 使用 {{ block.super }} 可以保留父模板該 block 的原始內容,再追加新內容
{# 使用 block.super 保留父模板內容 #}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/post_detail.css' %}">
{% endblock %}

多層繼承

實務上,我們常會設計三層繼承結構來區分不同的頁面佈局(Layout):

base.html            → 全站 HTML 骨架(head、navbar、footer)
├── blog_base.html   → 部落格佈局(含側邊欄)
│   ├── post_list.html
│   └── post_detail.html
└── page_base.html   → 單頁佈局(全寬,無側邊欄)
    ├── about.html
    └── contact.html
{# templates/blog/blog_base.html — 第二層:部落格佈局 #}
{% extends "base.html" %}

{% block content %}
<div class="blog-layout">
    <div class="blog-main">
        {% block blog_content %}{% endblock %}
    </div>
    <aside class="blog-sidebar">
        {% block sidebar %}
            {% include "partials/sidebar.html" %}
        {% endblock %}
    </aside>
</div>
{% endblock %}
{# templates/blog/post_detail.html — 第三層:文章詳情頁 #}
{% extends "blog/blog_base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block blog_content %}
<article>
    <h1>{{ post.title }}</h1>
    <time>{{ post.created_at|date:"Y年m月d日 H:i" }}</time>
    <div class="post-body">
        {{ post.content|safe }}
    </div>
</article>
{% endblock %}

建議不要超過三層繼承,過多層次會讓模板的 block 追蹤變得困難。


{% include %} 引入可重用片段

{% include %} 將另一個模板檔案的內容嵌入到當前位置,非常適合重複使用的 UI 元件(Component):

{# templates/partials/navbar.html #}
<nav class="navbar">
    <a href="{% url 'home' %}" class="logo">MyBlog</a>
    <ul>
        <li><a href="{% url 'post_list' %}">文章</a></li>
        <li><a href="{% url 'about' %}">關於</a></li>
    </ul>
</nav>
{# templates/partials/sidebar.html #}
<div class="sidebar">
    <h3>熱門文章</h3>
    <ul>
        {% for post in popular_posts %}
            <li><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></li>
        {% endfor %}
    </ul>

    <h3>標籤</h3>
    <div class="tag-cloud">
        {% for tag in tags %}
            <a href="{% url 'tag_posts' slug=tag.slug %}" class="tag">{{ tag.name }}</a>
        {% endfor %}
    </div>
</div>
{# templates/partials/footer.html #}
<footer>
    <p>&copy; {% now "Y" %} MyBlog. All rights reserved.</p>
</footer>

base.html 中使用 {% include %} 引入這些片段:

<body>
    {% include "partials/navbar.html" %}

    <main class="container">
        {% block content %}{% endblock %}
    </main>

    {% include "partials/footer.html" %}
</body>

你也可以透過 with 關鍵字傳遞變數給被引入的模板:

{# 傳遞變數給 include 的模板 #}
{% include "partials/sidebar.html" with popular_posts=top_posts tags=all_tags %}

自訂模板標籤(Custom Template Tags)

當內建的標籤無法滿足需求時,你可以建立自訂模板標籤(Custom Template Tags)。

建立 templatetags 目錄

首先,在你的 app 目錄下建立 templatetags 套件:

myapp/
├── templatetags/          # 建立此目錄
│   ├── __init__.py        # 必須有此檔案,讓 Python 識別為套件
│   └── blog_tags.py       # 自訂標籤模組
├── models.py
├── views.py
└── ...

注意:建立 templatetags 目錄後,需要重新啟動開發伺服器,Django 才會偵測到新的模組。

simple_tag – 簡單標籤

simple_tag 接受參數並回傳一個值,可以直接輸出或存入模板變數:

# myapp/templatetags/blog_tags.py
from django import template
from myapp.models import Post, Tag

register = template.Library()


@register.simple_tag
def total_posts():
    """回傳已發布的文章總數"""
    return Post.objects.filter(is_published=True).count()


@register.simple_tag(takes_context=True)
def user_post_count(context):
    """回傳當前登入使用者的文章數"""
    user = context['request'].user
    if user.is_authenticated:
        return Post.objects.filter(author=user).count()
    return 0

在模板中使用:

{% load blog_tags %}

{# 直接輸出 #}
<p>目前共有 {% total_posts %} 篇文章</p>

{# 存入變數再使用 #}
{% total_posts as post_count %}
{% if post_count > 100 %}
    <p>我們已經累積超過 100 篇文章了!</p>
{% endif %}

{# 帶 context 的標籤 #}
<p>你發表了 {% user_post_count %} 篇文章</p>

inclusion_tag – 包含標籤

inclusion_tag 會渲染一個子模板並將結果嵌入,最適合可重用的 UI 元件:

# myapp/templatetags/blog_tags.py

@register.inclusion_tag('partials/popular_posts.html')
def show_popular_posts(count=5):
    """顯示熱門文章元件"""
    posts = Post.objects.filter(
        is_published=True
    ).order_by('-view_count')[:count]
    return {'popular_posts': posts}


@register.inclusion_tag('partials/tag_cloud.html')
def show_tag_cloud():
    """顯示標籤雲元件"""
    tags = Tag.objects.all()
    return {'tags': tags}
{# templates/partials/popular_posts.html #}
<div class="popular-posts">
    <h3>熱門文章</h3>
    <ul>
        {% for post in popular_posts %}
            <li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
        {% endfor %}
    </ul>
</div>
{# 在任何模板中使用 #}
{% load blog_tags %}
{% show_popular_posts 3 %}
{% show_tag_cloud %}

自訂過濾器(Custom Template Filters)

過濾器(Filter)用於對模板變數進行轉換處理,使用管線符號 | 串接:

# myapp/templatetags/blog_filters.py
from django import template
from django.utils.safestring import mark_safe
import markdown as md

register = template.Library()


@register.filter(name='reading_time')
def reading_time(content):
    """估算文章閱讀時間(假設每分鐘閱讀 500 個中文字)"""
    char_count = len(content)
    minutes = max(1, round(char_count / 500))
    return f'{minutes} 分鐘'


@register.filter(name='markdown')
def markdown_to_html(value):
    """將 Markdown 文字轉換為 HTML"""
    return mark_safe(md.markdown(value, extensions=['extra', 'codehilite']))


@register.filter(name='truncate_chars_zh')
def truncate_chars_zh(value, length=50):
    """中文友善的字元截斷"""
    if len(value) <= length:
        return value
    return value[:length] + '...'

在模板中使用:

{% load blog_filters %}

<p>閱讀時間:{{ post.content|reading_time }}</p>
<div class="post-body">{{ post.content|markdown }}</div>
<p>{{ post.content|truncate_chars_zh:100 }}</p>

靜態檔案處理(Static Files)

靜態檔案(Static Files)是指 CSS、JavaScript、圖片等不會隨請求變動的資源。Django 提供了完善的靜態檔案管理機制。

{% load static %} 與 {% static %} 標籤

在模板中引用靜態檔案,必須先載入 static 標籤庫:

{% load static %}

{# 引用 CSS #}
<link rel="stylesheet" href="{% static 'css/style.css' %}">

{# 引用 JavaScript #}
<script src="{% static 'js/app.js' %}"></script>

{# 引用圖片 #}
<img src="{% static 'images/logo.png' %}" alt="Logo">

{% static %} 標籤會根據 settings.py 中的 STATIC_URL 設定自動生成正確的 URL 路徑,讓你不需要寫死路徑。

STATICFILES_DIRS 設定

settings.py 中配置靜態檔案的搜尋路徑:

# settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# 靜態檔案的 URL 前綴
STATIC_URL = '/static/'

# 額外的靜態檔案搜尋目錄(開發時使用)
STATICFILES_DIRS = [
    BASE_DIR / 'static',          # 專案根目錄下的 static 資料夾
    BASE_DIR / 'assets',          # 也可以加入其他資料夾
]

# 正式環境:collectstatic 收集靜態檔案的目標目錄
STATIC_ROOT = BASE_DIR / 'staticfiles'

推薦的靜態檔案目錄結構:

my_django_project/
├── static/                   # 全站共用的靜態檔案
│   ├── css/
│   │   └── main.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       └── logo.png
├── myapp/
│   └── static/              # App 專屬的靜態檔案
│       └── myapp/           # 加一層 app 名稱避免命名衝突
│           ├── css/
│           │   └── blog.css
│           └── js/
│               └── blog.js
└── ...

collectstatic 命令

部署到正式環境(Production)時,需要執行 collectstatic 命令,將所有靜態檔案收集到 STATIC_ROOT 指定的目錄中:

# 收集所有靜態檔案到 STATIC_ROOT
python manage.py collectstatic

# 輸出範例:
# 128 static files copied to '/path/to/staticfiles'.

收集後的檔案通常由 NginxCDN(內容傳遞網路,Content Delivery Network)等專門的靜態檔案伺服器提供服務,而不是由 Django 處理,這樣能大幅提升效能。


Jinja2 整合簡介

Django 預設使用自己的模板引擎 DTL(Django Template Language),但也支援整合 Jinja2 – 另一個功能更強大的 Python 模板引擎。Jinja2 允許在模板中執行更多 Python 表達式,適合需要複雜模板邏輯的場景。

settings.py 中同時配置兩個模板引擎:

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [BASE_DIR / 'jinja2_templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'environment': 'myapp.jinja2_env.environment',
        },
    },
]

建立自訂的 Jinja2 環境設定:

# myapp/jinja2_env.py
from jinja2 import Environment
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse


def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
    })
    return env

大多數 Django 專案使用 DTL 就已足夠。如果你的團隊已經熟悉 Jinja2,或者需要在模板中執行複雜的表達式運算,再考慮整合 Jinja2。


完整範例:具有導航列、內容區、側邊欄的部落格模板

讓我們綜合以上所有知識,建立一個完整的部落格模板架構。

專案結構

templates/
├── base.html                    # 第一層:全站骨架
├── blog/
│   ├── blog_base.html           # 第二層:部落格佈局(含側邊欄)
│   ├── post_list.html           # 第三層:文章列表頁
│   └── post_detail.html         # 第三層:文章詳情頁
└── partials/
    ├── navbar.html              # 導航列元件
    ├── footer.html              # 頁尾元件
    └── sidebar.html             # 側邊欄元件

第一層:base.html

{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}首頁{% endblock %} | MyBlog</title>
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    {% include "partials/navbar.html" %}

    {% if messages %}
    <div class="messages">
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">{{ message }}</div>
        {% endfor %}
    </div>
    {% endif %}

    <main>
        {% block content %}{% endblock %}
    </main>

    {% include "partials/footer.html" %}

    <script src="{% static 'js/main.js' %}"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

第二層:blog_base.html

{# templates/blog/blog_base.html #}
{% extends "base.html" %}
{% load blog_tags %}

{% block content %}
<div class="blog-layout">
    <section class="blog-main">
        {% block blog_content %}{% endblock %}
    </section>
    <aside class="blog-sidebar">
        {% block sidebar %}
            {% show_popular_posts 5 %}
            {% show_tag_cloud %}
        {% endblock %}
    </aside>
</div>
{% endblock %}

第三層:post_list.html

{# templates/blog/post_list.html #}
{% extends "blog/blog_base.html" %}
{% load blog_tags blog_filters %}

{% block title %}文章列表{% endblock %}

{% block blog_content %}
<h1>所有文章</h1>
{% total_posts as count %}
<p>共 {{ count }} 篇文章</p>

{% for post in posts %}
<article class="post-card">
    <h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h2>
    <div class="post-meta">
        <time>{{ post.created_at|date:"Y年m月d日" }}</time>
        <span>{{ post.content|reading_time }} 閱讀</span>
    </div>
    <p>{{ post.content|truncate_chars_zh:150 }}</p>
</article>
{% empty %}
<p>目前沒有任何文章。</p>
{% endfor %}

{# 分頁 #}
{% if is_paginated %}
<nav class="pagination">
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">上一頁</a>
    {% endif %}
    <span>第 {{ page_obj.number }} 頁,共 {{ page_obj.paginator.num_pages }} 頁</span>
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">下一頁</a>
    {% endif %}
</nav>
{% endif %}
{% endblock %}

第三層:post_detail.html

{# templates/blog/post_detail.html #}
{% extends "blog/blog_base.html" %}
{% load blog_filters %}

{% block title %}{{ post.title }}{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/highlight.css' %}">
{% endblock %}

{% block blog_content %}
<article class="post-detail">
    <header>
        <h1>{{ post.title }}</h1>
        <div class="post-meta">
            <span>作者:{{ post.author.get_full_name|default:post.author.username }}</span>
            <time>{{ post.created_at|date:"Y年m月d日 H:i" }}</time>
            <span>{{ post.content|reading_time }} 閱讀</span>
        </div>
    </header>

    <div class="post-body">
        {{ post.content|markdown }}
    </div>

    <div class="post-tags">
        {% for tag in post.tags.all %}
            <a href="{% url 'tag_posts' slug=tag.slug %}" class="tag">{{ tag.name }}</a>
        {% endfor %}
    </div>
</article>
{% endblock %}

{% block extra_js %}
<script src="{% static 'js/highlight.js' %}"></script>
{% endblock %}

總結

本文從 DRY 原則 出發,系統性地介紹了 Django 模板系統的進階功能。透過 {% extends %}{% block %} 建立 模板繼承 架構,用 {% include %} 拆分可重用的 UI 元件,再以自訂 Template TagsTemplate Filters 擴充模板的表達能力。在靜態檔案處理方面,我們學會了使用 {% static %} 標籤引用資源、配置 STATICFILES_DIRS 管理搜尋路徑,以及透過 collectstatic 命令為正式環境做準備。

掌握這些進階模板技巧後,你就能設計出結構清晰、高度可維護的模板架構 – 從三層繼承的佈局系統到模組化的 UI 元件,讓團隊中的前端設計師和後端開發者都能高效率地協作。在下一篇文章中,我們將進入 Django Forms 的世界,學習如何處理表單的建立、驗證與渲染。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。