FlaskテンプレートエンジンJinja2の基礎

2025-12-15

はじめに

前回までにFlaskの環境構築とルーティングについて学びました。今回は、Webアプリケーションの見た目を担当する「テンプレートエンジンJinja2」について詳しく解説します。Jinja2を使うことで、PythonコードとHTMLを分離し、よりメンテナンス性の高いWebアプリケーションを作成できるようになります。

テンプレートエンジンとは?

テンプレートエンジンの必要性

これまで私たちは、Pythonコードの中にHTMLを文字列として直接記述してきました。

@app.route('/')
def index():
    return '''
    <html>
        <body>
            <h1>Hello World</h1>
        </body>
    </html>
    '''

この方法には以下の問題点があります。

  • HTMLの編集が困難
  • デザイナーと開発者の協業がしにくい
  • コードの見通しが悪い
  • 繰り返し部分の管理が大変

Jinja2の解決策

Jinja2(ジンジャツー)は、Pythonで使われる高速で柔軟なテンプレートエンジンで、HTMLなどのテンプレートに変数や制御構文を埋め込み、動的なページやテキストを効率よく生成するための仕組みです。以下の機能を提供します。

  • 変数の埋め込み
  • 制御構造(条件分岐、ループ)
  • テンプレートの継承
  • フィルターと関数

基本的なディレクトリ構造

まず、テンプレートを使用するための適切なプロジェクト構成を作成しましょう。

myflaskapp/
│
├── venv/                 # 仮想環境
├── app.py               # メインアプリケーションファイル
├── templates/           # テンプレートディレクトリ
│   ├── base.html       # ベーステンプレート
│   ├── index.html      # ホームページ
│   ├── about.html      # 会社情報ページ
│   └── users.html      # ユーザー一覧ページ
└── static/             # 静的ファイル
    ├── css/
    │   └── style.css
    ├── js/
    └── images/

ディレクトリ作成

WindowsもmacOSもmkdirコマンドで必要なフォルダを作成します。

Windows (PowerShell):

mkdir templates, static, static\css, static\js, static\images

Mac (Terminal):

mkdir -p templates static/{css,js,images}

テンプレートの基本構文

最初のテンプレート

まず、最も基本的なテンプレートから始めましょう。

templates/index.html

<!DOCTYPE html>
<html>
<head>
    <title>私のFlaskアプリ</title>
</head>
<body>
    <h1>ようこそ!</h1>
    <p>これは私の最初のJinja2テンプレートです。</p>
</body>
</html>

Flask アプリでブラウザにアクセスしたとき、「ようこそ!」という見出しと、説明文を表示するシンプルなテンプレート です。

テンプレートのレンダリング

テンプレートを表示するには、render_template関数を使用します。

app.py

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

FlaskでWeb アプリを動かし、index.html を表示するための基本的な構成 を示しています。

基本的な構文ルール

