Django超初心者シリーズ

5回 / 全10

【Django Template完全ガイド】テンプレート継承・静的ファイル・高度なタグの使い方

2025/07/07に公開

はじめに:Web ページをもっと美しく、効率的に!

前回は、Django のビューとテンプレートの基本を学び、動的な Web ページを作ることができるようになりました。「Hello Django!」から始まって、データを表示するブログ記事一覧まで、本当によく頑張りましたね!

今回は、テンプレートの真の力を解き放ちます。テンプレートの継承を使って効率的にコードを書く方法、CSS や JavaScript、画像を使って美しいページを作る方法を学びましょう。

「えっ、また新しいことを覚えるの?」と思うかもしれません。でも安心してください。今回学ぶことは、あなたの Web ページを一気にプロフェッショナルなレベルに引き上げてくれる、とても楽しい内容です!

Note

初めてこの記事を読む方へ:

この記事は連載の第5回目です。ビューとテンプレートの基本がまだの方は、第4回の記事をご覧ください。もしすぐにテンプレートの高度な機能を学びたい方は、前回GitHubコードを参考にしながら進めることもできます。

この記事で学べること

  • テンプレートの継承システムの仕組みと使い方
  • 静的ファイル(CSS、JavaScript、画像)の設定と活用
  • 便利なテンプレートタグとフィルタ
  • include を使った部品化
  • 実践的なブログテンプレートの作成

それでは、Django テンプレートの世界をもっと深く探検していきましょう!

テンプレートの継承:DRY 原則を実現する

なぜテンプレートの継承が必要なの?

前回まで、各ページごとに完全な HTML を書いていました。でも、実際の Web サイトでは、ヘッダーやフッターなど、全ページで共通する部分がありますよね。

例えば、10 ページあるサイトでヘッダーを変更したい場合、10 個のファイルを全部修正する必要があります...これは大変です!😱

そこで登場するのがテンプレートの継承です。

Tip

DRY 原則とは?

DRY = Don't Repeat Yourself(同じことを繰り返すな)

プログラミングの重要な原則の一つです。同じコードを何度も書かずに、一箇所にまとめて再利用しようという考え方です。

メリット:

  • 修正が一箇所で済む
  • コードが読みやすい
  • バグが減る

ベーステンプレートを作成しよう

まず、すべてのページの「型」となるベーステンプレートを作成します。

blog/templates/blog/base.htmlを作成:

blog/templates/blog/base.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}My Blog{% endblock %}</title> {% block extra_css %}{% endblock %} </head> <body> <header> <nav> <h1><a href="{% url 'post_list' %}">My Blog</a></h1> <ul> <li><a href="{% url 'post_list' %}">ホーム</a></li> <li><a href="#">記事一覧</a></li> <li><a href="#">About</a></li> </ul> </nav> </header> <main> {% block content %} <!-- ここに各ページの内容が入ります --> {% endblock %} </main> <footer> <p>&copy; 2025 My Blog. All rights reserved.</p> </footer> {% block extra_js %}{% endblock %} </body> </html>

重要なポイントを解説

{% block ブロック名 %}の説明:

  • blockは「ここは子テンプレートで上書きできる場所だよ」という印
  • 子テンプレートで同じブロック名を使って内容を入れ替えられる
  • ブロックの中に書いた内容(例:My Blog)はデフォルト値として使われる

例えば、{% block title %}My Blog{% endblock %}の場合:

  • 子テンプレートがこのブロックを上書きしない場合 → 「My Blog」と表示される
  • 子テンプレートがこのブロックを上書きする場合 → 子テンプレートで指定した内容が表示される
Tip

デフォルト値の便利な使い方

パターン1:子テンプレートが何も指定しない場合のフォールバック

<!-- base.html --> <title>{% block title %}My Blog{% endblock %}</title> <!-- 子テンプレートでblockを上書きしない場合 --> <!-- 結果:ブラウザのタブに「My Blog」と表示される -->

パターン2:メインコンテンツエリアのデフォルトメッセージ

<!-- base.html --> <main> {% block content %} <div class="default-content"> <h2>ページが見つかりません</h2> <p>お探しのページは存在しないか、移動した可能性があります。</p> <a href="/">トップページへ戻る</a> </div> {% endblock %} </main> <!-- 通常のページ(post_list.html)で上書きする場合 --> {% block content %} <h2>記事一覧</h2> <!-- 記事のリスト表示 --> {% endblock %} <!-- もし子テンプレートでcontentブロックを定義し忘れた場合 --> <!-- デフォルトの「ページが見つかりません」が表示される -->

つまり、デフォルト値を使うことで:

  • 開発中のミスを防げる(空白ページにならない)
  • エラーページのような共通コンテンツを簡単に実装できる
  • テンプレートの実装忘れがあってもユーザーに適切なメッセージを表示できる

よく使うブロック名:

  • title:ページのタイトル(デフォルト値を設定することが多い)
  • content:メインコンテンツ(通常デフォルト値なし)
  • extra_css:ページ固有の CSS(通常デフォルト値なし)
  • extra_js:ページ固有の JavaScript(通常デフォルト値なし)

子テンプレートで継承を使う

では、前回作ったpost_list.htmlを継承を使って書き直してみましょう。

blog/templates/blog/post_list.htmlを修正:

blog/templates/blog/post_list.html
{% extends 'blog/base.html' %} {% block title %}記事一覧 - My Blog{% endblock %} {% block content %} <h2>ブログ記事一覧</h2> {% for post in posts %} <article class="post"> <h3>{{ post.title }}</h3> <p class="post-date">投稿日: {{ post.created_at }}</p> <p>{{ post.content }}</p> </article> {% endfor %} {% if not posts %} <p>まだ記事がありません。</p> {% endif %} {% endblock %}

なんとスッキリ!必要な部分だけを書けばいいんです。

Note

