【Flask】Jinja2テンプレートでHTMLを動的に生成する

Flask

こんにちは、かつコーチです。

これまでの記事では、return "文字列"で画面を表示してきましたが、実際のWebアプリでは装飾されたHTMLを表示したいはずです。

この記事では、Flaskに標準搭載されているJinja2というテンプレートエンジンの使い方を解説します。

Jinja2とは?

HTMLに変数やロジックを埋め込む仕組み

Jinja2(ジンジャツー)とは、HTMLファイルの中にPythonの変数や条件分岐を埋め込めるようにするテンプレートエンジン(雛形に動的なデータを流し込んで最終的なHTMLを作る仕組み)です。

Flaskには標準でJinja2が組み込まれているため、追加のインストールは不要です。

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", username="かつコーチ")

render_template関数がJinja2の呼び出し口で、第1引数にテンプレートファイル名、それ以降にテンプレートへ渡したい変数を指定します。

templatesフォルダの役割

Flaskは、プロジェクト直下のtemplatesという名前のフォルダを自動的に探しに行きます。

flask-first-app/
├── app.py
└── templates/
    └── index.html

フォルダ名をtemplatesから変更するには追加の設定が必要なので、まずはこの標準構成に慣れておくのがおすすめです。

基本の書き方・実装手順

手順1: 変数をHTMLに埋め込む

templates/index.htmlを作成し、{{ }}(二重波括弧)で変数を埋め込みます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ホーム</title>
</head>
<body>
    <h1>こんにちは、{{ username }}さん!</h1>
</body>
</html>

render_templateで渡したusername="かつコーチ"が、{{ username }}の部分に展開されます。

手順2: 条件分岐と繰り返しを使う

Jinja2では{% %}(波括弧とパーセント)を使って、ifやforといった制御構文を書けます。

@app.route("/dashboard")
def dashboard():
    items = ["タスク1", "タスク2", "タスク3"]
    is_admin = True
    return render_template("dashboard.html", items=items, is_admin=is_admin)
<body>
    {% if is_admin %}
        <p>管理者としてログイン中です</p>
    {% else %}
        <p>一般ユーザーとしてログイン中です</p>
    {% endif %}

    <ul>
    {% for item in items %}
        <li>{{ item }}</li>
    {% endfor %}
    </ul>
</body>

{% if %}{% for %}の終わりには、必ず{% endif %}{% endfor %}が必要になる点がPythonの書き方と異なります。

手順3: テンプレートの共通部分を継承する

すべてのページでヘッダーやフッターを毎回書くのは非効率です。

Jinja2のテンプレート継承を使うと、共通レイアウトを1つのファイルにまとめられます。

templates/base.html(親テンプレート):

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}マイサイト{% endblock %}</title>
</head>
<body>
    <header><h1>マイサイト</h1></header>
    <main>
        {% block content %}{% endblock %}
    </main>
    <footer>© 2026 かつコーチ</footer>
</body>
</html>

templates/index.html(子テンプレート):

{% extends "base.html" %}

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

{% block content %}
    <p>ようこそ、{{ username }}さん!</p>
{% endblock %}

{% extends %}で親テンプレートを継承し、{% block %}で囲んだ部分だけを子テンプレート側で上書きします。

ページが増えるほど、この仕組みの恩恵は大きくなります。

つまずきやすい設定・注意点

| safeフィルタは慎重に使う

Jinja2はデフォルトで、変数に含まれるHTMLタグを自動的に無害化する自動エスケープ<script>などのタグをそのまま表示用の文字列に変換し、意図しないコードとして実行されないようにする仕組み)が有効になっています。

<p>{{ user_comment }}</p>

もしuser_comment<script>alert('攻撃')</script>のような文字列が入っていても、Jinja2は自動的に無害な文字列として表示してくれます。

これを{{ user_comment | safe }}のようにsafeフィルタで解除すると、自動エスケープが無効になります。

自分で完全に信頼できるデータ以外にsafeを使うと、XSS(クロスサイトスクリプティング)という脆弱性の原因になるため、基本的には使わない方針を徹底しましょう。

よくあるつまずきポイント・エラー対処

jinja2.exceptions.TemplateNotFound

❌ Before:templatesフォルダを作らずにrender_template("index.html")を呼び出し、jinja2.exceptions.TemplateNotFound: index.htmlというエラーが出る

✅ After:app.pyと同じ階層にtemplatesフォルダを作成し、その中にHTMLファイルを配置する

私が最初につまずいたのはまさにこのエラーでした。

templatesという名前を1文字でも間違える(Templateと大文字にする、templateと単数形にするなど)とFlaskはフォルダを見つけられません。

エラーメッセージにファイル名だけが表示されるため、最初はどこが間違っているのか分かりにくいポイントです。

変数名の渡し忘れでUndefinedErrorが起きる

❌ Before:テンプレート側で{{ username }}を使っているのに、render_template("index.html")usernameを渡し忘れる

この場合エラーにはならず、単に空文字として表示されてしまうことが多く、気づきにくいバグになります。

✅ After:render_templateに渡す変数名と、テンプレート内で使う変数名が一致しているかを、実装のたびに見比べる習慣をつける

より厳格にしたい場合は、Jinja2の設定で未定義変数をエラーにするStrictUndefinedを使う方法もあります。

応用・一歩先の使い方

static フォルダでCSS・画像を管理する

HTMLを装飾するCSSファイルは、templatesではなくstaticという名前のフォルダに配置します。

flask-first-app/
├── app.py
├── templates/
│   └── index.html
└── static/
    └── style.css

HTMLからはurl_forを使って参照します。

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

url_forを使うことで、staticフォルダのパスが変わってもHTML側の修正が不要になります。

まとめ

この記事のポイント

  • Jinja2はFlask標準搭載のテンプレートエンジン
  • {{ }}で変数展開、{% %}でif・forなどの制御構文を書く
  • {% extends %}{% block %}でレイアウトを共通化できる
  • 自動エスケープはXSS対策として重要なのでsafeフィルタは慎重に使う
  • templatesフォルダの命名ミスがエラーの定番原因

次に読むべき記事

画面表示ができるようになったら、次はデータをどう保存するかがテーマです。

「Flask-SQLAlchemyでモデルを定義しCRUD操作をする」で、データベースとの連携方法を解説します。


タグ: Flask, 初心者向け, フレームワーク基礎

タイトルとURLをコピーしました