Jinja2では、以下の特殊な記号を使用します。

  • {{ ... }} – 変数の表示
  • {% ... %} – 制御構造(if, for, extendsなど)
  • {# ... #} – コメント

単純な変数の受け渡し

Pythonからテンプレートに変数を渡す方法です。

app.py

トップページにアクセスされたときに「田中太郎」という名前と「25」という年齢の値を用意し、それらを nameage という名前で index.html テンプレートに渡して表示させるための処理を行っています。

@app.route('/')
def index():
    user_name = "田中太郎"
    user_age = 25
    return render_template('index.html', 
                         name=user_name, 
                         age=user_age)

templates/index.html

Python側から渡された nameage の値をそのままページ内に表示するためのHTMLファイルで、ブラウザを開くと「ユーザー情報」という見出しの下に、受け取った名前と年齢が差し込まれて表示される仕組みになっています。

<!DOCTYPE html>
<html>
<head>
    <title>ユーザー情報</title>
</head>
<body>
    <h1>ユーザー情報</h1>
    <p>名前: {{ name }}</p>
    <p>年齢: {{ age }}</p>
</body>
</html>

複雑なデータ構造の受け渡し

app.py

/user にアクセスされたときに「佐藤由紀子」というユーザー情報をまとめた辞書を作り、その辞書を user という名前でテンプレート user.html に渡し、ページ側で名前や年齢、メールアドレス、趣味の一覧を表示できるようにする処理を行っています。

@app.route('/user')
def show_user():
    user_data = {
        'name': '佐藤由紀子',
        'age': 30,
        'email': 'hanako@example.com',
        'hobbies': ['読書', '旅行', '料理']
    }
    return render_template('user.html', user=user_data)

templates/user.html

Python側から渡された user という辞書の中身を取り出して画面に表示するためのもので、ページを開くと名前・年齢・メールアドレスがそのまま挿入され、趣味についてはリストになっている項目をカンマ区切りの文字列に変換してまとめて表示するようになっています。

<!DOCTYPE html>
<html>
<head>
    <title>ユーザー詳細</title>
</head>
<body>
    <h1>ユーザー詳細情報</h1>
    <p>名前: {{ user.name }}</p>
    <p>年齢: {{ user.age }}</p>
    <p>メール: {{ user.email }}</p>
    <p>趣味: {{ user.hobbies|join(', ') }}</p>
</body>
</html>

変数のエスケープ

Jinja2はデフォルトでHTMLエスケープを行います。

app.py

/escape にアクセスされたときに、HTMLタグを含んだ2つのテキストを用意し、それぞれを safeunsafe という名前でテンプレートに渡して表示させるための処理を行っています。safeunsafe の内容はどちらも <b> タグを含んでいますが、テンプレート側でそのまま表示するか、エスケープしてタグを無効化するかを区別できるようにするために用意された例になっています。

@app.route('/escape')
def escape_example():
    safe_text = "これは安全なテキストです"
    unsafe_text = "これは危険なテキストです"
    return render_template('escape.html', 
                         safe=safe_text, 
                         unsafe=unsafe_text)

templates/escape.html

Python側から渡されたテキストを、エスケープの有無によってどのように表示が変わるかを確認するためのもので、自動エスケープが有効な場合には <b> タグが文字としてそのまま表示され、safe フィルターを使うとタグが機能して太字として表示されるようになっており、通常のテキストとして渡した内容もそのまま画面に表示される仕組みになっています。

<!DOCTYPE html>
<html>
<head>
    <title>エスケープの例</title>
</head>
<body>
    <h1>エスケープの例</h1>

    <h2>安全な表示(自動エスケープ):</h2>
    <p>{{ unsafe }}</p>

    <h2>安全でない表示(エスケープ無効):</h2>
    <p>{{ unsafe|safe }}</p>

    <h2>通常の表示:</h2>
    <p>{{ safe }}</p>
</body>
</html>

制御構造

条件分岐 (if文)

app.py

/conditional にアクセスされたときに「鈴木一郎」というユーザー情報を含む辞書を作り、その中にログイン状態や役割(admin)が入ったまま user としてテンプレートに渡し、ページ側でユーザーがログインしているかどうか、あるいは管理者かどうかといった条件に応じて表示内容を切り替えられるようにしている処理です。

@app.route('/conditional')
def conditional_example():
    user = {
        'name': '鈴木一郎',
        'is_logged_in': True,
        'role': 'admin'
    }
    return render_template('conditional.html', user=user)

templates/conditional.html

渡されたユーザー情報をもとにログイン状態やユーザーの役割に応じて画面に表示するメッセージやスタイルを切り替える仕組みになっており、ログインしていれば名前を表示しながら管理者・一般ユーザー・ゲストのどれに該当するかで文言を変え、ログインしていない場合にはログインを促す内容が表示されるようになっています。

<!DOCTYPE html>
<html>
<head>
    <title>条件分岐の例</title>
    <style>
        .admin { color: red; font-weight: bold; }
        .user { color: blue; }
        .guest { color: gray; }
    </style>
</head>
<body>
    <h1>ユーザー状態</h1>

    {% if user.is_logged_in %}
        <p class="success">ログイン中: {{ user.name }}</p>

        {% if user.role == 'admin' %}
            <p class="admin">管理者権限があります</p>
        {% elif user.role == 'user' %}
            <p class="user">一般ユーザーです</p>
        {% else %}
            <p class="guest">ゲストユーザーです</p>
        {% endif %}

    {% else %}
        <p class="warning">ログインしていません</p>
        <a href="/login">ログイン</a>
    {% endif %}
</body>
</html>

ループ (for文)

app.py

/products にアクセスされたときに複数の商品の情報を一覧として用意し、そのリストを products という名前でテンプレートに渡して、ページ側で商品名や価格、在庫の有無などを繰り返し処理によって表示できるようにしている処理です。

@app.route('/products')
def products():
    products_list = [
        {'id': 1, 'name': 'ノートパソコン', 'price': 99800, 'in_stock': True},
        {'id': 2, 'name': 'マウス', 'price': 2500, 'in_stock': True},
        {'id': 3, 'name': 'キーボード', 'price': 5800, 'in_stock': False},
        {'id': 4, 'name': 'モニター', 'price': 32800, 'in_stock': True},
        {'id': 5, 'name': 'Webカメラ', 'price': 7200, 'in_stock': False}
    ]
    return render_template('products.html', products=products_list)

templates/products.html

渡された商品のリストを表形式で表示するためのもので、商品ごとにIDや名前、価格、在庫状況を1行ずつ並べ、在庫がある場合は緑色、在庫がない場合は赤色といったようにスタイルを変えつつ、行番号によって背景色を切り替えることで見やすい一覧として表示できるように構成されています。また、ループの最後には商品数を表示する仕組みも含まれています。

<!DOCTYPE html>
<html>
<head>
    <title>商品一覧</title>
    <style>
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #f2f2f2; }
        .in-stock { color: green; }
        .out-of-stock { color: red; }
        .even { background-color: #f9f9f9; }
    </style>
</head>
<body>
    <h1>商品一覧</h1>

    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>商品名</th>
                <th>価格</th>
                <th>在庫状況</th>
            </tr>
        </thead>
        <tbody>
            {% for product in products %}
            <tr class="{% if loop.index % 2 == 0 %}even{% else %}odd{% endif %}">
                <td>{{ product.id }}</td>
                <td>{{ product.name }}</td>
                <td>{{ product.price }}円</td>
                <td class="{% if product.in_stock %}in-stock{% else %}out-of-stock{% endif %}">
                    {% if product.in_stock %}
                        ○ 在庫あり
                    {% else %}
                        ✗ 在庫切れ
                    {% endif %}
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>

    <h2>ループ変数の情報</h2>
    <p>商品数: {{ products|length }}</p>
</body>
</html>

ループ変数

forループ内では、以下の特別な変数が利用できます。

templates/loop_vars.html

Jinja2 のループ機能を使って「りんご」「バナナ」「オレンジ」の3つを順番に表示しながら、各アイテムについてループ専用の変数を利用して、現在の位置や総回数、最初や最後の要素かどうかといった情報を同時に画面に表示する仕組みになっています。

<!DOCTYPE html>
<html>
<head>
    <title>ループ変数</title>
</head>
<body>
    <h1>ループ変数の例</h1>

    <ul>
    {% for item in ['りんご', 'バナナ', 'オレンジ'] %}
        <li>
            アイテム: {{ item }}<br>
            インデックス: {{ loop.index }}<br>
            0始まりインデックス: {{ loop.index0 }}<br>
            反復回数: {{ loop.length }}<br>
            最初の要素: {{ loop.first }}<br>
            最後の要素: {{ loop.last }}<br>
            <hr>
        </li>
    {% endfor %}
    </ul>
</body>
</html>

フィルター

Jinja2フィルターは変数を変換またはフォーマットするための関数です。

基本的なフィルター

app.py

/filters にアクセスされたときに文字列や数値、リスト、HTML文字列、現在時刻といったさまざまな種類のデータをまとめて用意し、それらを data という名前でテンプレートに渡すことで、Jinja2 のフィルターを使った加工や整形の例をページ側で確認できるようにするための処理です。

@app.route('/filters')
def filters_example():
    sample_data = {
        'title': 'hello world',
        'description': 'This is a LONG description that needs to be shortened.',
        'price': 1234.5678,
        'items': ['apple', 'banana', 'cherry'],
        'html_content': '<p>This is <strong>HTML</strong> content</p>',
        'now': datetime.now()
    }
    return render_template('filters.html', data=sample_data)

templates/filters.html

Jinja2 のさまざまなフィルターを実際のデータに適用するとどのような結果になるのかを一覧で確認できるようにした例で、文字列の変換や短縮、リストの結合、数値の整形、未定義値へのデフォルト設定、HTMLを安全にそのまま表示する処理、さらには日付のフォーマットまでを表形式で分かりやすく示す構成になっています。

<!DOCTYPE html>
<html>
<head>
    <title>フィルターの例</title>
</head>
<body>
    <h1>Jinja2フィルターの例</h1>

    <table border="1">
        <tr>
            <th>フィルター</th>
            <th>結果</th>
        </tr>

        <tr>
            <td>capitalize</td>
            <td>"{{ data.title }}" → "{{ data.title|capitalize }}"</td>
        </tr>

        <tr>
            <td>upper</td>
            <td>"{{ data.title }}" → "{{ data.title|upper }}"</td>
        </tr>

        <tr>
            <td>lower</td>
            <td>"{{ data.title|upper }}" → "{{ data.title|upper|lower }}"</td>
        </tr>

        <tr>
            <td>truncate</td>
            <td>"{{ data.description }}" → "{{ data.description|truncate(20) }}"</td>
        </tr>

        <tr>
            <td>length</td>
            <td>"{{ data.items }}" → {{ data.items|length }} 項目</td>
        </tr>

        <tr>
            <td>join</td>
            <td>"{{ data.items }}" → "{{ data.items|join(', ') }}"</td>
        </tr>

        <tr>
            <td>float</td>
            <td>"{{ data.price }}" → "{{ data.price|float }}"</td>
        </tr>

        <tr>
            <td>round</td>
            <td>"{{ data.price }}" → "{{ data.price|round(2) }}"</td>
        </tr>

        <tr>
            <td>currency format</td>
            <td>"{{ data.price }}" → "{{ data.price|round|int }}円"</td>
        </tr>

        <tr>
            <td>default</td>
            <td>"{{ undefined_var|default('デフォルト値') }}"</td>
        </tr>

        <tr>
            <td>safe (HTMLエスケープ無効)</td>
            <td>{{ data.html_content|safe }}</td>
        </tr>

        <tr>
            <td>datetime format</td>
            <td>"{{ data.now }}" → "{{ data.now.strftime('%Y年%m月%d日 %H:%M') }}"</td>
        </tr>
    </table>
</body>
</html>

カスタムフィルターの作成

独自のフィルターを作成することもできます。

app.py

Flask に独自のカスタムフィルターを追加し、それをテンプレート内で使えるようにした例で、金額を「¥1,234,567」のような日本向けの書式に変換するフィルターと、日付から「月・火・水…」といった日本語の曜日を取り出すフィルターを登録し、そのフィルターを利用できるように値(価格と現在日時)をテンプレートへ渡して表示させるしくみになっています。

from flask import Flask, render_template
from datetime import datetime

app = Flask(__name__)

# カスタムフィルターの登録
@app.template_filter('format_currency')
def format_currency(value):
    return f"¥{value:,.0f}"

@app.template_filter('japanese_weekday')
def japanese_weekday(date):
    weekdays = ['月', '火', '水', '木', '金', '土', '日']
    return weekdays[date.weekday()]

@app.route('/custom-filters')
def custom_filters():
    return render_template('custom_filters.html', 
                         price=1234567, 
                         today=datetime.now())

if __name__ == '__main__':
    app.run(debug=True)

templates/custom_filters.html

Flask側で登録したカスタムフィルターを実際に使って表示するためのページで、渡された priceformat_currency フィルターで「¥1,234,567」のような日本向けの通貨表記に変換し、today に対してはそのままの日付表示に加えて、japanese_weekday フィルターを使って「月・火・水…」といった日本語の曜日を取り出して併記するように作られています。

<!DOCTYPE html>
<html>
<head>
    <title>カスタムフィルター</title>
</head>
<body>
    <h1>カスタムフィルターの例</h1>

    <p>価格: {{ price|format_currency }}</p>
    <p>今日の曜日: {{ today }} ({{ today|japanese_weekday }}曜日)</p>
</body>
</html>

テンプレートの継承

ベーステンプレートの作成

テンプレート継承は、共通のレイアウトを定義し、各部分を子テンプレートで上書きできる機能です。

templates/base.html

Flask のアプリ全体で共通して使うレイアウトを定義した“親テンプレート”になっており、ページのタイトル、ヘッダー、ナビゲーションメニュー、メッセージ表示、メインコンテンツ部分、フッターなどの基本構造をまとめておき、子テンプレートが必要な部分だけを block を使って差し替えられるように作られています。ページ全体のデザインやレイアウトを一箇所で管理しつつ、個別ページは contenttitle などのブロックを上書きするだけで独自表示ができるようにする仕組みです。

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}マイサイト{% endblock %}</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        .header {
            background-color: #333;
            color: white;
            padding: 1rem;
        }
        .nav {
            background-color: #444;
            padding: 0.5rem;
        }
        .nav a {
            color: white;
            text-decoration: none;
            margin-right: 1rem;
        }
        .nav a:hover {
            text-decoration: underline;
        }
        .content {
            padding: 2rem;
            background-color: white;
            margin: 1rem;
            border-radius: 5px;
        }
        .footer {
            background-color: #333;
            color: white;
            text-align: center;
            padding: 1rem;
            margin-top: 2rem;
        }
        .messages {
            padding: 1rem;
            margin: 1rem;
            border-radius: 5px;
        }
        .success { background-color: #d4edda; color: #155724; }
        .error { background-color: #f8d7da; color: #721c24; }
        .warning { background-color: #fff3cd; color: #856404; }
    </style>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header class="header">
        <h1>{% block header %}マイFlaskアプリ{% endblock %}</h1>
    </header>

    <nav class="nav">
        <a href="/">ホーム</a>
        <a href="/about">会社情報</a>
        <a href="/users">ユーザー一覧</a>
        <a href="/products">商品一覧</a>
    </nav>

    <div class="messages">
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    <div class="{{ category }}">{{ message }}</div>
                {% endfor %}
            {% endif %}
        {% endwith %}
    </div>

    <main class="content">
        {% block content %}
        <!-- この部分は子テンプレートで上書きされます -->
        {% endblock %}
    </main>

    <footer class="footer">
        <p>&copy; 2024 マイFlaskアプリ. All rights reserved.</p>
        {% block footer %}
        <p>連絡先: info@example.com</p>
        {% endblock %}
    </footer>

    {% block extra_js %}{% endblock %}
</body>
</html>

子テンプレートの作成

templates/index.html

Flask + Jinja2 の「子テンプレート」 の典型的な使い方を示しており、base.html を継承しながら必要な部分だけカスタマイズしてホームページを作る例になっています。

{% extends "base.html" %}

{% block title %}ホーム - マイサイト{% endblock %}

{% block header %}ようこそ!{% endblock %}

{% block content %}
<h2>ホームページ</h2>
<p>これはホームページの内容です。</p>

<div class="features">
    <h3>特徴</h3>
    <ul>
        <li>レスポンシブデザイン</li>
        <li>ユーザーフレンドリー</li>
        <li>高速なパフォーマンス</li>
    </ul>
</div>

<div class="stats">
    <h3>統計情報</h3>
    <p>登録ユーザー数: 1,234人</p>
    <p>総訪問者数: 45,678人</p>
</div>
{% endblock %}

{% block footer %}
<p>特別なフッターコンテンツ - ホームページ限定</p>
{{ super() }}  <!-- 親テンプレートのフッター内容も表示 -->
{% endblock %}

templates/about.html

base.html を継承して会社情報ページを構成するための子テンプレートとして機能しています。まず最初に親テンプレートを継承し、そのうえで title ブロックと header ブロックを上書きすることで、このページ固有のタイトルと見出しを設定しています。続く content ブロックでは、会社紹介の文章や会社概要を表にまとめた内容、さらに企業としてのミッションが記述されており、この部分がページの主要コンテンツとなります。

{% extends "base.html" %}

{% block title %}会社情報 - マイサイト{% endblock %}

{% block header %}会社情報{% endblock %}

{% block content %}
<h2>私たちについて</h2>
<p>当社は革新的なWebソリューションを提供しています。</p>

<div class="company-info">
    <h3>会社概要</h3>
    <table>
        <tr>
            <th>会社名</th>
            <td>株式会社サンプル</td>
        </tr>
        <tr>
            <th>設立</th>
            <td>2020年1月</td>
        </tr>
        <tr>
            <th>所在地</th>
            <td>東京都渋谷区</td>
        </tr>
        <tr>
            <th>従業員数</th>
            <td>50名</td>
        </tr>
    </table>
</div>

<div class="mission">
    <h3>ミッション</h3>
    <p>お客様の成功を通じて社会に貢献します。</p>
</div>
{% endblock %}

{% block extra_css %}
<style>
    table {
        border-collapse: collapse;
        width: 100%;
    }
    th, td {
        border: 1px solid #ddd;
        padding: 8px;
        text-align: left;
    }
    th {
        background-color: #f2f2f2;
        width: 30%;
    }
</style>
{% endblock %}

また、最後に定義されている extra_css ブロックでは、このページだけに適用されるテーブルのスタイルが記述されており、セルの罫線や背景色、レイアウトに関するデザインを個別に調整しています。全体として、親テンプレートのレイアウト構造を保ちながら、このページ専用の内容とスタイルを柔軟に追加・変更するためのテンプレート構成になっています。

インクルードの使用

共通部分を別ファイルに分割してインクルードすることもできます。

templates/includes/navigation.html

このナビゲーション部分のコードは、サイト全体で共通して利用されるメニューを構成しており、ユーザーが主要なページへ素早く移動できるようにリンクを並べたものです。<nav> 要素はナビゲーション領域であることを示し、その中に複数の <a> タグが配置されています。リンク先はホーム、会社情報、ユーザー一覧、商品一覧、そしてお問い合わせページへと続いており、どれもサイト運営に必要な基本的コンテンツへアクセスするための導線となっています。スタイルクラスとして nav が指定されているため、親テンプレートや外部CSSでまとめてデザインが施され、サイト全体で統一感のあるナビゲーションバーとして表示されます。

<nav class="nav">
    <a href="/">ホーム</a>
    <a href="/about">会社情報</a>
    <a href="/users">ユーザー一覧</a>
    <a href="/products">商品一覧</a>
    <a href="/contact">お問い合わせ</a>
</nav>

ベーステンプレートでインクルードを使用

このコードは base.html の一部で、ヘッダーとナビゲーションをまとめてページに組み込む部分を示しています。<header> 内の header ブロックは子テンプレートで自由に上書きできるようになっており、デフォルトでは「マイFlaskアプリ」と表示されます。その下で {% include 'includes/navigation.html' %} によってナビゲーション用のHTMLファイルを挿入しており、共通のメニューを複数のページで再利用できるように構成されています。これにより、ヘッダーとナビゲーションの構造を一箇所で管理しつつ、子テンプレートごとにヘッダー内容だけを変えることが可能になります。

<!-- base.htmlの一部 -->
<body>
    <header class="header">
        <h1>{% block header %}マイFlaskアプリ{% endblock %}</h1>
    </header>

    {% include 'includes/navigation.html' %}

    <!-- 残りの内容 -->
</body>

ベストプラクティス

1. テンプレートの整理

  • 関連するテンプレートはグループ化
  • 共通部分はインクルードまたはベーステンプレートで管理
  • 命名規則を統一

2. ビジネスロジックとプレゼンテーションの分離

  • 複雑な計算はPythonコードで行う
  • テンプレートでは表示に専念
  • フィルターを使用して簡単な変換のみ行う

3. エラーハンドリング

Flask でカスタムエラーページを設定する部分です。@app.errorhandler(404) デコレーターは、存在しないページにアクセスしたときに呼び出され、先ほどの 404.html テンプレートを表示して HTTP ステータスコード 404 を返します。同様に、@app.errorhandler(500) はサーバー内部でエラーが発生した場合に 500.html を表示してステータスコード 500 を返すようになっています。これにより、ユーザーにわかりやすいエラーページを提供しつつ、標準のブラウザエラー画面ではなく自作のページでエラー内容を案内できるようになります。

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    return render_template('500.html'), 500

templates/404.html

存在しないページにアクセスした際に表示される 404 エラーページ用の子テンプレートです。base.html を継承して共通レイアウトを使いながら、タイトルを「ページが見つかりません」に変更し、content ブロック内でエラーメッセージと説明文を表示しています。さらにホームページへのリンクをボタンとして設置しており、ユーザーが簡単にトップページに戻れるように配慮された構成になっています。

{% extends "base.html" %}

{% block title %}ページが見つかりません{% endblock %}

{% block content %}
<div class="error-page">
    <h1>404 - ページが見つかりません</h1>
    <p>お探しのページは存在しないか、移動した可能性があります。</p>
    <a href={{ url_for('index') }} class="button">ホームに戻る</a>
</div>
{% endblock %}

実践的な例:ユーザー管理システム

app.py

Flask を使って簡易的なユーザー管理アプリを作成した例です。アプリはトップページとユーザー一覧ページを持ち、ユーザーごとの詳細ページや新規ユーザー追加ページも用意されています。ユーザー情報はサンプルデータとしてリストに保持され、user_detail では URL から取得した ID に基づいて対象ユーザーを検索し、存在しなければフラッシュメッセージを表示して一覧ページにリダイレクトします。add_user ではフォームから送信されたデータを受け取り、新しいユーザーをリストに追加した後、成功メッセージをフラッシュしてユーザー一覧に戻る処理が行われています。全体として、テンプレートのレンダリング、URL パラメータの取得、条件によるリダイレクト、フラッシュメッセージの活用といった Flask の基本機能を統合したシンプルなユーザー管理の流れを示しています。

from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = 'your_secret_key_here'  # フラッシュメッセージ用

# サンプルユーザーデータ
users = [
    {'id': 1, 'name': '田中太郎', 'email': 'taro@example.com', 'role': 'admin', 'active': True},
    {'id': 2, 'name': '佐藤由紀子', 'email': 'hanako@example.com', 'role': 'user', 'active': True},
    {'id': 3, 'name': '鈴木一郎', 'email': 'ichiro@example.com', 'role': 'user', 'active': False},
    {'id': 4, 'name': '高橋美咲', 'email': 'misaki@example.com', 'role': 'editor', 'active': True}
]

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/users')
def user_list():
    return render_template('users.html', users=users)

@app.route('/user/')
def user_detail(user_id):
    user = next((u for u in users if u['id'] == user_id), None)
    if user:
        return render_template('user_detail.html', user=user)
    else:
        flash('ユーザーが見つかりません', 'error')
        return redirect(url_for('user_list'))

@app.route('/add-user', methods=['GET', 'POST'])
def add_user():
    if request.method == 'POST':
        # 新しいユーザーの追加処理(簡易版)
        new_user = {
            'id': len(users) + 1,
            'name': request.form.get('name'),
            'email': request.form.get('email'),
            'role': request.form.get('role'),
            'active': bool(request.form.get('active'))
        }
        users.append(new_user)
        flash('ユーザーが正常に追加されました', 'success')
        return redirect(url_for('user_list'))

    return render_template('add_user.html')

if __name__ == '__main__':
    app.run(debug=True)

templates/users.html

base.html を継承してユーザー一覧ページを構築する子テンプレートで、ページタイトルとヘッダーを「ユーザー一覧」「ユーザー管理」に上書きしています。content ブロックでは、新規ユーザー追加ボタンやユーザー情報を表形式で一覧表示し、ユーザーごとのID、名前、メール、権限、状態、詳細ページへのリンクを動的に表示しています。権限や状態に応じて文字色や背景色を変える条件分岐が含まれており、非アクティブユーザーは行の背景色が淡くなるなど視覚的な区別もされています。さらに、総ユーザー数や有効ユーザー数、管理者数などの統計情報も表示され、extra_css ブロックではボタンやテーブル、ユーザーの役割や状態に応じたスタイルを定義してページ全体の見た目を整えています。

{% extends "base.html" %}

{% block title %}ユーザー一覧 - マイサイト{% endblock %}

{% block header %}ユーザー管理{% endblock %}

{% block content %}
<div class="actions">
    <a href={{ url_for('add_user') }} class="button">新しいユーザーを追加</a>
</div>

<table class="user-table">
    <thead>
        <tr>
            <th>ID</th>
            <th>名前</th>
            <th>メール</th>
            <th>権限</th>
            <th>状態</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        {% for user in users %}
        <tr class="{% if not user.active %}inactive{% endif %}">
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td>
                <span class="role {{ user.role }}">
                    {% if user.role == 'admin' %}管理者
                    {% elif user.role == 'editor' %}編集者
                    {% else %}ユーザー
                    {% endif %}
                </span>
            </td>
            <td>
                {% if user.active %}
                <span class="active">有効</span>
                {% else %}
                <span class="inactive">無効</span>
                {% endif %}
            </td>
            <td>
                <a href={{ url_for('user_detail', user_id=user.id) }}>詳細</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

<div class="stats">
    <p>総ユーザー数: {{ users|length }}</p>
    <p>有効ユーザー: {{ users|selectattr('active')|list|length }}</p>
    <p>管理者: {{ users|selectattr('role', 'equalto', 'admin')|list|length }}</p>
</div>
{% endblock %}

{% block extra_css %}
<style>
    .button {
        background-color: #007bff;
        color: white;
        padding: 10px 20px;
        text-decoration: none;
        border-radius: 5px;
        display: inline-block;
        margin-bottom: 20px;
    }
    .user-table {
        width: 100%;
        border-collapse: collapse;
    }
    .user-table th, .user-table td {
        border: 1px solid #ddd;
        padding: 12px;
        text-align: left;
    }
    .user-table th {
        background-color: #f2f2f2;
    }
    .role.admin { color: #dc3545; font-weight: bold; }
    .role.editor { color: #fd7e14; }
    .role.user { color: #28a745; }
    .active { color: #28a745; }
    .inactive { color: #6c757d; }
    tr.inactive { background-color: #f8f9fa; }
    .stats {
        margin-top: 20px;
        padding: 15px;
        background-color: #e9ecef;
        border-radius: 5px;
    }
</style>
{% endblock %}

まとめ

この記事では、Jinja2テンプレートエンジンの基礎について詳細に学びました。

  1. テンプレートの基本構文: {{ }}, {% %}, {# #}の使い方
  2. 変数表示: 単純な変数から複雑なデータ構造まで
  3. 制御構造: 条件分岐とループの実践的な使用方法
  4. フィルター: 組み込みフィルターとカスタムフィルター
  5. テンプレート継承: 効率的なレイアウト管理

Jinja2をマスターすることで、保守性が高く、美しいWebアプリケーションを作成できるようになります。次のステップでは、フォーム処理とデータベース連携について学び、よりインタラクティブなアプリケーションを作成していきましょう。

演習問題

初級問題(3問)

初級問題1: 基本的な変数表示

問題: 以下のPythonコードから渡される変数をテンプレートで表示してください。

@app.route('/basic')
def basic():
    student = {
        'name': '山田太郎',
        'grade': 3,
        'subjects': ['数学', '英語', '国語']
    }
    return render_template('basic.html', student=student)

指示: templates/basic.html を作成し、以下の情報を表示するテンプレートを完成させてください:

  • 生徒の名前
  • 学年(○年生という形式で)
  • 教科のリスト(箇条書きで)

初級問題2: 条件分岐の基本

問題: ユーザーのログイン状態に応じて表示を変えるテンプレートを作成してください。

@app.route('/login-status')
def login_status():
    users = [
        {'name': 'ユーザーA', 'logged_in': True},
        {'name': 'ユーザーB', 'logged_in': False},
        {'name': 'ユーザーC', 'logged_in': True}
    ]
    return render_template('login_status.html', users=users)

指示: 各ユーザーについて、ログイン中の場合は「[名前]さんはログイン中です」、ログインしていない場合は「[名前]さんはログアウト中です」と表示してください。

初級問題3: 基本的なループ

問題: 商品リストを表形式で表示するテンプレートを作成してください。

@app.route('/products-basic')
def products_basic():
    products = [
        {'name': 'ノート', 'price': 100, 'stock': 50},
        {'name': 'ペン', 'price': 50, 'stock': 100},
        {'name': '消しゴム', 'price': 30, 'stock': 0}
    ]
    return render_template('products_basic.html', products=products)

指示: 商品名、価格、在庫数を表形式で表示し、在庫が0の場合は「在庫切れ」と表示してください。

中級問題(6問)

中級問題1: フィルターの活用

問題: 様々なフィルターを使用してデータを整形してください。

@app.route('/filters-practice')
def filters_practice():
    data = {
        'title': 'welcome to my website',
        'description': 'This is a very long description that needs to be shortened for display purposes.',
        'price': 1234.567,
        'tags': ['python', 'flask', 'web-development'],
        'created_at': '2024-01-15 14:30:00'
    }
    return render_template('filters_practice.html', data=data)

指示: 以下の変換を行ってください。

  • タイトルを適切な大文字表記に
  • 説明文を20文字で切り詰め
  • 価格を整数に丸めて通貨形式で(例: ¥1,235)
  • タグをカンマ区切りで表示
  • 日付を「2024年1月15日」形式で表示

中級問題2: ループ変数の活用

問題: ループ変数を使用して特別なスタイルを適用してください。

@app.route('/loop-vars')
def loop_vars():
    items = ['項目A', '項目B', '項目C', '項目D', '項目E']
    return render_template('loop_vars.html', items=items)

指示: 以下の条件でスタイルを適用してください。

  • 最初の項目は太字で表示
  • 最後の項目は斜体で表示
  • 偶数番目の項目は背景色を薄い灰色に
  • 各項目に連番を表示(1. 項目A, 2. 項目B…)

中級問題3: 条件分岐の組み合わせ

問題: ユーザーの権限と状態に基づいて表示を制御してください。

@app.route('/user-permissions')
def user_permissions():
    users = [
        {'name': '管理者', 'role': 'admin', 'active': True},
        {'name': '編集者', 'role': 'editor', 'active': True},
        {'name': '一般ユーザー', 'role': 'user', 'active': True},
        {'name': '停止ユーザー', 'role': 'user', 'active': False}
    ]
    return render_template('user_permissions.html', users=users)

指示: 以下の条件で表示を分けてください。

  • 管理者:赤色で「管理者」と表示
  • 編集者:オレンジ色で「編集者」と表示
  • 一般ユーザー:緑色で「ユーザー」と表示
  • 無効なユーザー:灰色で取り消し線を表示

中級問題4: テンプレートのインクルード

問題: ヘッダーとフッターを別ファイルに分割してインクルードしてください。

@app.route('/with-include')
def with_include():
    page_title = "インクルードの練習"
    content = "これはメインコンテンツです。"
    return render_template('with_include.html', 
                         page_title=page_title, 
                         content=content)

指示:

  1. templates/includes/header.html を作成(サイト名とナビゲーション)
  2. templates/includes/footer.html を作成(著作権表示)
  3. メインテンプレートでこれらをインクルードして使用

中級問題5: マクロの作成

問題: カード表示用のマクロを作成してください。

@app.route('/macros')
def macros():
    cards = [
        {'title': 'カード1', 'content': 'これは最初のカードです', 'color': 'blue'},
        {'title': 'カード2', 'content': 'これは二番目のカードです', 'color': 'green'},
        {'title': 'カード3', 'content': 'これは三番目のカードです', 'color': 'red'}
    ]
    return render_template('macros.html', cards=cards)

指示: card_macro.html にマクロを作成し、色に応じたボーダーを持つカードを表示するマクロを定義してください。

中級問題6: コンテンツブロックの基本

問題: ベーステンプレートを作成し、シンプルな継承を行ってください。

@app.route('/inheritance-basic')
def inheritance_basic():
    return render_template('inheritance_basic.html', 
                         title='基本継承の練習',
                         content='これは子テンプレートの内容です')

指示:

  1. base_simple.html を作成(タイトル、ヘッダー、メインコンテンツブロック、フッター)
  2. inheritance_basic.html でベーステンプレートを継承
  3. 各ブロックを適切にオーバーライド

上級問題(3問)

上級問題1: 複雑なデータ構造の表示

問題: ネストしたデータ構造を適切に表示するテンプレートを作成してください。

@app.route('/complex-data')
def complex_data():
    company = {
        'name': 'テクノロジー株式会社',
        'departments': [
            {
                'name': '開発部',
                'manager': '田中開発',
                'employees': [
                    {'name': '山田プログラム', 'position': 'エンジニア', 'skills': ['Python', 'JavaScript']},
                    {'name': '佐藤コーダー', 'position': 'デベロッパー', 'skills': ['HTML', 'CSS']}
                ]
            },
            {
                'name': '営業部', 
                'manager': '鈴木営業',
                'employees': [
                    {'name': '高橋セールス', 'position': '営業担当', 'skills': ['交渉', 'プレゼン']}
                ]
            }
        ]
    }
    return render_template('complex_data.html', company=company)

指示: 以下の構造で情報を表示してください:

  • 会社名
  • 部署ごとのセクション
  • 各部署のマネージャー
  • 従業員一覧(名前、役職、スキル)

上級問題2: 動的なフォーム生成

問題: 設定データに基づいて動的にフォームを生成してください。

@app.route('/dynamic-form')
def dynamic_form():
    form_config = {
        'title': 'ユーザー登録フォーム',
        'fields': [
            {'name': 'username', 'type': 'text', 'label': 'ユーザー名', 'required': True},
            {'name': 'email', 'type': 'email', 'label': 'メールアドレス', 'required': True},
            {'name': 'age', 'type': 'number', 'label': '年齢', 'required': False},
            {'name': 'subscribe', 'type': 'checkbox', 'label': 'ニュースレターを購読する', 'required': False}
        ]
    }
    return render_template('dynamic_form.html', form=form_config)

指示: 設定データに基づいて完全なHTMLフォームを動的に生成するマクロを作成してください。

上級問題3: 完全なWebサイト構築

問題: これまで学んだすべての技術を組み合わせて、完全なブログサイトのテンプレートを作成してください。

@app.route('/blog')
def blog():
    posts = [
        {
            'id': 1,
            'title': 'Flaskの始め方',
            'content': 'FlaskはPythonの軽量Webフレームワークです...',
            'author': '山田太郎',
            'created_at': '2024-01-10',
            'tags': ['Python', 'Flask', 'Web'],
            'published': True
        },
        {
            'id': 2,
            'title': 'Jinja2テンプレートの活用',
            'content': 'Jinja2を使うと動的なWebページが作成できます...',
            'author': '佐藤由紀子', 
            'created_at': '2024-01-12',
            'tags': ['Jinja2', 'テンプレート'],
            'published': True
        }
    ]

    site_config = {
        'name': 'マイ技術ブログ',
        'description': 'PythonとWeb開発についてのブログ',
        'nav_links': [
            {'name': 'ホーム', 'url': '/'},
            {'name': '記事一覧', 'url': '/posts'},
            {'name': 'About', 'url': '/about'}
        ]
    }

    return render_template('blog.html', 
                         posts=posts, 
                         site_config=site_config,
                         current_page='ホーム')

指示:

  1. 完全なベーステンプレートを作成(ヘッダー、ナビゲーション、フッター)
  2. ブログ記事一覧をカード形式で表示
  3. タグの表示、日付のフォーマット、条件付き表示を実装
  4. ナビゲーションの現在ページを強調表示
  5. レスポンシブなデザインを意識

演習問題 解答例

初級問題 解答例

初級問題1: 基本的な変数表示

templates/basic.html

<!DOCTYPE html>
<html>
<head>
    <title>生徒情報</title>
</head>
<body>
    <h1>生徒情報</h1>
    <div class="student-info">
        <p><strong>名前:</strong> {{ student.name }}</p>
        <p><strong>学年:</strong> {{ student.grade }}年生</p>
        <p><strong>教科:</strong></p>
        <ul>
            {% for subject in student.subjects %}
            <li>{{ subject }}</li>
            {% endfor %}
        </ul>
    </div>
</body>
</html>

初級問題2: 条件分岐の基本

templates/login_status.html

<!DOCTYPE html>
<html>
<head>
    <title>ログイン状態</title>
    <style>
        .logged-in { color: green; }
        .logged-out { color: red; }
    </style>
</head>
<body>
    <h1>ユーザーログイン状態</h1>
    <div class="user-list">
        {% for user in users %}
            <div class="user">
                {% if user.logged_in %}
                    <p class="logged-in">{{ user.name }}さんはログイン中です</p>
                {% else %}
                    <p class="logged-out">{{ user.name }}さんはログアウト中です</p>
                {% endif %}
            </div>
        {% endfor %}
    </div>
</body>
</html>

初級問題3: 基本的なループ

templates/products_basic.html

<!DOCTYPE html>
<html>
<head>
    <title>商品一覧</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
        .out-of-stock {
            color: red;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <h1>商品一覧</h1>
    <table>
        <thead>
            <tr>
                <th>商品名</th>
                <th>価格</th>
                <th>在庫状況</th>
            </tr>
        </thead>
        <tbody>
            {% for product in products %}
            <tr>
                <td>{{ product.name }}</td>
                <td>{{ product.price }}円</td>
                <td>
                    {% if product.stock > 0 %}
                        {{ product.stock }}個
                    {% else %}
                        <span class="out-of-stock">在庫切れ</span>
                    {% endif %}
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

中級問題 解答例

中級問題1: フィルターの活用

templates/filters_practice.html

<!DOCTYPE html>
<html>
<head>
    <title>フィルター練習</title>
</head>
<body>
    <h1>フィルター使用例</h1>

    <div class="filter-examples">
        <h2>元のデータ:</h2>
        <pre>{{ data }}</pre>

        <h2>フィルター適用後:</h2>
        <ul>
            <li><strong>タイトル:</strong> {{ data.title|title }}</li>
            <li><strong>説明文:</strong> {{ data.description|truncate(20) }}</li>
            <li><strong>価格:</strong> ¥{{ "%.0f"|format(data.price) }}</li>
            <li><strong>タグ:</strong> {{ data.tags|join(', ') }}</li>
            <li><strong>作成日:</strong> {{ data.created_at|replace('-', '年', 1)|replace('-', '月', 1) }}日</li>
        </ul>

        <h2>別の方法での日付フォーマット:</h2>
        <p>{{ data.created_at.split(' ')[0].replace('-', '年', 1).replace('-', '月') }}日</p>
    </div>
</body>
</html>

中級問題2: ループ変数の活用

templates/loop_vars.html

<!DOCTYPE html>
<html>
<head>
    <title>ループ変数</title>
    <style>
        .first-item { font-weight: bold; }
        .last-item { font-style: italic; }
        .even-item { background-color: #f0f0f0; }
        .item { padding: 5px; margin: 2px; }
    </style>
</head>
<body>
    <h1>ループ変数の活用</h1>

    <div class="items-list">
        {% for item in items %}
        <div class="item {% if loop.first %}first-item{% endif %} 
                      {% if loop.last %}last-item{% endif %} 
                      {% if loop.index % 2 == 0 %}even-item{% endif %}">
            {{ loop.index }}. {{ item }}
            {% if loop.first %} (最初の項目) {% endif %}
            {% if loop.last %} (最後の項目) {% endif %}
        </div>
        {% endfor %}
    </div>

    <div class="loop-info">
        <h2>ループ情報:</h2>
        <p>総項目数: {{ items|length }}</p>
        <p>ループカウンタ: loop.index (1始まり), loop.index0 (0始まり)</p>
    </div>
</body>
</html>

中級問題3: 条件分岐の組み合わせ

templates/user_permissions.html

<!DOCTYPE html>
<html>
<head>
    <title>ユーザー権限</title>
    <style>
        .admin { color: red; font-weight: bold; }
        .editor { color: orange; }
        .user { color: green; }
        .inactive { color: gray; text-decoration: line-through; }
        .user-card {
            border: 1px solid #ddd;
            padding: 10px;
            margin: 5px;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>ユーザー権限と状態</h1>

    <div class="users-container">
        {% for user in users %}
        <div class="user-card {% if not user.active %}inactive{% endif %}">
            <h3>{{ user.name }}</h3>
            <p>
                権限: 
                {% if user.role == 'admin' %}
                    <span class="admin">管理者</span>
                {% elif user.role == 'editor' %}
                    <span class="editor">編集者</span>
                {% else %}
                    <span class="user">ユーザー</span>
                {% endif %}
            </p>
            <p>
                状態: 
                {% if user.active %}
                    <span style="color: green;">有効</span>
                {% else %}
                    <span style="color: red;">無効</span>
                {% endif %}
            </p>
        </div>
        {% endfor %}
    </div>
</body>
</html>

中級問題4: テンプレートのインクルード

templates/includes/header.html

<header>
    <h1>マイウェブサイト</h1>
    <nav>
        <a href="/">ホーム</a>
        <a href="/about">About</a>
        <a href="/contact">お問い合わせ</a>
    </nav>
</header>

templates/includes/footer.html

<footer>
    <p>&copy; 2024 マイウェブサイト. All rights reserved.</p>
    <p>連絡先: info@example.com</p>
</footer>

templates/with_include.html

<!DOCTYPE html>
<html>
<head>
    <title>{{ page_title }}</title>
    <style>
        header { background-color: #333; color: white; padding: 1rem; }
        nav a { color: white; margin-right: 1rem; text-decoration: none; }
        footer { background-color: #f1f1f1; padding: 1rem; text-align: center; margin-top: 2rem; }
        main { padding: 2rem; }
    </style>
</head>
<body>

    {% include 'includes/header.html' %}

    <main>
        <h1>{{ page_title }}</h1>
        <p>{{ content }}</p>
    </main>

    {% include 'includes/footer.html' %}
</body>
</html>

中級問題5: マクロの作成

templates/macros/card_macro.html

{% macro render_card(card) %}
    <div class="card card-{{ card.color }}">
        <h3>{{ card.title }}</h3>
        <p>{{ card.content }}</p>
    </div>
{% endmacro %}

templates/macros.html

<!DOCTYPE html>
<html>
<head>
    <title>マクロの練習</title>
    <style>
        .card {
            border: 2px solid;
            padding: 15px;
            margin: 10px;
            border-radius: 5px;
        }
        .card-blue { border-color: blue; background-color: #e6f3ff; }
        .card-green { border-color: green; background-color: #e6ffe6; }
        .card-red { border-color: red; background-color: #ffe6e6; }
        .cards-container {
            display: flex;
            flex-wrap: wrap;
        }
    </style>
</head>
<body>
    <h1>カード表示 - マクロ使用例</h1>

    {% from 'macros/card_macro.html' import render_card %}

    <div class="cards-container">
        {% for card in cards %}
            {{ render_card(card) }}
        {% endfor %}
    </div>
</body>
</html>

中級問題6: コンテンツブロックの基本

templates/base_simple.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}デフォルトタイトル{% endblock %}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
        header { background-color: #333; color: white; padding: 1rem; }
        main { padding: 2rem; }
        footer { background-color: #f1f1f1; padding: 1rem; text-align: center; }
    </style>
</head>
<body>
    <header>
        {% block header %}
            <h1>デフォルトヘッダー</h1>
        {% endblock %}
    </header>

    <main>
        {% block content %}
        <!-- デフォルトコンテンツ -->
        {% endblock %}
    </main>

    <footer>
        {% block footer %}
            <p>&copy; 2024 マイサイト</p>
        {% endblock %}
    </footer>
</body>
</html>

templates/inheritance_basic.html

{% extends "base_simple.html" %}

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

{% block header %}
    <h1>{{ title }}</h1>
    <nav>
        <a href="/">ホーム</a> | 
        <a href="/about">About</a>
    </nav>
{% endblock %}

{% block content %}
    <div class="content">
        <h2>メインコンテンツ</h2>
        <p>{{ content }}</p>

        <div class="additional-info">
            <h3>追加情報</h3>
            <p>このページはテンプレート継承を使用しています。</p>
        </div>
    </div>
{% endblock %}

{% block footer %}
    <p>&copy; 2024 継承練習サイト</p>
    <p>連絡先: contact@example.com</p>
    {{ super() }}
{% endblock %}

上級問題 解答例

上級問題1: 複雑なデータ構造の表示

templates/complex_data.html

<!DOCTYPE html>
<html>
<head>
    <title>{{ company.name }} - 組織図</title>
    <style>
        .company { margin: 20px; }
        .department { 
            border: 1px solid #ddd; 
            margin: 10px 0; 
            padding: 15px;
            border-radius: 5px;
        }
        .department-header { 
            background-color: #f5f5f5; 
            padding: 10px; 
            margin: -15px -15px 15px -15px;
            border-bottom: 1px solid #ddd;
        }
        .employee { 
            border-left: 3px solid #007bff; 
            padding: 10px; 
            margin: 5px 0;
            background-color: #f8f9fa;
        }
        .skills { font-size: 0.9em; color: #666; }
        .skill-tag { 
            display: inline-block; 
            background: #e9ecef; 
            padding: 2px 8px; 
            margin: 2px;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <div class="company">
        <h1>{{ company.name }}</h1>

        {% for department in company.departments %}
        <div class="department">
            <div class="department-header">
                <h2>{{ department.name }}</h2>
                <p><strong>マネージャー:</strong> {{ department.manager }}</p>
            </div>

            <h3>従業員一覧 ({{ department.employees|length }}名)</h3>

            {% for employee in department.employees %}
            <div class="employee">
                <h4>{{ employee.name }} - {{ employee.position }}</h4>
                <div class="skills">
                    <strong>スキル:</strong>
                    {% for skill in employee.skills %}
                        <span class="skill-tag">{{ skill }}</span>
                    {% endfor %}
                </div>
            </div>
            {% endfor %}
        </div>
        {% endfor %}

        <div class="summary">
            <h2>組織概要</h2>
            <p>部署数: {{ company.departments|length }}</p>
            <p>総従業員数: 
                {% set total_employees = [] %}
                {% for department in company.departments %}
                    {% for employee in department.employees %}
                        {% if total_employees.append(1) %}{% endif %}
                    {% endfor %}
                {% endfor %}
                {{ total_employees|length }}名
            </p>
        </div>
    </div>
</body>
</html>

上級問題2: 動的なフォーム生成

templates/macros/form_macros.html

{% macro render_field(field) %}
    <div class="form-field">
        <label for="{{ field.name }}">
            {{ field.label }}
            {% if field.required %}<span style="color: red;">*</span>{% endif %}
        </label>

        {% if field.type == 'text' or field.type == 'email' or field.type == 'number' %}
            <input type="{{ field.type }}" 
                   id="{{ field.name }}" 
                   name="{{ field.name }}"
                   {% if field.required %}required{% endif %}
                   class="form-control">

        {% elif field.type == 'checkbox' %}
            <input type="checkbox" 
                   id="{{ field.name }}" 
                   name="{{ field.name }}"
                   value="true"
                   class="form-checkbox">

        {% elif field.type == 'textarea' %}
            <textarea id="{{ field.name }}" 
                      name="{{ field.name }}"
                      {% if field.required %}required{% endif %}
                      class="form-textarea"></textarea>

        {% elif field.type == 'select' %}
            <select id="{{ field.name }}" 
                    name="{{ field.name }}"
                    {% if field.required %}required{% endif %}
                    class="form-select">
                {% for option in field.options %}
                    <option value="{{ option.value }}">{{ option.label }}</option>
                {% endfor %}
            </select>
        {% endif %}

        {% if field.help_text %}
            <small class="help-text">{{ field.help_text }}</small>
        {% endif %}
    </div>
{% endmacro %}

{% macro render_form(form_config) %}
    <form method="POST" class="dynamic-form">
        <h2>{{ form_config.title }}</h2>

        {% for field in form_config.fields %}
            {{ render_field(field) }}
        {% endfor %}

        <div class="form-actions">
            <button type="submit" class="btn btn-primary">送信</button>
            <button type="reset" class="btn btn-secondary">リセット</button>
        </div>
    </form>
{% endmacro %}

templates/dynamic_form.html

<!DOCTYPE html>
<html>
<head>
    <title>{{ form.title }}</title>
    <style>
        .dynamic-form {
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .form-field {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .form-control, .form-textarea, .form-select {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        .form-textarea {
            height: 100px;
            resize: vertical;
        }
        .form-checkbox {
            margin-right: 5px;
        }
        .help-text {
            color: #666;
            font-size: 0.9em;
        }
        .form-actions {
            margin-top: 20px;
            text-align: center;
        }
        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 0 5px;
        }
        .btn-primary {
            background-color: #007bff;
            color: white;
        }
        .btn-secondary {
            background-color: #6c757d;
            color: white;
        }
    </style>
</head>
<body>
    {% from 'macros/form_macros.html' import render_form %}

    <div class="container">
        {{ render_form(form) }}
    </div>
</body>
</html>

上級問題3: 完全なWebサイト構築

templates/blog_base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{{ site_config.name }}{% endblock %}</title>
    <meta name="description" content="{{ site_config.description }}">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: #333;
            background-color: #f8f9fa;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 20px;
        }
        header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 2rem 0;
            margin-bottom: 2rem;
        }
        .site-title {
            font-size: 2.5rem;
            margin-bottom: 0.5rem;
        }
        .site-description {
            font-size: 1.1rem;
            opacity: 0.9;
        }
        nav {
            background: white;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-bottom: 2rem;
        }
        .nav-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .nav-links {
            display: flex;
            list-style: none;
        }
        .nav-links a {
            display: block;
            padding: 1rem 1.5rem;
            text-decoration: none;
            color: #333;
            transition: all 0.3s ease;
        }
        .nav-links a:hover,
        .nav-links a.active {
            background-color: #667eea;
            color: white;
        }
        main {
            min-height: 60vh;
        }
        .posts-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
            gap: 2rem;
            margin-bottom: 2rem;
        }
        .post-card {
            background: white;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
            overflow: hidden;
            transition: transform 0.3s ease;
        }
        .post-card:hover {
            transform: translateY(-5px);
        }
        .post-content {
            padding: 1.5rem;
        }
        .post-title {
            font-size: 1.25rem;
            margin-bottom: 0.5rem;
            color: #333;
        }
        .post-meta {
            color: #666;
            font-size: 0.9rem;
            margin-bottom: 1rem;
        }
        .post-excerpt {
            color: #555;
            margin-bottom: 1rem;
        }
        .tags {
            display: flex;
            flex-wrap: wrap;
            gap: 0.5rem;
        }
        .tag {
            background: #e9ecef;
            padding: 0.25rem 0.5rem;
            border-radius: 15px;
            font-size: 0.8rem;
            color: #495057;
        }
        footer {
            background: #343a40;
            color: white;
            text-align: center;
            padding: 2rem 0;
            margin-top: 3rem;
        }
        @media (max-width: 768px) {
            .nav-container {
                flex-direction: column;
            }
            .nav-links {
                margin-top: 1rem;
            }
            .posts-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header>
        <div class="container">
            <h1 class="site-title">{{ site_config.name }}</h1>
            <p class="site-description">{{ site_config.description }}</p>
        </div>
    </header>

    <nav>
        <div class="container nav-container">
            <div class="logo">
                <strong>{{ site_config.name }}</strong>
            </div>
            <ul class="nav-links">
                {% for link in site_config.nav_links %}
                <li>
                    <a href="{{ link.url }}" 
                       {% if link.name == current_page %}class="active"{% endif %}>
                        {{ link.name }}
                    </a>
                </li>
                {% endfor %}
            </ul>
        </div>
    </nav>

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

    <footer>
        <div class="container">
            <p>&copy; 2024 {{ site_config.name }}. All rights reserved.</p>
            <p>{{ site_config.description }}</p>
        </div>
    </footer>

    {% block extra_js %}{% endblock %}
</body>
</html>

templates/blog.html

{% extends "blog_base.html" %}

{% block title %}ホーム - {{ site_config.name }}{% endblock %}

{% block content %}
<section class="hero">
    <h1 style="text-align: center; margin-bottom: 2rem;">最新の記事</h1>
</section>

<section class="posts-section">
    <div class="posts-grid">
        {% for post in posts %}
        {% if post.published %}
        <article class="post-card">
            <div class="post-content">
                <h2 class="post-title">{{ post.title }}</h2>

                <div class="post-meta">
                    <span>著者: {{ post.author }}</span> | 
                    <span>{{ post.created_at }}</span>
                </div>

                <p class="post-excerpt">
                    {{ post.content|truncate(150) }}
                </p>

                <div class="tags">
                    {% for tag in post.tags %}
                    <span class="tag">{{ tag }}</span>
                    {% endfor %}
                </div>

                <a href="/post/{{ post.id }}" 
                   style="display: inline-block; margin-top: 1rem; color: #667eea; text-decoration: none;">
                    続きを読む →
                </a>
            </div>
        </article>
        {% endif %}
        {% endfor %}
    </div>
</section>

<section class="blog-stats" style="background: white; padding: 2rem; border-radius: 10px; margin-top: 2rem;">
    <h2>ブログ統計</h2>
    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem;">
        <div class="stat-item">
            <h3>公開記事数</h3>
            <p style="font-size: 2rem; color: #667eea; font-weight: bold;">
                {{ posts|selectattr('published')|list|length }}
            </p>
        </div>
        <div class="stat-item">
            <h3>著者数</h3>
            <p style="font-size: 2rem; color: #667eea; font-weight: bold;">
                {{ posts|map(attribute='author')|unique|list|length }}
            </p>
        </div>
        <div class="stat-item">
            <h3>総タグ数</h3>
            <p style="font-size: 2rem; color: #667eea; font-weight: bold;">
                {% set all_tags = [] %}
                {% for post in posts %}
                    {% for tag in post.tags %}
                        {% if tag not in all_tags %}
                            {% set _ = all_tags.append(tag) %}
                        {% endif %}
                    {% endfor %}
                {% endfor %}
                {{ all_tags|length }}
            </p>
        </div>
    </div>
</section>
{% endblock %}