あれ?前回の<style>タグがなくなった?

そうです!前回は以下のように<style>タグでスタイルを定義していました:

<style> .post { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; border-radius: 5px; } /* 他のスタイル... */ </style>

でも今回はclass属性(class="post"など)だけ残して、スタイルの定義は削除しています。

心配しないでください!このあとの「静的ファイルでページを美しくする」のセクションで、CSSファイルを使ってもっとプロフェッショナルなスタイルを適用する方法を学びます。

内部スタイルシート(<style>タグ)より外部CSSファイルを使う方が:

  • 複数のページで同じスタイルを共有できる
  • HTMLとCSSを分離して管理しやすい
  • ブラウザがCSSファイルをキャッシュして高速化できる

という利点があります!

継承の仕組みを図解

テンプレート継承の仕組み

この図を詳しく見てみましょう:

  1. base.html(親テンプレート)

    • HTMLの全体構造を定義
    • {% block title %}{% block content %}で、子テンプレートが上書きできる場所を指定
    • ヘッダーやフッターなど、全ページ共通の部分はここに書く
  2. post_list.html(子テンプレート)

    • {% extends 'base.html' %}で親テンプレートを継承することを宣言
    • 必要なブロックだけを定義(この例ではcontentブロックのみ)
    • titleブロックは定義していないので、親のデフォルト値「My Blog」が使われる
  3. 処理後の結果

    • Djangoが自動的に親と子を組み合わせて、完全なHTMLを生成
    • 子で定義したブロックは上書きされ、定義していないブロックは親の内容がそのまま使われる
    • 結果として、共通部分を何度も書く必要がなくなる!

もう一つページを作ってみよう

継承の便利さを実感するため、記事詳細ページも作ってみましょう。

blog/views.pyを修正して、共通のデータを定義し、詳細表示用の関数を追加します:

blog/views.py
from django.shortcuts import render # 共通の記事データ POSTS = [ { "id": 1, "title": "Djangoを始めました", "content": "Djangoの学習を始めました。楽しいです!", "created_at": "2025-07-01", "body": """今日からDjangoの学習を始めました。 Pythonは少し触ったことがあったけど、Webフレームワークは初めてです。 最初は難しそうだと思ったけど、チュートリアルが分かりやすくて助かります。 これからブログアプリを作っていきたいと思います!""", }, { "id": 2, "title": "ビューについて学んだこと", "content": "今日はビューについて学びました。MVTパターンの理解が深まりました。", "created_at": "2025-07-02", "body": """Djangoのビューは、リクエストを受け取ってレスポンスを返す関数です。 MVTパターンのVに当たる部分で、ビジネスロジックを担当します。 テンプレートにデータを渡す方法も学びました。 contextという辞書を使うのが面白いです。""", }, { "id": 3, "title": "テンプレートは便利", "content": "テンプレートを使うとHTMLが書きやすいです。継承システムが特に便利!", "created_at": "2025-07-03", "body": """今日はテンプレートについて学習しました。 変数の表示、forループ、if文など、基本的な機能を試しました。 特に継承システムが素晴らしいです! 共通部分を一箇所にまとめられるのは、とても効率的ですね。""", }, ] def post_list(request): context = { "posts": POSTS, } return render(request, "blog/post_list.html", context) def post_detail(request, post_id): # 指定されたIDの記事を探す post = None for p in POSTS: if p["id"] == post_id: post = p break context = { "post": post, } return render(request, "blog/post_detail.html", context)

このコードでは:

  • 記事データをPOSTSという共通の変数にまとめました
  • post_list関数は前回作成済みで、POSTSを使うように修正
  • post_detail関数を新たに追加し、指定されたIDの記事を表示

blog/templates/blog/post_detail.htmlを作成:

blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %} {% block title %}{{ post.title }} - My Blog{% endblock %} {% block content %} {% if post %} <article class="post-detail"> <h2>{{ post.title }}</h2> <p class="post-meta">投稿日: {{ post.created_at }}</p> <div class="post-body"> {{ post.body|linebreaks }} </div> <p><a href="{% url 'post_list' %}">← 記事一覧に戻る</a></p> </article> {% else %} <p>記事が見つかりませんでした。</p> <p><a href="{% url 'post_list' %}">記事一覧に戻る</a></p> {% endif %} {% endblock %}

blog/urls.pyのURLpatternsに追加:

