Jinja2 Templates

Introduction

Jinja2 renders HTML from templates with variables, loops, and inheritance—Flask’s default engine. This chapter sets up templates/ and static/, uses render_template, builds a base layout, includes partials, links CSS with url_for, and introduces filters and macros.

Prerequisites

Project Structure

text
flask-hello/
├── app.py
├── templates/
│   ├── base.html
│   ├── index.html
│   └── partials/
│       └── nav.html
└── static/
    ├── css/
    │   └── style.css
    └── js/
        └── app.js

render_template

python
from flask import Flask, render_template
 
app = Flask(__name__)
 
@app.route("/")
def index():
    return render_template("index.html", title="Home", user="Alice")

templates/index.html:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ title }}</title>
</head>
<body>
  <h1>Welcome, {{ user }}</h1>
</body>
</html>

Code explanation:

  • Keyword args to render_template become template variables
  • Flask looks in templates/ next to the app root or package

Variables, Statements, Comments

Pass data from view:

python
return render_template(
    "list.html",
    items=["Flask", "Jinja2", "Werkzeug"],
    logged_in=True,
)

Template Inheritance

templates/base.html:

templates/index.html:

html
{% extends "base.html" %}
 
{% block title %}Home{% endblock %}
 
{% block content %}
  <h1>Hello, {{ user }}</h1>
{% endblock %}

Code explanation:

  • extends must be first meaningful statement in child template
  • block names must match between parent and child

Static Files

static/css/style.css:

css
body {
  font-family: system-ui, sans-serif;
  margin: 2rem;
}

Link in template:

html
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/app.js') }}" defer></script>

Code explanation:

  • url_for('static', filename=...) generates /static/... URLs
  • Production may serve static via Nginx instead of Flask

Include Partials

templates/partials/nav.html:

html
<nav>
  <a href="{{ url_for('index') }}">Home</a>
  <a href="{{ url_for('about') }}">About</a>
</nav>

Register routes so url_for('about') resolves:

python
@app.route("/about")
def about():
    return render_template("about.html", title="About")

Filters

Built-in:

html
{{ name|title }}
{{ text|truncate(80) }}
{{ created_at|default("unknown") }}

Custom filter:

python
@app.template_filter("reverse")
def reverse_filter(s):
    return s[::-1]

Template:

html
{{ "Flask"|reverse }}

Macros

templates/macros/forms.html:

html
{% macro input_field(name, label, type="text") %}
  <label for="{{ name }}">{{ label }}</label>
  <input id="{{ name }}" name="{{ name }}" type="{{ type }}">
{% endmacro %}

Use:

html
{% from 'macros/forms.html' import input_field %}
{{ input_field("email", "Email", "email") }}

Forms chapter uses Flask-WTF for CSRF-safe forms.

Tip

Auto-Escape Is On

Jinja2 escapes HTML in {{ }} by default—safe against XSS when rendering user input.

FAQ

TemplateNotFound?

Check folder name templates/, file path, and Flask app root.

CSS not loading?

Verify static/ path and url_for('static', ...); hard refresh browser.

extends order error?

Child template: {% extends %} first, then {% block %}.

Raw HTML from user?

Use {{ content|safe }} only for trusted content—otherwise keep auto-escape.

Multiple template folders?

Flask(__name__, template_folder='views/templates').

Jinja vs JavaScript {{?

Server renders Jinja before browser sees page—client frameworks use different syntax.