ビューとテンプレート — 記事リストのレンダリング

この章では、Django ビューを使ってデータベースからデータを取得し、テンプレートと組み合わせてブログのホームページの記事カードリストをレンダリングする方法を学びます。


ビュー(View)の役割

ビューは Django の中核ロジック層であり、以下を担当します:

  • ユーザーのリクエスト(requestパラメータ)
  • データベースから必要なデータをクエリする
  • データをテンプレートに渡して HTML をレンダリングし、ユーザーに返す

Django ビューには2つの書き方があります:関数ビュー(FBV)とクラスビュー(CBV)。

これまでの8章では FBV を優先的に使用してきました。FBV はより直感的で、初心者がリクエスト処理の流れを理解するのに適しています。


関数ビューの基本

Django のビュー関数は、requestパラメータを受け取り、HttpResponseオブジェクトを返します。

実例

# ファイルパス:blog/views.py
from django.shortcuts import render
from .models import Post

def index(request):
    """ブログホームページ:すべての記事を表示"""
    # データベースからすべての記事をクエリ(時間の降順)
    posts = Post.objects.all().order_by('-created_at')

    # render の3つのパラメータ:
    # 1. request — リクエストオブジェクト
    # 2. テンプレートパス — blog/index.html
    # 3. context 辞書 — テンプレートに渡すデータ
    context = {
        'posts': posts,
        'title': 'サンプル ブログ - ホームページ'
    }
    return render(request, 'blog/index.html', context)

render()は Django で最もよく使われるショートカット関数で、テンプレートのロード → データの注入 → HTML の生成 → レスポンスの返却を組み合わせています。


Django テンプレート構文

Django テンプレート構文は Vue3 のテンプレート構文と非常に似ていますが、いくつかの重要な違いがあります。

用途Django テンプレートVue3 テンプレート
変数の出力{{ variable }}{{ variable }}
ループ{% for item in list %}...{% endfor %}v-for="item in list"
条件{% if cond %}...{% endif %}v-if="cond"
URL 生成{% url 'name' %}router-link to="name"
レイアウトの継承{% extends 'base.html' %}コンポーネントのネスト

重要な違い:Django テンプレートは{% %}制御タグを包みます。Vue3 はv-ディレクティブを使用します。サーバーサイドでレンダリングされます。HTML が生成されてからブラウザに送信されます。Vue3 はブラウザサイドでレンダリングされます。ブラウザが JS を受け取った後に HTML を組み立てます。


テンプレート継承:base.html

複数のページで同じナビゲーションバーとフッターを共有するには、テンプレート継承を使って重複を避けます。

親テンプレートは「ブロック(block)」を定義し、子テンプレートが具体的な内容を埋め込みます。

base.html 親テンプレートを作成

実例

<!-- ファイルパス:blog/templates/blog/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>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f5f5;
            color: #333;
        }
        .navbar {
            display: flex; justify-content: space-between; align-items: center;
            padding: 16px 40px; background: #fff;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .navbar .logo { font-size: 22px; font-weight: bold; color: #2c3e50; text-decoration: none; }
        .navbar a { margin-left: 20px; text-decoration: none; color: #333; }
        .container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
        .footer { text-align: center; padding: 20px; color: #999; border-top: 1px solid #eee; }
        .section-title { font-size: 24px; margin-bottom: 20px; }
    </style>
    {% block extra_head %}{% endblock %}
</head>
<body>
    <!-- ナビゲーションバー -->
    <header class="navbar">
        <a href="/" class="logo">サンプル ブログ</a>
        <nav>
            <a href="/">ホーム</a>
            <a href="#">概要</a>
        </nav>
    </header>

    <!-- メインコンテンツ領域(子テンプレートが埋め込み) -->
    <main class="container">
        {% block content %}
        {% endblock %}
    </main>

    <!-- フッター -->
    <footer class="footer">
        <p>© 2024 サンプル ブログ. Powered by Django.</p>
    </footer>
</body>
</html>

index.html 子テンプレートを作成

実例

<!-- ファイルパス:blog/templates/blog/index.html -->
<!-- extends は必ず先頭に置く必要があります -->
{% extends 'blog/base.html' %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<h2 class="section-title">最新の記事</h2>

{% if posts %}
    <div class="article-grid">
        {% for post in posts %}
        <div class="article-card">
            <div class="card-content">
                <span class="card-category">{{ post.category.name }}</span>
                <h3>{{ post.title }}</h3>
                <p>{{ post.summary|truncatechars:80 }}</p>
                <span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
            </div>
        </div>
        {% endfor %}
    </div>
{% else %}
    <p class="empty-tip">まだ記事がありません。お楽しみに。</p>
{% endif %}
{% endblock %}

テンプレートで使ったいくつかの知識ポイント:

構文意味
{% extends 'blog/base.html' %}親テンプレートを継承。必ず最初の行に書く
{% block content %}...{% endblock %}親テンプレートで定義された content ブロックを埋め込む
{{ post.category.name }}外部キーを介して関連オブジェクトの属性にアクセス
{{ post.summary|truncatechars:80 }}フィルター:先頭80文字を切り出す
{{ post.created_at|date:"Y-m-d" }}フィルター:日付をフォーマット

Django テンプレートのフィルターはパイプ記号|で表し、変数の後に続けて出力をフォーマットします。よく使われるフィルター:date、truncatechars、length、default。


記事リストのスタイルを追加

base.html の style タグに以下の CSS を追加します:

実例

/* 記事カードのグリッドレイアウト */
.article-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 24px;
}

.article-card {
    background: #fff;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 12px rgba(0,0,0,0.08);
    transition: transform 0.2s, box-shadow 0.2s;
}

.article-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}

.card-content {
    padding: 20px;
}

.card-category {
    display: inline-block;
    padding: 2px 10px;
    background: #E8EEF5;
    color: #3D71A6;
    border-radius: 12px;
    font-size: 12px;
    margin-bottom: 8px;
}

.article-card h3 {
    font-size: 18px;
    margin-bottom: 8px;
    color: #222;
}

.article-card p {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-bottom: 12px;
}

.card-date {
    font-size: 12px;
    color: #999;
}

.empty-tip {
    text-align: center;
    color: #999;
    padding: 60px 0;
    font-size: 16px;
}

実践:ホームページの効果を確認

以下の点が完了していることを確認してください:

  • models.py で Post と Category モデルを定義し、migrate 済みである
  • Admin バックエンドに少なくとも3〜5件の記事(異なるカテゴリに属する)が登録されている
  • views.py の index 関数がデータベースから記事をクエリしてテンプレートに渡す
  • urls.py で / ルートが index ビューを指している

サーバーを起動してホームページにアクセスすると、データベースのデータから動的にレンダリングされた記事カードリストが表示されるはずです。


本章のまとめ

この章では、Django ビューとテンプレートの核となる連携を習得しました:views.py がデータベースからデータをクエリして context に渡す、render() がテンプレートをレンダリングする、base.html 親テンプレートがレイアウト + block を定義する、子テンプレートが extends で継承して内容を埋め込む、Django テンプレート構文({{ }}、{% %}、フィルター)。

これでブログのホームページはデータベース駆動になり、ハードコーディングではなくなりました。

その他の拡張