Djangoのテンプレートは、書き方そのものより「どこまでをテンプレートに持たせるか」でコードの寿命が決まります。Djangoとは?Python製Webフレームワークの特徴・MVT・始め方を入門解説で触れているとおり、MVTのTemplateは表示だけを受け持つ層。ところが実装では、別アプリの同名テンプレートが先に読まれた、変数が黙って空文字になった、JavaScriptへ値を渡した瞬間に画面が壊れた、といった詰まり方をします。ここでは6.1系を前提に、構文と継承の基本から、コンテキストの受け渡し、JSへの値渡し、Jinja2への差し替え判断までを実装の順で整理します。
まとめ:テンプレートは表示専用と割り切るほど壊れにくい
- テンプレートの探索は DIRS が先で APP_DIRS が後。アプリ名のディレクトリを1段挟まないと、別アプリの同名ファイルが先に当たります。
- 存在しない変数はエラーになりません。既定では空文字に置き換わって消えるため、原因の切り分けには設定を一時的に変える必要があります。
- テンプレートでは引数付きのメソッドを呼べません。この制約を「表示に必要な値はviewで確定させる」という設計指針として使います。
- 継承は extends と block、部品化は include。include は別のコンテキストで評価されるため、with と only で渡す値を絞ります。
- JavaScriptへ値を渡すときは script タグへ直接書かず json_script を通します。文字列連結は閉じタグの混入で壊れます。
- Jinja2への差し替えは、url・static・csrf_token を自前で登録し直す移植量に見合うかどうかで決めます。
テンプレートの探索順序とTEMPLATES設定で最初に決めること
テンプレートを1枚追加しただけなのに別の画面が表示された、という事故の大半はここが原因です。Djangoはテンプレート名を受け取ると、設定された順に探索して最初に見つかった1枚を返します。名前が衝突していても警告は出ません。
DIRSとAPP_DIRSのどちらに置くかで探索の順序が決まる
settings.py の TEMPLATES は、バックエンドごとの設定をリストで持ちます。プロジェクト共通のテンプレートは DIRS、アプリに紐づくものは APP_DIRS の探索に任せる形が標準構成です。
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
探索は DIRS に並べた順から始まり、そのあとで INSTALLED_APPS の順にアプリの templates ディレクトリを見ます。つまりアプリ側のテンプレートを上書きしたい場合、DIRS に同名で置けば勝てるということ。逆に言えば、アプリ側だけを直したつもりが DIRS の古いファイルに阻まれる事故も同じ仕組みで起きます。
テンプレート名の衝突はアプリ名のサブディレクトリを挟んで避ける
アプリごとに templates/list.html を置くと、先に読まれたアプリのファイルが全画面で使われます。これを避けるため、アプリ名のサブディレクトリを1段挟むのが定石です。
orders/
templates/
orders/
list.html
customers/
templates/
customers/
list.html
viewからは render(request, "orders/list.html", ctx) のようにアプリ名込みで指定します。冗長に見えますが、この1段の有無で、アプリを増やしたときの事故率が変わります。
テンプレート言語の構文:変数・フィルタ・タグと自動エスケープ
Djangoテンプレート言語の要素は4つだけです。値を出す {{ }}、制御を書く {% %}、値を整形するフィルタ |、テンプレート内に残るコメント {# #}。Pythonの式をそのまま書ける言語ではなく、意図的に機能が絞られています。
変数の解決は辞書・属性・添字の順で試され失敗しても止まらない
{{ user.name }} と書いたとき、Djangoは辞書のキー参照、属性またはメソッドの参照、数値添字の参照を順に試します。辞書でも属性でも同じドット記法で書けるのはこのためです。
注意すべきは失敗時の挙動です。どれも当たらなかった場合、テンプレートは例外を投げず string_if_invalid(既定は空文字)に置き換えます。タイプミスした変数名は画面から静かに消えるだけで、ログにも残りません。テンプレートを何度読んでも原因が見えないのは、この仕様が理由です。
テンプレートで書けるのは整形までで引数付きメソッドは呼べない
テンプレートからメソッドを呼ぶことはできますが、括弧を書いて引数を渡す構文がありません。{{ order.total_with_tax }} のような無引数のメソッドやプロパティは動く一方、{{ order.total(0.1) }} は解釈されません。
これは制限ではなく設計です。表示の直前に値を計算しはじめると、テンプレートを読んでもクエリ本数が見えなくなります。引数が要る計算はviewかモデルのメソッド側で確定させ、テンプレートには確定済みの値だけを渡す。この線引きが、後述するコンテキスト設計とも噛み合います。
自動エスケープは既定で有効なのでsafeの使用箇所を絞り込む
変数を出力するとき、Djangoは <・>・&・引用符をHTMLエンティティへ変換します。ユーザー入力をそのまま表示してもスクリプトが動かないのは、この既定のおかげ。
エスケープを止める手段は safe フィルタと {% autoescape off %} の2つです。公式には、safe で印を付けた文字列も後続のフィルタが適用されると安全でない状態へ戻り得ると明記されています。管理画面から入力されたHTMLを出す要件があるなら、保存時にサニタイズしたうえで出力箇所を数えられる数に絞るのが現実的な運用です。標準で防げる範囲と設定ミスで空く穴の境界はDjangoの脆弱性への対応|セキュリティリリースの追い方と標準で防げない穴で整理しています。
| フィルタ | 用途 | 注意点 |
|---|---|---|
| default | 偽の値を差し替える | 空文字や0も対象 |
| default_if_none | Noneだけ差し替える | 空文字は残る |
| date | 日時の書式指定 | 書式文字は独自記法 |
| safe | エスケープ除外 | 後続フィルタで戻る |
| json_script | JSへ値を渡す | script要素で出力 |
extendsとblockでの継承とincludeを使い分ける基準
共通のヘッダーやサイドバーを1枚に集めるのが継承、繰り返し使う小さな部品を切り出すのが include。両者は似て見えますが、コンテキストの扱いが違います。
extendsは先頭に1つだけ書きblockで差し替え口を用意する
親テンプレートに {% block %} で穴を開け、子テンプレートが {% extends %} で継承して穴を埋める形。{% extends %} はテンプレートの最初のタグでなければならず、1枚につき1つだけです。
{# templates/base.html #}
<!DOCTYPE html>
<html lang="ja">
<head>
<title>{% block title %}受注管理{% endblock %}</title>
</head>
<body>
{% include "_nav.html" %}
<main>{% block content %}{% endblock %}</main>
</body>
</html>
{# templates/orders/list.html #}
{% extends "base.html" %}
{% block title %}受注一覧 | {{ block.super }}{% endblock %}
{% block content %}
<h1>受注一覧</h1>
{% for order in orders %}
<p>{{ order.code }} - {{ order.total|default:"0" }}</p>
{% empty %}
<p>該当する受注はありません</p>
{% endfor %}
{% endblock %}
{% for %} に {% empty %} を添えておくと、0件時の分岐をviewへ持ち出さずに済みます。一覧画面の検索条件をフォームと連動させる実装はdjango-filterで一覧検索を実装する|FilterSet設計とDRF指定の判断で扱っています。
block.superで親の中身を残したまま子で追記する書き方
子で {% block %} を書くと親の中身は置き換わります。親の内容を残して前後に足したいときは {{ block.super }} を挟みます。上の例では、タイトルが「受注一覧 | 受注管理」となる形。
継承の段数は3層までに収めるのが扱いやすい範囲です。サイト全体の base.html、セクション単位の base_orders.html、個別画面という構成なら、どのブロックがどこで埋まるかを追えます。4層を超えると、開いているブロックを探すだけで時間を使うようになります。
includeは共通部品の再利用でwithとonlyで渡す値を絞る
{% include %} は別のテンプレートをその場で描画します。既定では現在のコンテキストをそのまま引き継ぐため、呼び出し元によって中身が変わる部品ができあがってしまう。これを避けるのが with と only です。
{% include "_badge.html" with label=order.status color="red" only %}
only を付けると、with で明示した値だけが部品へ渡ります。部品の依存が引数として読み取れるようになり、呼び出し元を変えたときの影響範囲も限定できます。共通部品を切り出すときは、only を付ける前提で設計してください。
テンプレート側に編集可能な領域を宣言し、そこへCMSの部品を差し込む構成にするなら、django CMSとは:Page・Placeholder・pluginの構造と後付け判断のPlaceholderの仕組みが参考になります。
viewからテンプレートへコンテキストを渡す3つの経路と名前の衝突
テンプレートに値が届く経路は3つあります。どれも同じ辞書へ流れ込むため、名前が重なったときの優先順位を知らないと「渡したはずの値が出ない」状態になります。
renderとTemplateViewとcontext_processorsの3経路
関数ベースビューでは render() の第3引数、クラスベースビューでは get_context_data() の戻り値、全画面共通の値は context_processors が担当します。
from django.shortcuts import render
from .models import Order
def order_list(request):
orders = Order.objects.select_related("customer")[:50]
return render(request, "orders/list.html", {"orders": orders})
from django.views.generic import ListView
from .models import Order
class OrderListView(ListView):
model = Order
template_name = "orders/list.html"
context_object_name = "orders"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["unpaid_count"] = Order.objects.filter(paid=False).count()
return ctx
クラスベースビューを使う場合、context_object_name を指定しないと一覧は object_list という名前で届きます。テンプレート側の名前が実装依存になるため、明示しておくほうが後から読みやすくなります。
コンテキストの名前が重なったときにどの値が勝つのかの優先順位
優先されるのはviewが渡した値です。context_processors が用意する値と同じキーをviewで設定すると、view側で上書きされます。user や request といった共通名を業務用の変数に使うと、認証まわりの表示が静かに壊れる原因になります。
共通の値を足すときは、site_settings のように接頭辞を付けた名前で context_processors に登録しておくと衝突を避けられます。プロジェクト独自の context_processors は毎リクエストで実行される点にも注意が必要で、ここでクエリを1本足すと全画面のコストになります。
重い処理をテンプレートに持ち込まずviewの側で確定させておく
テンプレートで {{ order.items.all }} のようにリレーションを辿ると、ループの回数だけクエリが飛びます。テンプレートは表示のたびに評価されるため、ここが性能問題の温床になりやすいところ。
対処は単純で、viewの側で select_related や prefetch_related を付けて評価済みの状態にしてから渡します。QuerySetの遅延評価がいつ確定するのか、集計をどの順で書くべきかはDjango ORMのクエリ設計|遅延評価と集計の順序・生SQLへ落とす判断で整理しています。テンプレートを読んだだけでクエリ本数が見積もれる状態を保つことが、この層の設計目標です。
JavaScriptへ値を渡すときにjson_scriptを使う理由と実装
サーバー側で組み立てた画面に少しだけJavaScriptを足す構成では、Python側の値をJS側へ渡す必要が出てきます。ここが、テンプレート実装でもっとも壊れやすい箇所です。
scriptタグへ直接埋め込む書き方が壊れる2つの経路と原因
今も見かけるのが、<script> の中へ変数を直接書く方法です。
{# 壊れる書き方 #}
<script>
var items = {{ items }};
var title = "{{ order.title }}";
</script>
この書き方は2つの経路で壊れます。1つめは、値に </script> という文字列が含まれた場合。ブラウザはHTMLとして先に解釈するため、そこでスクリプトが打ち切られ、以降が本文として描画されます。2つめは、Pythonの辞書表現がJavaScriptのオブジェクトリテラルと一致しない点で、None や True がそのまま出れば構文エラーになります。
さらに、値をエスケープしようとして safe を付けると、今度は入力された文字列がスクリプトとして実行される経路が開きます。文字列連結でJSを組み立てる限り、安全側と動作側のどちらかが必ず犠牲になる構造です。
json_scriptの出力とJavaScript側での受け取り方の実装
Djangoが用意している解答が json_script フィルタです。値をJSONへ変換し、type="application/json" の script 要素として出力します。
{# テンプレート側 #}
{{ items|json_script:"items-data" }}
{# 出力されるHTML #}
<script id="items-data" type="application/json">
[{"id": 1, "name": "<管理>"}]
</script>
// JavaScript側
const items = JSON.parse(
document.getElementById("items-data").textContent
);
安全性の根拠は、<・>・& をJSON文字列のユニコードエスケープへ置き換える点にあります。ブラウザが閉じタグとして解釈できる文字列が出力に残らないため、値の中身によらず要素の境界が崩れません。type がJSONなのでスクリプトとして実行されることもなく、JS側は明示的に JSON.parse したときだけ値を受け取ります。
フィルタの引数(id属性)は省略できますが、省略するとJS側から要素を特定しづらくなるため、実務ではidを付けて呼ぶ形が扱いやすいところ。処理そのものは外部のJSファイルへ置き、テンプレートにはデータブロックだけを残します。
日付やDecimalも通るがHTML断片を渡す用途には使わない
JSONへの変換にはDjangoのエンコーダが使われるため、datetime や Decimal、UUID もそのまま渡せます。日時はISO 8601形式の文字列、Decimal は文字列として出力されるので、JS側で数値として扱うなら明示的に変換してください。
一方、HTMLの断片をこの仕組みで渡して innerHTML へ入れる使い方は、エスケープの意味を打ち消します。渡すのはデータだけにして、描画はJS側のテンプレートかDOM APIで組み立てるのが筋です。画面全体をJavaScript側で持つ構成に踏み込むなら、テンプレートへの埋め込みではなくAPIとして分離する判断になります。その場合の構成と認証はReactとDjangoを連携する方法|REST API・CORS・JWT認証の実装手順で扱っています。
DjangoテンプレートをJinja2へ差し替えてよい条件と見送る場面
「Jinja2のほうが速い」という理由で差し替えを検討する場面があります。設定自体は数行で済みますが、移植の実費は別のところに出ます。
Jinja2バックエンドは併用でき置き場所はjinja2ディレクトリ
Djangoは TEMPLATES に複数のバックエンドを並べられます。既存の DjangoTemplates を残したまま、一部だけJinja2へ寄せる形も可能です。
TEMPLATES = [
{
"BACKEND": "django.template.backends.jinja2.Jinja2",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {"environment": "myproject.jinja2.environment"},
},
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
},
]
APP_DIRS が True のとき、Jinja2バックエンドはアプリの jinja2 ディレクトリを探索します。DjangoTemplates が見る templates とは別の場所なので、ファイルの置き場所で使い分けが決まる仕組みです。
urlとstaticとcsrf_tokenは自前でグローバルへ登録する
移植で効いてくるのがここです。{% url %} と {% static %} はDjangoテンプレートのタグであり、Jinja2側には存在しません。環境を作る関数でグローバルへ登録する必要があります。
// myproject/jinja2.py
from django.templatetags.static import static
from django.urls import reverse
from jinja2 import Environment
def environment(**options):
env = Environment(**options)
env.globals.update({"static": static, "url": reverse})
return env
テンプレート側は {{ url('orders:list') }} のような関数呼び出しへ書き換えます。csrf_token はグローバル変数として扱い、{% csrf_token %} と同じ出力にはなりません。加えて、Jinja2では context_processors の使用が推奨されていないため、共通値の供給方法も設計し直すことになります。既存プロジェクトなら、テンプレート枚数と共通値の数がそのまま移植量です。
Jinja2へ差し替えてよい条件と標準のまま進める場面の線引き
差し替えを選んでよいのは、次の3条件がそろう場合に限られます。第一に、描画時間がプロファイルで実測されており、テンプレート描画がボトルネックとして特定できていること。第二に、テンプレート内でマクロや複雑な式が必要な要件が実在すること。第三に、対象テンプレートが数十枚規模に収まり、共通タグの書き換え範囲を見積もれること。
逆に、標準のまま進めるべきなのは、描画より先にクエリやN+1が支配的な場合、管理画面や既存パッケージのテンプレートを継承している場合、そしてチームにJinja2の経験者がいない場合です。多くの案件では、テンプレート層を触るより先にクエリと静的ファイル配信を見直すほうが効きます。配信側の構成はDjangoのデプロイ手順:gunicorn+nginx構成と本番設定・配置先の選び方で扱っています。
社内ポータルや情報集約系の画面のように、サーバー側で組み立てた画面が主役で管理機能が中心になる案件では、標準のテンプレートに継承とincludeを効かせた構成で足ります。この形の設計と実装はポータルサイトシステム開発で承っています。
テンプレートが原因の不具合を切り分ける手順と本番前の確認項目
テンプレート層の不具合は例外を出さずに進むものが多く、切り分けの順序を決めておくと早く終わります。
変数が空で出るときはstring_if_invalidを一時的に立てる
値が表示されないとき、原因はview側で渡し忘れているか、テンプレート側の名前が違うかのどちらかです。OPTIONS に string_if_invalid を設定すると、解決できなかった変数が指定した文字列で表示されます。
"OPTIONS": {
"string_if_invalid": "【未解決:%s】",
}
この設定は開発環境だけで使ってください。既定が空文字である前提のテンプレートやパッケージが多く、常時有効にすると意図しない箇所に文字が出ます。
本番でDEBUGを落とす前にテンプレートの読み込みを確認する
DEBUG が True のあいだは、テンプレートが見つからないときにどのパスを探したかが画面に一覧表示されます。本番設定へ切り替える前に、この一覧で意図したファイルが読まれているかを確認しておくと、切り替え後の500エラーを減らせます。
あわせて、本番前に見る項目は3つです。{% static %} で参照している静的ファイルが collectstatic の対象に入っているか、フォームに {% csrf_token %} が入っているか、そしてエスケープを解除している箇所が意図した数に収まっているか。この3点を切り替え前のチェックリストに固定しておくと、公開直後の手戻りが減ります。
よくある質問
DjangoテンプレートとJinja2はどちらを使うべきですか?
新規プロジェクトでは標準のDjangoテンプレートで始めるのが無難です。{% url %}・{% static %}・{% csrf_token %} や context processors がそのまま使え、管理画面や外部パッケージのテンプレートとも噛み合います。Jinja2は、描画時間の実測でボトルネックが特定でき、かつ移植対象のテンプレート数を見積もれる場合の選択肢です。両者は併用できるため、全面移行ではなく重い画面だけ寄せる進め方もできます。
テンプレートで変数が表示されないときは何を見ればよいですか?
まずviewが実際にそのキーを渡しているかを確認します。Djangoは解決できない変数を例外にせず、既定では空文字へ置き換えるため、タイプミスは画面から消えるだけです。開発環境で string_if_invalid を一時的に設定すると、どの変数が解決できていないかが画面に出ます。クラスベースビューの場合、context_object_name を指定していないと一覧が object_list という名前で届いている可能性もあります。
テンプレートからJavaScriptへ値を渡す方法は何が安全ですか?
json_script フィルタを使います。{{ value|json_script:"my-data" }} と書くと type="application/json" の script 要素として出力され、JS側は JSON.parse で読み取ります。HTMLの特殊文字がユニコードエスケープへ置き換わるため、値に閉じタグ相当の文字列が含まれていても要素が壊れません。script タグへ直接書く方法や safe での出力は、XSSの経路になるため避けてください。
extendsとincludeはどう使い分ければよいですか?
ページ全体の骨格を共有するのが extends、部品を差し込むのが include です。include は現在のコンテキストを引き継ぐため、共通部品として使うなら with で必要な値を明示し only で他を遮断します。判断に迷ったら、単独の画面として成立するかどうかで分けてください。
テンプレート内でデータベースにアクセスしても問題ありませんか?
動作はしますが、設計としては避けるべきところです。テンプレートからリレーションを辿るとループの回数だけクエリが発行され、テンプレートを読んでもクエリ本数が見えない状態になります。select_related や prefetch_related をview側で付けて評価済みの値を渡すか、集計はデータベース側で済ませてから渡す形にしてください。テンプレートは受け取った値を並べるだけ、という分担を保つのが安全です。
関連記事
- Djangoとは?Python製Webフレームワークの特徴・MVT・始め方を入門解説:フレームワーク全体とMVTの位置づけです。
- Django ORMのクエリ設計|遅延評価と集計の順序・生SQLへ落とす判断:テンプレートへ渡す前のクエリ設計です。
- django-filterで一覧検索を実装する|FilterSet設計とDRF指定の判断:検索フォームと一覧の連動です。
- Djangoの脆弱性への対応|セキュリティリリースの追い方と標準で防げない穴:エスケープを解除する境界です。
- ReactとDjangoを連携する方法|REST API・CORS・JWT認証の実装手順:画面をJS側へ寄せる構成です。
- Djangoのデプロイ手順:gunicorn+nginx構成と本番設定・配置先の選び方:静的ファイル配信と本番設定です。