blog/urls.py
from django.urls import path from . import views urlpatterns = [ path("", views.post_list, name="post_list"), path("post/<int:post_id>/", views.post_detail, name="post_detail"), # 追加 ]

見てください!ヘッダーやフッターを書かなくても、完全なページができあがりました。これがテンプレート継承の力です!

静的ファイルでページを美しくする

静的ファイルとは?

静的ファイルとは、サーバー側で処理される必要がない、そのまま配信されるファイルのことです:

  • CSS:スタイルシート
  • JavaScript:動的な機能
  • 画像:ロゴ、アイコン、写真など
  • フォント:Web フォント

静的ファイルの設定

まず、mysite/settings.pyで静的ファイルの設定を確認しましょう:

mysite/settings.py
# 静的ファイルのURL(すでに設定されているはず) STATIC_URL = "static/"

この設定は最初から存在しているはずです。これは「静的ファイルにアクセスするときのURLのプレフィックス」を指定しています。

例えば、blog/static/blog/css/style.cssというファイルは、ブラウザからはhttp://localhost:8000/static/blog/css/style.cssでアクセスできます。

Note

Djangoの静的ファイルの仕組み

Djangoは以下の場所から静的ファイルを自動的に探します:

  1. 各アプリのstaticフォルダ(自動認識)

    blog/static/ ← 自動的に認識される practice/static/ ← これも自動的に認識される
  2. STATICFILES_DIRSで指定したフォルダ(追加設定が必要)

    # プロジェクト共通の静的ファイルを置きたい場合のみ追加 STATICFILES_DIRS = [BASE_DIR / "static"]

今回は各アプリのstaticフォルダを使うので、STATICFILES_DIRSの設定は不要です。 もしサイト全体で使うロゴ画像などを管理したい場合は、この設定を追加して、プロジェクトルートにstaticフォルダを作成します。

Tip

開発環境と本番環境の違い

開発中(DEBUG = True)は、python manage.py runserverが自動的に静的ファイルを配信してくれます。

本番環境では、パフォーマンスのためにNginxやApacheなどのWebサーバーが静的ファイルを配信します。 その際はSTATIC_ROOTという別の設定を使いますが、今は気にする必要はありません。

静的ファイル用のディレクトリ構造

静的ファイルは以下の構造で配置する予定です。

blog/ ├── static/ │ └── blog/ │ ├── css/ │ │ └── style.css │ ├── js/ │ │ └── main.js │ └── images/ │ └── logo.png ├── templates/ └── ...
Tip

なぜ static/blog という二重構造?

テンプレートと同じ理由です!複数のアプリケーションがある場合、名前の衝突を避けるためです。

Django は全アプリの static フォルダを一箇所にまとめるので、アプリ名のフォルダで区別します。

CSS ファイルを作成して適用する

blog/static/blog/css/style.cssを作成:

blog/static/blog/css/style.css
/* リセットとベーススタイル */ * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f4f4f4; display: flex; flex-direction: column; } /* ヘッダースタイル */ header { background-color: #2c3e50; color: white; padding: 1rem 0; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } header nav { max-width: 1200px; margin: 0 auto; padding: 0 2rem; display: flex; justify-content: space-between; align-items: center; } header h1 { margin: 0; line-height: 1; } header h1 a { color: white; text-decoration: none; font-size: 1.8rem; display: flex; align-items: center; gap: 0.5rem; } header h1 img { height: 40px; width: auto; display: block; } header ul { list-style: none; display: flex; gap: 2rem; margin: 0; padding: 0; } header ul a { color: white; text-decoration: none; transition: opacity 0.3s; } header ul a:hover { opacity: 0.8; } /* メインコンテンツ */ main { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; flex: 1; width: 100%; } /* h2タグのマージン調整 */ main h2 { margin-bottom: 1.5rem; } /* 記事スタイル */ .post { background: white; padding: 2rem; margin-bottom: 2rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); transition: transform 0.3s; } .post:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.15); } .post h3 { color: #2c3e50; margin-bottom: 0.5rem; } .post-date { color: #7f8c8d; font-size: 0.9rem; margin-bottom: 1rem; } /* フッター */ footer { background-color: #34495e; color: white; text-align: center; padding: 1rem 0; }

テンプレートで CSS を読み込む

blog/templates/blog/base.htmlを修正:

blog/templates/blog/base.html
{% load static %} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}My Blog{% endblock %}</title> <!-- 静的ファイルの読み込み --> <link rel="stylesheet" href="{% static 'blog/css/style.css' %}"> {% block extra_css %}{% endblock %} </head> <!-- 以下省略 -->

重要なポイント:

  1. {% load static %}static タグを使えるようにする(ファイルの最初に 1 回だけ)
  2. {% static 'パス' %}: 静的ファイルの URL を生成

動作確認してみよう

開発サーバーを起動して、ブラウザで確認してみましょう:

ターミナル
$ python manage.py runserver
Important

すでにサーバーが起動している場合

前回からサーバーを起動したままの方は、一度サーバーを停止してから再起動する必要があります

  1. ターミナルで Ctrl + C(Macの場合は Control + C)を押してサーバーを停止
  2. 再度 python manage.py runserver でサーバーを起動

これをしないと、新しく追加したCSSファイルが読み込まれません!

http://localhost:8000/blog/ にアクセスすると...

CSSが適用されたブログ

わぁ!一気にプロフェッショナルな見た目になりましたね!🎨

Tip

CSS が反映されない場合は?

  1. ブラウザのキャッシュをクリア(Ctrl+F5 または Cmd+Shift+R)
  2. settings.pySTATICFILES_DIRS設定を確認
  3. ファイルパスが正しいか確認(static/blog/css/style.css

JavaScript も追加してみよう

blog/static/blog/js/main.jsを作成:

blog/static/blog/js/main.js
// ページ読み込み完了時に実行 document.addEventListener('DOMContentLoaded', function() { // 記事をクリックしたときのアニメーション const posts = document.querySelectorAll('.post'); posts.forEach(post => { post.addEventListener('click', function() { // クリックアニメーション this.style.transform = 'scale(0.98)'; setTimeout(() => { this.style.transform = ''; }, 100); }); }); // スムーズスクロール const links = document.querySelectorAll('a[href^="#"]'); links.forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // コンソールにメッセージ console.log('Djangoブログへようこそ!'); });

blog/templates/blog/base.htmlの body タグの閉じタグ前に追加:

blog/templates/blog/base.html
<footer> <p>&copy; 2024 My Blog. All rights reserved.</p> </footer> <!-- JavaScriptの読み込み --> <script src="{% static 'blog/js/main.js' %}"></script> {% block extra_js %}{% endblock %} </body> </html>

画像も追加してみよう

ロゴ画像を表示する例を見てみましょう。

blog/templates/blog/base.htmlのヘッダー部分を修正:

blog/templates/blog/base.html
<header> <nav> <h1> <a href="{% url 'post_list' %}"> <img src="{% static 'blog/images/logo.png' %}" alt="My Blog" height="40"> <span>My Blog</span> </a> </h1> <ul> <li><a href="{% url 'post_list' %}">ホーム</a></li> <li><a href="#">記事一覧</a></li> <li><a href="#">About</a></li> </ul> </nav> </header>
Note

画像ファイルについて

実際に表示するには、blog/static/blog/images/フォルダに logo.png ファイルを配置する必要があります。例えば icons8.com からテスト用のロゴファイルをダウンロードしてみてください。

他の無料の画像素材サイト:

Faviconも追加しよう

Favicon(ファビコン) とは、ブラウザのタブやブックマークに表示される小さなアイコンのことです。Webサイトのブランディングに重要な要素で、ユーザーが複数のタブを開いているときにサイトを識別しやすくなります。

Tip

Faviconの豆知識

  • Faviconは「Favorite Icon」の略称です
  • 一般的なサイズは16×16、32×32、48×48ピクセル
  • 最近では、より大きなサイズ(192×192など)も推奨されています
  • .ico形式が伝統的ですが、.pngや.svgも使用可能です

Faviconを追加するには、blog/templates/blog/base.html<head>セクションに以下を追加します:

blog/templates/blog/base.html
{% load static %} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}My Blog{% endblock %}</title> <!-- Favicon --> <link rel="icon" type="image/png" sizes="32x32" href="{% static 'blog/images/favicon-32x32.png' %}"> <link rel="icon" type="image/png" sizes="16x16" href="{% static 'blog/images/favicon-16x16.png' %}"> <link rel="apple-touch-icon" sizes="180x180" href="{% static 'blog/images/apple-touch-icon.png' %}"> <!-- 静的ファイルの読み込み --> <link rel="stylesheet" href="{% static 'blog/css/style.css' %}"> {% block extra_css %}{% endblock %} </head>

