Django基礎 #5 Templates と静的ファイル
#4 URL と Views で view が render(request, "blog/post_list.html", {"posts": posts}) のような呼び出しで HTML を返しました。今回はその HTML の正体 — Django テンプレート の構文と、テンプレートが参照する 静的ファイル (CSS、JS、画像) の処理を見ます。
テンプレートディレクトリ構造 #
Django のテンプレート検索パスは 2 つ — 各アプリの templates/ と プロジェクト共通の templates/。
myblog/
├── config/
│ └── settings.py
├── templates/ # プロジェクト共通 (base.html など)
│ └── base.html
└── blog/
├── views.py
└── templates/
└── blog/ # もう一段深く入れるのがコンベンション
├── post_list.html
└── post_detail.htmlblog/templates/blog/post_list.html のように アプリ名をもう一度入れる理由 は、別アプリの同じ名前のテンプレートとの衝突を避けるためです。2 つのアプリが両方 post_list.html を持っていると、どちらが選択されるか分からなくなります。
settings.py の TEMPLATES 設定
#
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # 共通テンプレートディレクトリ
"APP_DIRS": True, # 各アプリの templates/ を自動検索
"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",
],
},
},
]DIRS に共通ディレクトリ、APP_DIRS=True でアプリ別の自動検索。
変数の出力 — {{ }}
#
<h1>{{ post.title }}</h1>
<p>作成者: {{ post.author.username }}</p>
<p>作成日: {{ post.created_at }}</p>
<div>{{ post.content }}</div>{{ var }} が変数の出力。ドット記法 は次の順序で試します:
- dict のキー (
var["title"]) - オブジェクトの属性 (
var.title) - メソッド (
var.title()) — 引数なしで呼び出せる場合 - リストのインデックス (
var[0])
この自動の試行が、テンプレートが 単純な式だけ 許可する理由です — Python コードを直接書けません (それが Django テンプレートの哲学)。
自動エスケープ — XSS のデフォルト防御 #
Django テンプレートは {{ }} で出力されるすべての値を 自動 HTML エスケープ します。<script>alert(1)</script> が変数に入っていると <script>... に変換されます。
これを解除したい (例: 事前に sanitize した HTML) なら |safe フィルタ:
{{ post.content_html|safe }}|safe は本当に信頼できるデータにだけ。ユーザー入力にそのまま付けると XSS 脆弱性です。
タグ — {% %}
#
タグが ロジック を担当します。よく使うものたち。
{% if %}
#
{% if post.is_published %}
<span class="badge">公開済み</span>
{% elif post.status == "draft" %}
<span class="badge">草稿</span>
{% else %}
<span class="badge">その他</span>
{% endif %}
{% if posts %}
<p>合計 {{ posts|length }} 件</p>
{% else %}
<p>まだ記事がありません。</p>
{% endif %}比較演算子 — ==、!=、<、>、<=、>=、in、not in、is、is not。論理演算子 — and、or、not。
{% for %}
#
<ul>
{% for post in posts %}
<li>
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>
<small>by {{ post.author.username }}</small>
</li>
{% empty %}
<li>登録された記事がありません。</li>
{% endfor %}
</ul>{% empty %} が空のコレクションのときの分岐。とてもよく使います。
ループの中で使える変数:
| 変数 | 意味 |
|---|---|
forloop.counter | 1 から始まるインデックス |
forloop.counter0 | 0 から始まる |
forloop.first | 最初の繰り返しなら True |
forloop.last | 最後の繰り返しなら True |
forloop.revcounter | 最後から 1 まで |
{% for post in posts %}
<div class="{% if forloop.first %}featured{% endif %}">
{{ forloop.counter }}. {{ post.title }}
</div>
{% endfor %}{% url %}
#
<a href="{% url 'blog:post_list' %}">一覧</a>
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>
<a href="{% url 'blog:post_detail' post_id=post.id %}">{{ post.title }}</a>#4 で見た named URL のテンプレート版。
{% include %}
#
{% include "blog/_post_card.html" %}
{% include "blog/_post_card.html" with post=post show_author=True %}部分テンプレートを再利用。ファイル名を _ で始めるのが partial のコンベンション。
{% csrf_token %}
#
フォームには必ず入れなければならないタグ。
<form method="post">
{% csrf_token %}
<input name="title" />
<button>保存</button>
</form>抜けると 403 Forbidden で弾かれます。#6 で詳しく。
フィルタ — |
#
フィルタは変数を 変換 します。{{ value|filter }} または {{ value|filter:"arg" }}。
{{ post.title|upper }} {# 大文字 #}
{{ post.title|lower }}
{{ post.title|truncatechars:50 }} {# 50 文字で切って ... #}
{{ post.title|truncatewords:10 }}
{{ post.created_at|date:"Y-m-d H:i" }} {# 日付フォーマット #}
{{ post.created_at|timesince }} {# "3 日前" #}
{{ post.content|linebreaks }} {# 改行 → <p>, <br> #}
{{ post.content|striptags }} {# HTML タグ除去 #}
{{ posts|length }}
{{ post.content|default:"内容なし" }}
{{ value|yesno:"はい,いいえ,不明" }}
{{ price|floatformat:2 }} {# 1234.567 → 1234.57 #}
{{ name|capfirst }} {# 最初の文字だけ大文字 #}
{{ posts|first }}
{{ posts|last }}
{{ posts|slice:":5" }}フィルタは チェーン 可能 — {{ post.title|truncatechars:50|upper }}。
テンプレート継承 — {% extends %} / {% block %}
#
Web ページの共通骨格 (ヘッダ、フッタ、サイドバー) を一カ所にまとめておき、ページごとに変わる部分だけを埋めるパターン。
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Blog{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>
<header>
<a href="{% url 'blog:post_list' %}">ホーム</a>
{% if user.is_authenticated %}
<span>{{ user.username }}</span>
<a href="{% url 'logout' %}">ログアウト</a>
{% else %}
<a href="{% url 'login' %}">ログイン</a>
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2026 My Blog</p>
</footer>
</body>
</html>{% extends "base.html" %}
{% block title %}記事一覧 - My Blog{% endblock %}
{% block content %}
<h1>記事一覧</h1>
<ul>
{% for post in posts %}
<li>
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>
<small>{{ post.created_at|date:"Y-m-d" }}</small>
</li>
{% empty %}
<li>まだ記事がありません。</li>
{% endfor %}
</ul>
{% endblock %}ルール:
- 子テンプレートの 最初の行 が
{% extends "..." %} - 親の
{% block name %}...{% endblock %}の位置に子の同じ名前の block が入る - 子が空にすると親のデフォルトがそのまま表示される
- block 内で
{{ block.super }}を使うと親の内容 + 子の内容を合わせられる
多段継承 #
base.html (サイト骨格)
↑
blog/_layout.html (サイドバー追加、blog 共通)
↑
blog/post_list.html (実際のページ)各段階が自分の上のテンプレートを extends すれば良いです。深く積み上げるほど慎重に。
静的ファイル — CSS、JS、画像 #
settings とディレクトリ #
STATIC_URL = "static/"
# 開発段階での追加検索パス
STATICFILES_DIRS = [
BASE_DIR / "static",
]
# 本番デプロイ時の collectstatic の出力位置
STATIC_ROOT = BASE_DIR / "staticfiles"各アプリの static/ ディレクトリも自動的に検索されます (django.contrib.staticfiles が有効化されているとき — デフォルト)。
myblog/
├── static/ # プロジェクト共通
│ └── css/
│ └── main.css
└── blog/
└── static/
└── blog/ # アプリ名をもう一度 (テンプレートと同じコンベンション)
├── css/
│ └── post.css
└── images/
└── logo.pngテンプレートで {% static %}
#
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<link rel="stylesheet" href="{% static 'blog/css/post.css' %}">
<img src="{% static 'blog/images/logo.png' %}" alt="logo">
<script src="{% static 'blog/js/post.js' %}"></script>{% load static %} をテンプレートの上に一度宣言する必要があります。通常は base.html の <head> の上のほうに。
本番デプロイ — collectstatic
#
runserver (開発サーバー) は静的ファイルを自動で配信します。本番では collectstatic ですべての静的ファイルを 1 つのディレクトリ (STATIC_ROOT) に集めた後、Nginx などの Web サーバーが直接配信します。
uv run python manage.py collectstaticこのコマンドがやること:
- すべてのアプリの
static/+STATICFILES_DIRSのファイルをSTATIC_ROOTにコピー - 同じ名前があれば最初の検索パスのファイルが優先
本番の詳しい流れ (WhiteNoise、CDN、manifest storage) は 中級 #6 Static/Media 運用 で。
Media ファイル — ユーザーアップロード #
STATIC は 開発者が作った ファイル (CSS、JS)、MEDIA は ユーザーがアップロードした ファイル (プロフィール画像、添付ファイルなど)。両者は別の場所です。
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)本番でユーザーアップロードを直接配信してはいけません — S3 のような外部ストレージに。これも 中級 #6 で。
Django テンプレート vs Jinja2 — 一行比較 #
Django テンプレートは 意図的に制限された言語 です。Python コードを直接書けず、任意の関数呼び出しも難しいです ({% load %} したタグ / フィルタだけ)。それが セキュリティとデザイナーフレンドリー を守る方式です。
Jinja2 はより強力 — テンプレート内でほぼすべての Python 式が可能です。それだけビジネスロジックがテンプレートに漏れやすいという意味でもあります。
Django でも Jinja2 を backend として使えますが (TEMPLATES の BACKEND を変更)、Admin / Form / その他のビルトインは Django テンプレート基準なので混ざって動作します。最初は Django テンプレートをそのまま使うのが無難です。
ユーザー定義タグ / フィルタ #
よく使う表現があるなら自分で作れます。
from django import template
register = template.Library()
@register.filter
def markdown_to_html(value: str) -> str:
import markdown
return markdown.markdown(value)
@register.simple_tag
def current_year():
from datetime import datetime
return datetime.now().year{% load blog_extras %}
<div>{{ post.content|markdown_to_html|safe }}</div>
<footer>© {% current_year %}</footer>templatetags/ ディレクトリ、__init__.py が必要。このルールを破ると Django が見つけられません。
まとめ #
今回つかんだもの:
- テンプレートの位置 — アプリ別
templates/<app>/...+ 共通templates/ TEMPLATES設定のDIRS/APP_DIRS- 変数 —
{{ }}、自動 HTML エスケープ (XSS 防御) - タグ —
{% if %}、{% for %}(with{% empty %})、{% url %}、{% include %}、{% csrf_token %} - フィルタ —
|date、|truncatechars、|linebreaks、|safe、… - テンプレート継承 —
{% extends %}、{% block %}、{{ block.super }} - 静的ファイル —
STATIC_URL、STATICFILES_DIRS、{% load static %}、{% static %} collectstaticは本番用- Media ファイルは別 (
MEDIA_URL/MEDIA_ROOT) - Django テンプレートは意図的に制限された言語 — セキュリティ / シンプルさ
- ユーザー定義タグ / フィルタ —
templatetags/
次回(#6 Forms と ModelForm)ではこのテンプレートの中でフォームを処理する標準の方法 — Form / ModelForm で検証、CSRF、エラー表示まで一度に解くパターンを扱います。