Faviconの作成方法

  1. オンラインツールを使用する方法(推奨)

  2. 画像編集ソフトを使用する方法

    • 正方形の画像(512×512px推奨)を作成
    • 必要なサイズにリサイズ
    • blog/static/blog/images/フォルダに保存
Important

Faviconが表示されない場合

  • ブラウザのキャッシュをクリア(Ctrl+F5 または Cmd+Shift+R)
  • 開発サーバーを再起動
  • ファイルパスが正しいか確認
  • ブラウザによってはico形式のみ対応している場合があるので、以下も追加:
    <link rel="shortcut icon" href="{% static 'blog/images/favicon.ico' %}">

JavaScriptと画像の動作確認

サーバーを再起動して、すべての機能が動作しているか確認しましょう。

Faviconの動作確認方法:

  1. ブラウザのタブを確認 - 小さなアイコンが表示されているはず
  2. ページをブックマークしてみる - ブックマークリストにアイコンが表示される
  3. スマートフォンでホーム画面に追加 - アプリアイコンとして表示される(apple-touch-iconを設定した場合)

JavaScriptの動作確認方法:

  1. ブラウザの開発者ツールを開く(F12キーまたは右クリック→「検証」)
  2. 「Console」タブを選択
  3. 「Djangoブログへようこそ!」と表示されていればJavaScriptが正しく読み込まれています
  4. 記事のボックスをクリックすると、少し縮小されるアニメーションが動作します

CSS、JavaScript、画像がすべて適用されたブログ

上の画像では:

  • ブラウザのタブにFaviconが表示されています
  • ヘッダーにロゴ画像が表示されています
  • CSSによるスタイリングが適用されています
  • JavaScriptが正常に動作しています(コンソールで確認)

便利なテンプレートタグとフィルタ

よく使うテンプレートタグ

Django には便利なテンプレートタグがたくさん用意されています。

if 文:条件分岐

{% if user.is_authenticated %} <p>ようこそ、{{ user.username }}さん!</p> {% else %} <p>ログインしてください</p> {% endif %} <!-- 複数条件 --> {% if posts %} <p>{{ posts|length }}件の記事があります</p> {% elif drafts %} <p>下書きが{{ drafts|length }}件あります</p> {% else %} <p>記事がありません</p> {% endif %}

for ループの便利な変数

{% for post in posts %} <div class="post"> <span class="number">{{ forloop.counter }}</span> <!-- 1から始まる番号 --> <h3>{{ post.title }}</h3> {% if forloop.first %} <span class="badge">NEW!</span> {% endif %} {% if forloop.last %} <hr> {% endif %} </div> {% endfor %}

forloop の便利な変数:

  • forloop.counter:1 から始まる番号
  • forloop.counter0:0 から始まる番号
  • forloop.first:最初の要素なら True
  • forloop.last:最後の要素なら True

URL の生成

<!-- 基本形 --> <a href="{% url 'post_list' %}">記事一覧</a> <!-- パラメータ付き --> <a href="{% url 'post_detail' post_id=post.id %}">続きを読む</a>

便利なフィルタ

フィルタは、変数の値を変換するために使います。パイプ(|)でつなげます。

文字列関連のフィルタ

<!-- 大文字に変換 --> {{ post.title|upper }} <!-- 小文字に変換 --> {{ post.title|lower }} <!-- 最初の文字を大文字に --> {{ post.title|capfirst }} <!-- 文字数制限(...を追加) --> {{ post.content|truncatechars:100 }} <!-- 単語数制限 --> {{ post.content|truncatewords:20 }} <!-- 改行をbrタグに変換 --> {{ post.body|linebreaks }} <!-- HTMLタグをエスケープしない(注意して使用) --> {{ post.html_content|safe }}

数値関連のフィルタ

<!-- 3桁ごとにカンマ --> {{ price|floatformat:0|intcomma }} <!-- 1,234 --> <!-- 小数点以下の桁数指定 --> {{ price|floatformat:2 }} <!-- 1234.50 --> <!-- デフォルト値 --> {{ user.age|default:"不明" }}

日付関連のフィルタ

<!-- 日付フォーマット --> {{ post.created_at|date:"Y年m月d日" }} <!-- 2025年07月10日 --> <!-- 時刻フォーマット --> {{ post.created_at|time:"H:i" }} <!-- 21:30 -->

実践:記事一覧を改良しよう

学んだことを使って、記事一覧をもっと良くしましょう。

blog/templates/blog/post_list.htmlを修正:

blog/templates/blog/post_list.html
{% extends 'blog/base.html' %} {% block title %}記事一覧 - My Blog{% endblock %} {% block content %} <div class="page-header"> <h2>ブログ記事一覧</h2> <p class="post-count"> {% if posts %} 全{{ posts|length }}件の記事 {% endif %} </p> </div> {% for post in posts %} <article class="post"> <div class="post-header"> <h3> <span class="post-number">#{{ forloop.counter }}</span> {{ post.title }} </h3> {% if forloop.first %} <span class="new-badge">NEW!</span> {% endif %} </div> <p class="post-date">投稿日: {{ post.created_at }}</p> <div class="post-content"> {{ post.content|truncatechars:150 }} </div> <a href="{% url 'post_detail' post_id=post.id %}" class="read-more"> 続きを読む → </a> </article> {% empty %} <div class="no-posts"> <p>まだ記事がありません。</p> <p>最初の記事を書いてみましょう!</p> </div> {% endfor %} {% endblock %}

上記のコードは長く見えるかもしれませんが、一行ずつ見ていけば、内容はすべてこれまでに学んだものばかりです!

  • {% extends %}でテンプレート継承
  • {% block %}でブロックの上書き
  • {% if %}で条件分岐
  • {% for %}でループ処理
  • {{ forloop.counter }}で番号表示
  • {{ post.content|truncatechars:150 }}で文字数制限
  • {% url 'post_detail' post_id=post.id %}でURL生成
Tip

{% empty %}タグについて

{% for %}ループで要素が空の場合の処理を書けます。

{% for item in items %} <!-- アイテムがある場合 --> {% empty %} <!-- アイテムがない場合 --> {% endfor %}

これは{% if not items %}を使うより簡潔です!

改良後の記事一覧ページはこのようになります:

改良された記事一覧ページ

さらに凄いのは、「続きを読む →」リンクをクリックすると、実際に記事詳細ページに遷移することです!

これこそが動的なWebアプリケーションの力です。単なるHTMLではできない、ページ間のシームレスなナビゲーションが実現できています。

include で部品を再利用する

include とは?

大きなテンプレートを小さな部品に分割して、再利用できるようにする機能です。

例えば、記事の表示部分を部品化してみましょう。

部品テンプレートを作成

blog/templates/blog/includes/post_card.htmlを作成:

blog/templates/blog/includes/post_card.html
<article class="post"> <div class="post-header"> <h3> <span class="post-number">#{{ number }}</span> {{ post.title }} </h3> {% if is_new %} <span class="new-badge">NEW!</span> {% endif %} </div> <p class="post-date">投稿日: {{ post.created_at }}</p> <div class="post-content"> {{ post.content|truncatechars:150 }} </div> <a href="{% url 'post_detail' post_id=post.id %}" class="read-more"> 続きを読む → </a> </article>

include で部品を使う

blog/templates/blog/post_list.htmlを修正:

blog/templates/blog/post_list.html
{% extends 'blog/base.html' %} {% block title %}記事一覧 - My Blog{% endblock %} {% block content %} <div class="page-header"> <h2>ブログ記事一覧</h2> <p class="post-count"> {% if posts %} 全{{ posts|length }}件の記事 {% endif %} </p> </div> {% for post in posts %} {% include 'blog/includes/post_card.html' with is_new=forloop.first number=forloop.counter %} {% empty %} <div class="no-posts"> <p>まだ記事がありません。</p> <p>最初の記事を書いてみましょう!</p> </div> {% endfor %} {% endblock %}

withを使って複数の変数を渡すこともできます!

Tip

includeのwith句について

{% include %}で部品テンプレートを読み込むとき、withを使って変数を渡せます:

{% include 'template.html' with var1=value1 var2=value2 %}

ここでは:

  • is_new=forloop.first:最初の記事かどうか
  • number=forloop.counter:記事の番号

を部品テンプレートに渡しています。

サイドバーを作ってみよう

より実践的な例として、サイドバーを追加してみましょう。

blog/templates/blog/base.htmlを修正:

blog/templates/blog/base.html
<!-- mainタグ部分を修正 --> <main> <div class="container"> <div class="content"> {% block content %} <!-- ここに各ページの内容が入ります --> {% endblock %} </div> <aside class="sidebar"> {% block sidebar %} {% include 'blog/includes/sidebar.html' %} {% endblock %} </aside> </div> </main>

blog/templates/blog/includes/sidebar.htmlを作成:

blog/templates/blog/includes/sidebar.html
<div class="sidebar-section"> <h3>カテゴリー</h3> <ul class="category-list"> <li><a href="#">Python</a></li> <li><a href="#">Django</a></li> <li><a href="#">Web開発</a></li> <li><a href="#">その他</a></li> </ul> </div> <div class="sidebar-section"> <h3>最近の投稿</h3> <ul class="recent-posts"> {% for post in recent_posts|default:posts|slice:":3" %} <li> <a href="{% url 'post_detail' post_id=post.id %}"> {{ post.title|truncatechars:30 }} </a> </li> {% endfor %} </ul> </div> <div class="sidebar-section"> <h3>アーカイブ</h3> <ul class="archive-list"> <li><a href="#">2025年7月</a></li> <li><a href="#">2025年6月</a></li> <li><a href="#">2025年5月</a></li> </ul> </div>

対応する CSS も追加しましょう:

blog/static/blog/css/style.css
/* コンテナとレイアウト */ .container { display: grid; grid-template-columns: 1fr 300px; gap: 2rem; } .content { min-width: 0; /* グリッドレイアウトのバグ対策 */ } /* サイドバー */ .sidebar { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); height: fit-content; position: sticky; top: 2rem; } .sidebar-section { margin-bottom: 2rem; } .sidebar-section:last-child { margin-bottom: 0; } .sidebar h3 { color: #2c3e50; font-size: 1.1rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #3498db; } .sidebar ul { list-style: none; } .sidebar li { margin-bottom: 0.5rem; } .sidebar a { color: #7f8c8d; text-decoration: none; transition: color 0.3s; } .sidebar a:hover { color: #3498db; } /* レスポンシブ対応 */ @media (max-width: 768px) { .container { grid-template-columns: 1fr; } .sidebar { position: static; margin-top: 2rem; } }
Important

詳細ページのpost_detail関数を更新

サイドバーが「最近の投稿」を表示するためにposts変数を使用しているので、post_detail関数でもcontextにpostsを追加する必要があります。

blog/views.pypost_detail関数を以下のように修正してください:

blog/views.py
def post_detail(request, post_id): # 指定されたIDの記事を探す post = None for p in POSTS: if p['id'] == post_id: post = p break context = { 'post': post, 'posts': POSTS, # サイドバー用に追加 } return render(request, 'blog/post_detail.html', context)

サイドバーを追加した結果

すべてのコードを追加して、サーバーを再起動してみましょう。

サイドバーが追加されたブログ

見てください!これがincludeの力です:

  • サイドバーがすべてのページに自動的に表示される
  • カテゴリー、最近の投稿、アーカイブが綺麗に整理されている
  • レスポンシブ対応で、スマホではサイドバーが下に移動する
  • 一つのファイル(sidebar.html)を修正するだけで、全ページのサイドバーが更新される

テンプレートの継承とincludeを組み合わせることで、こんなに柔軟でメンテナンスしやすい構造が作れるんです!

カスタムテンプレートタグを作る(発展)

Django の組み込みタグだけでは足りない場合、自分でタグを作ることもできます。

テンプレートタグ用のディレクトリを作成

blog/ ├── templatetags/ │ ├── __init__.py # 空ファイル │ └── blog_extras.py

簡単なカスタムタグを作成

blog/templatetags/blog_extras.pyを作成:

blog/templatetags/blog_extras.py
from django import template from datetime import datetime register = template.Library() @register.simple_tag def current_year(): """現在の年を返す""" return datetime.now().year @register.simple_tag def greeting(): """時間帯に応じた挨拶を返す""" hour = datetime.now().hour if 5 <= hour < 12: return "おはようございます" elif 12 <= hour < 17: return "こんにちは" elif 17 <= hour < 21: return "こんばんは" else: return "おやすみなさい" @register.filter def add_mark(value, mark="!"): """文字列の最後にマークを追加""" return f"{value}{mark}"

カスタムタグを使う

テンプレートで使用する例:

blog/templates/blog/base.htmlを修正:

blog/templates/blog/base.html:
{% load static %} {% load blog_extras %} <!DOCTYPE html> <html lang="ja"> <!-- 省略 --> <footer> <p>&copy; {% current_year %} My Blog. All rights reserved.</p> <p>{% greeting %}、訪問ありがとうございます{{ ""|add_mark }}</p> </footer>
Important

{% load %}タグの記述位置

{% load blog_extras %}テンプレートファイルの先頭に書きます。

  • {% load static %}の後に追加するのが一般的
  • {% extends %}がある場合は、その直後に書く
  • 各テンプレートファイルごとに記述が必要(継承されない)

例:

{% extends 'blog/base.html' %} {% load blog_extras %} {% block content %} <!-- ここでカスタムタグが使える --> {% endblock %}
Note

カスタムタグを作った後は

開発サーバーを再起動する必要があります

ターミナル
$ python manage.py runserver

実践:完成度の高いブログテンプレートを作ろう

今まで学んだことをすべて組み合わせて、プロフェッショナルなブログを作ってみましょう。

ホームページを改良

blog/views.pypost_list関数を改良:

blog/views.py
from django.shortcuts import render # 共通の記事データ POSTS = [ { "id": 1, "title": "Djangoを始めました", "content": "Djangoの学習を始めました。楽しいです!", "created_at": "2025-07-01", "category": "Django", "tags": ["Python", "Django", "初心者"], "is_featured": True, "body": """今日からDjangoの学習を始めました。 Pythonは少し触ったことがあったけど、Webフレームワークは初めてです。 最初は難しそうだと思ったけど、チュートリアルが分かりやすくて助かります。 これからブログアプリを作っていきたいと思います!""", }, { "id": 2, "title": "ビューについて学んだこと", "content": "今日はビューについて学びました。MVTパターンの理解が深まりました。", "created_at": "2025-07-02", "category": "Django", "tags": ["Django", "View", "MVT"], "is_featured": False, "body": """Djangoのビューは、リクエストを受け取ってレスポンスを返す関数です。 MVTパターンのVに当たる部分で、ビジネスロジックを担当します。 テンプレートにデータを渡す方法も学びました。 contextという辞書を使うのが面白いです。""", }, { "id": 3, "title": "テンプレートは便利", "content": "テンプレートを使うとHTMLが書きやすいです。継承システムが特に便利!", "created_at": "2025-07-03", "category": "Django", "tags": ["Django", "Template"], "is_featured": True, "body": """今日はテンプレートについて学習しました。 変数の表示、forループ、if文など、基本的な機能を試しました。 特に継承システムが素晴らしいです! 共通部分を一箇所にまとめられるのは、とても効率的ですね。""", }, ] def post_list(request): # 注目記事を抽出 featured_posts = [p for p in POSTS if p.get("is_featured", False)] context = { "posts": POSTS, "featured_posts": featured_posts, "total_posts": len(POSTS), } return render(request, "blog/post_list.html", context) def post_detail(request, post_id): # 指定されたIDの記事を探す post = None for p in POSTS: if p["id"] == post_id: post = p break context = {"post": post, "posts": POSTS} return render(request, "blog/post_detail.html", context)

より洗練されたテンプレート

blog/templates/blog/post_list.htmlを修正:

blog/templates/blog/post_list.html
{% extends 'blog/base.html' %} {% load blog_extras %} {% block title %}ホーム - My Blog{% endblock %} {% block content %} <!-- ヒーローセクション --> <section class="hero"> <h1>Welcome to My Blog</h1> <p>{% greeting %}!Djangoで作られたブログへようこそ。</p> </section> <!-- 注目記事 --> {% if featured_posts %} <section class="featured-section"> <h2>注目記事</h2> <div class="featured-grid"> {% for post in featured_posts %} <div class="featured-post"> <h3>{{ post.title }}</h3> <p>{{ post.content|truncatechars:100 }}</p> <div class="tags"> {% for tag in post.tags %} <span class="tag">#{{ tag }}</span> {% endfor %} </div> <a href="{% url 'post_detail' post_id=post.id %}" class="btn btn-primary"> 読む </a> </div> {% endfor %} </div> </section> {% endif %} <!-- 最新記事 --> <section class="posts-section"> <h2>最新記事</h2> <div class="posts-grid"> {% for post in posts %} {% include 'blog/includes/post_card.html' %} {% empty %} <p class="no-posts">まだ記事がありません。</p> {% endfor %} </div> </section> {% endblock %}

blog/templates/blog/includes/post_card.htmlを修正:

blog/templates/blog/includes/post_card.html
<article class="post-card"> <div class="post-card-header"> <span class="category">{{ post.category }}</span> <span class="date">{{ post.created_at }}</span> </div> <h3 class="post-card-title"> <a href="{% url 'post_detail' post_id=post.id %}"> {{ post.title }} </a> </h3> <p class="post-card-excerpt"> {{ post.content|truncatechars:120 }} </p> <div class="post-card-footer"> <div class="tags"> {% for tag in post.tags %} <span class="tag">#{{ tag }}</span> {% endfor %} </div> <a href="{% url 'post_detail' post_id=post.id %}" class="read-more"> 続きを読む → </a> </div> </article>

最終的なスタイルも追加:

Note

CSSファイルの完全版

これまでに追加したCSSをすべて含む、最終的なblog/static/blog/css/style.cssの全体を以下に示します。

もしすでにCSSを書いている場合は、以下の内容で置き換えてください。

blog/static/blog/css/style.css
/* リセットとベーススタイル */ * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f4f4f4; display: flex; flex-direction: column; } /* ヘッダースタイル */ header { background-color: #2c3e50; color: white; padding: 1rem 0; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } header nav { max-width: 1200px; margin: 0 auto; padding: 0 2rem; display: flex; justify-content: space-between; align-items: center; } header h1 { margin: 0; line-height: 1; } header h1 a { color: white; text-decoration: none; font-size: 1.8rem; display: flex; align-items: center; gap: 0.5rem; } header h1 img { height: 40px; width: auto; display: block; } header ul { list-style: none; display: flex; gap: 2rem; margin: 0; padding: 0; } header ul a { color: white; text-decoration: none; transition: opacity 0.3s; } header ul a:hover { opacity: 0.8; } /* メインコンテンツ */ main { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; flex: 1; width: 100%; } /* h2タグのマージン調整 */ main h2 { margin-bottom: 1.5rem; } /* フッター */ footer { background-color: #34495e; color: white; text-align: center; padding: 1rem 0; } /* コンテナとレイアウト */ .container { display: grid; grid-template-columns: 1fr 300px; gap: 2rem; } .content { /* グリッドレイアウトのバグ対策 */ min-width: 0; } /* サイドバー */ .sidebar { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); height: fit-content; position: sticky; top: 2rem; } .sidebar-section { margin-bottom: 2rem; } .sidebar-section:last-child { margin-bottom: 0; } .sidebar h3 { color: #2c3e50; font-size: 1.1rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #3498db; } .sidebar ul { list-style: none; } .sidebar li { margin-bottom: 0.5rem; } .sidebar a { color: #7f8c8d; text-decoration: none; transition: color 0.3s; } .sidebar a:hover { color: #3498db; } /* ヒーローセクション */ .hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4rem 2rem; text-align: center; border-radius: 8px; margin-bottom: 3rem; } .hero h1 { font-size: 2.5rem; margin-bottom: 1rem; } /* 注目記事 */ .featured-section { margin-bottom: 3rem; } .featured-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; } .featured-post { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); border-top: 4px solid #3498db; } /* ボタン */ .btn { display: inline-block; padding: 0.5rem 1.5rem; text-decoration: none; border-radius: 4px; transition: all 0.3s; } .btn-primary { background: #3498db; color: white; } .btn-primary:hover { background: #2980b9; transform: translateY(-1px); } /* タグ */ .tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 1rem 0; } .tag { background: #ecf0f1; color: #7f8c8d; padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.85rem; } /* 記事一覧ページのスタイル */ .posts-section h2 { color: #2c3e50; margin-bottom: 2rem; } .no-posts { text-align: center; padding: 3rem; background: white; border-radius: 8px; color: #7f8c8d; } /* ポストカード */ .posts-grid { display: grid; gap: 2rem; } .post-card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: all 0.3s; } .post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); } .post-card-header { display: flex; justify-content: space-between; margin-bottom: 1rem; font-size: 0.9rem; } .category { background: #3498db; color: white; padding: 0.25rem 0.75rem; border-radius: 4px; } .date { color: #7f8c8d; } .post-card-title { margin-bottom: 1rem; } .post-card-title a { color: #2c3e50; text-decoration: none; font-size: 1.5rem; font-weight: bold; } .post-card-title a:hover { color: #3498db; } .post-card-excerpt { color: #555; margin-bottom: 1.5rem; line-height: 1.6; } .post-card-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem; } .read-more { color: #3498db; text-decoration: none; font-weight: 500; transition: all 0.3s; } .read-more:hover { color: #2980b9; transform: translateX(3px); } /* 記事詳細ページのスタイル */ .post-detail { background: white; padding: 3rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .post-detail h2 { color: #2c3e50; margin-bottom: 1rem; font-size: 2rem; } .post-meta { color: #7f8c8d; font-size: 0.9rem; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #ecf0f1; } .post-body { line-height: 1.8; color: #333; font-size: 1.1rem; } .post-body p { margin-bottom: 1.5rem; } .post-detail>p:last-child { margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #ecf0f1; } /* レスポンシブ対応 */ @media (max-width: 768px) { .container { grid-template-columns: 1fr; } .sidebar { position: static; margin-top: 2rem; } .hero h1 { font-size: 2rem; } .featured-grid { grid-template-columns: 1fr; } header nav { flex-direction: column; gap: 1rem; } header ul { gap: 1rem; } }

完成したブログアプリケーション

ここまでの作業で、素晴らしいブログアプリケーションが完成しました!最終的にどのような見た目になったか、確認してみましょう。

記事一覧ページ

完成した記事一覧ページ

ヒーローセクション、注目記事、記事カード、サイドバーなど、すべての要素が美しく配置されています。最初の「Hello Django!」から、ここまで成長しました!

記事詳細ページ

完成した記事詳細ページ

記事の詳細ページも、読みやすく洗練されたデザインになりました。サイドバーには関連情報が表示され、本格的なブログサイトの雰囲気が出ています。

よくあるトラブルと解決方法

静的ファイルが読み込まれない

症状:CSS や JavaScript が反映されない

解決方法

  1. {% load static %}がテンプレートの最初にあるか確認
  2. {% static 'パス' %}の記述が正しいか確認
  3. ファイルが正しい場所にあるか確認(static/blog/...
  4. 開発サーバーを再起動
  5. ブラウザのキャッシュをクリア

テンプレートが見つからない

症状TemplateDoesNotExistエラー

解決方法

  1. テンプレートのパスが正しいか確認
  2. templates/blog/の構造を確認
  3. settings.pyINSTALLED_APPSにアプリが登録されているか確認

カスタムタグが使えない

症状Invalid block tagエラー

解決方法

  1. {% load タグファイル名 %}を忘れていないか確認
  2. templatetagsフォルダに__init__.pyがあるか確認
  3. 開発サーバーを再起動

まとめ

今回は、Django テンプレートの強力な機能について学びました:

  • テンプレートの継承で効率的にコードを書く方法
  • 静的ファイルでページを美しくする方法
  • 便利なタグとフィルタで表現力を高める方法
  • includeで部品を再利用する方法
  • カスタムタグで機能を拡張する方法

現場での開発について

ここで大切なことをお伝えします。実際の現場の開発では、最初から完璧なコードを書くことはほとんどありません

今回の記事を振り返ってみてください:

  1. 最初は単純なHTMLから始めました
  2. テンプレート継承を導入してコードを整理しました
  3. CSSを追加して見た目を改善しました
  4. JavaScriptで動きを加えました
  5. includeで部品化して、さらに整理しました

このように、必要最小限の機能から始めて、徐々にリファクタリング(改善)していくのが、実際の開発現場でも一般的なアプローチです。

Tip

リファクタリングとは?

リファクタリング(Refactoring)は、コードの外部的な振る舞いを変えずに、内部構造を改善することです。

今回の例:

  • 各ページに同じHTMLを書いていた → テンプレート継承で共通化
  • スタイルをHTMLに直接書いていた → 外部CSSファイルに分離
  • 同じような構造が繰り返されていた → includeで部品化

これらはすべてリファクタリングの例です!

あなたへのメッセージ

最初は「覚えることが多い...」と感じたかもしれません。でも、ここまでついてきたあなたは、本当にすごいです!

思い出してください。最初は「Hello Django!」という文字を表示するだけでした。それが今では、プロフェッショナルな見た目のブログサイトを作れるようになったんです。この成長は、あなたの努力の結果です。

プログラミングは、小さな一歩の積み重ねです。今日できなかったことが、明日にはできるようになる。その喜びを感じながら、一緒に進んでいきましょう。

そして、CSS を適用した瞬間の感動はどうでしたか?地味だったページが、一気にプロフェッショナルな見た目になりましたね。これが Web デザインの醍醐味です。

完璧を目指す必要はありません。大切なのは、動くものを作り、少しずつ改善していくこと。これができるようになったあなたは、もう立派な開発者の仲間入りです!

完成したコード

もし実行でエラーが出たり、不明な点がある場合は、以下の完成後のコードと比較してみてください:

https://github.com/techarm/django-blog-tutorial/tree/05-templates

特にbase.htmlの継承構造、settings.pyの静的ファイル設定、CSSファイルの配置を確認することで、問題を解決できるでしょう。

次回予告

次回は、いよいよデータベースモデルについて学びます!

今まではビューに直接データを書いていましたが、実際の Web アプリケーションでは、データベースにデータを保存します。

  • モデルの作成方法
  • データベースのマイグレーション
  • 管理画面でのデータ操作
  • モデルとビューの連携

データベースを使えるようになると、本格的な Web アプリケーションが作れるようになります。楽しみにしていてくださいね!

ここまで一緒に学んできたあなたなら、必ずマスターできます。自信を持って進みましょう!🚀

この記事はいかがでしたか?

もしこの記事が参考になりましたら、
高評価をいただけると大変嬉しいです!

--

皆様からの応援が励みになります。ありがとうございます! ✨

Django超初心者シリーズについて

このシリーズでは、プログラミング初心者の方でもDjangoを使ったWebアプリケーション開発ができるようになることを目標としています。わからないことがあれば、コメント欄でお気軽にご質問ください!