Jinja2 Templating Engine

Creating dynamic, reusable HTML templates with Flask's integrated templating system

Introduction to Jinja2

Jinja2 is a modern and designer-friendly templating language for Python, modeled after Django's template system but with added flexibility and power. As Flask's default templating engine, Jinja2 allows you to generate dynamic HTML by combining static template files with data provided by your application.

"Templates are not just about replacing variables with their values; they're about expressing your application's interface in a way that separates logic from presentation."

Why Use Templates?

Templates provide several benefits in web application development:

flowchart LR A[View Function] -->|Context Data| B[Jinja2 Template Engine] C[Template Files] --> B B -->|Renders| D[HTML Response] D --> E[Browser]

Flask Template Basics

Template Organization

By default, Flask looks for templates in a folder named templates in your application directory:

myapp/
├── app.py
├── static/
│   └── css/
│       └── style.css
└── templates/
    ├── base.html
    ├── home.html
    ├── about.html
    └── contact.html

Rendering Templates

To render a template from a Flask route, use the render_template() function:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('home.html', title='Home Page')

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

@app.route('/user/')
def profile(username):
    user = get_user(username)  # Get user data from database
    return render_template('profile.html', user=user, title=f"{user.name}'s Profile")

The render_template() function takes:

Basic Template Syntax

A simple Jinja2 template might look like this:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
    
    {% if user.is_admin %}
        <p>You have admin privileges.</p>
    {% else %}
        <p>Welcome to our site.</p>
    {% endif %}
    
    <h2>Your Skills:</h2>
    <ul>
    {% for skill in user.skills %}
        <li>{{ skill }}</li>
    {% endfor %}
    </ul>
</body>
</html>

Jinja2 uses three types of delimiters:

Auto-Escaping in Jinja2

By default, Jinja2 automatically escapes all variables to prevent XSS attacks. HTML special characters like <, >, & are converted to their HTML entities.

To output raw HTML (use with caution), you can use the |safe filter:

<div>{{ html_content|safe }}</div>

Jinja2 Variables and Expressions

Variable Access

You can access variables in templates in several ways:

{{ foo }}          
{{ foo.bar }}      
{{ foo['bar'] }}   
{{ foo.method() }} 

Filters

Filters modify variables and are applied with a pipe (|) symbol:

{{ name|upper }}              
{{ name|lower }}              
{{ name|title }}              
{{ name|trim }}               
{{ name|striptags }}          
{{ list|join(', ') }}         
{{ number|round(2) }}         
{{ date|strftime('%Y-%m-%d') }} 
{{ text|truncate(100) }}      
{{ text|escape }}             
{{ html|safe }}               
{{ none|default('N/A') }}     

Tests

Tests check if a variable meets certain conditions:

{% if name is defined %}    
{% if name is none %}      
{% if name is string %}    
{% if value is divisibleby(3) %} 
{% if list is empty %}     
{% if user is admin %}      

Math Operations

Mathematical operations can be performed directly in templates:

{{ a + b }}        
{{ a - b }}        
{{ a * b }}        
{{ a / b }}        
{{ a // b }}       
{{ a % b }}        
{{ a ** b }}       

Comparisons and Logic

Comparison and logical operators work as expected:

{% if a > b %}     
{% if a >= b %}    
{% if a < b %}     
{% if a <= b %}    
{% if a == b %}    
{% if a != b %}    

{% if a and b %}   
{% if a or b %}    
{% if not a %}     

Real-World Example: User Profile Template

Here's a more complete example of a user profile page using Jinja2:

<!DOCTYPE html>
<html>
<head>
    <title>{{ user.name }}'s Profile</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <header>
        <h1>
            {{ user.name }}
            {% if user.is_premium %}
                <span class="badge">Premium</span>
            {% endif %}
        </h1>
        <p>Member since {{ user.join_date|strftime('%B %d, %Y') }}</p>
    </header>
    
    <section class="profile-details">
        <div class="avatar">
            {% if user.avatar %}
                <img src="{{ user.avatar }}" alt="{{ user.name }}">
            {% else %}
                <img src="{{ url_for('static', filename='img/default-avatar.png') }}" alt="Default Avatar">
            {% endif %}
        </div>
        
        <div class="bio">
            {% if user.bio %}
                {{ user.bio|urlize }}
            {% else %}
                <p>No bio provided.</p>
            {% endif %}
        </div>
        
        <div class="stats">
            <p>Posts: {{ user.post_count }}</p>
            <p>Comments: {{ user.comment_count }}</p>
            <p>Reputation: {{ user.reputation|default(0) }}</p>
        </div>
    </section>
    
    {% if user.posts %}
        <section class="recent-posts">
            <h2>Recent Posts</h2>
            <ul>
                {% for post in user.posts[:5] %}
                    <li>
                        <a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a>
                        <span class="date">{{ post.date|timesince }}</span>
                    </li>
                {% endfor %}
            </ul>
            
            {% if user.posts|length > 5 %}
                <a href="{{ url_for('user_posts', username=user.username) }}">View all {{ user.posts|length }} posts</a>
            {% endif %}
        </section>
    {% else %}
        <p>This user hasn't published any posts yet.</p>
    {% endif %}
    
    <footer>
        {% if current_user and current_user.id == user.id %}
            <a href="{{ url_for('edit_profile') }}" class="button">Edit Profile</a>
        {% elif current_user %}
            {% if user in current_user.following %}
                <form action="{{ url_for('unfollow', username=user.username) }}" method="post">
                    <button type="submit">Unfollow</button>
                </form>
            {% else %}
                <form action="{{ url_for('follow', username=user.username) }}" method="post">
                    <button type="submit">Follow</button>
                </form>
            {% endif %}
        {% endif %}
    </footer>
</body>
</html>

Control Structures

Conditionals

Conditional statements allow you to render different content based on conditions:

{% if condition %}
    <p>This is rendered if condition is true</p>
{% elif another_condition %}
    <p>This is rendered if the first condition is false but another_condition is true</p>
{% else %}
    <p>This is rendered if both conditions are false</p>
{% endif %}

Loops

Loops allow you to iterate over collections:

{% for item in items %}
    <p>{{ item.name }}: {{ item.description }}</p>
{% endfor %}

{# Loop with index #}
{% for item in items %}
    <p>{{ loop.index }}. {{ item }}</p>
{% endfor %}

{# Using loop variables #}
{% for item in items %}
    {% if loop.first %}<ul>{% endif %}
    <li class="{% if loop.odd %}odd{% else %}even{% endif %}">
        {{ item }}
        {% if not loop.last %} | {% endif %}
    </li>
    {% if loop.last %}</ul>{% endif %}
{% endfor %}

{# Empty list handling #}
{% for item in items %}
    <p>{{ item }}</p>
{% else %}
    <p>No items found.</p>
{% endfor %}

Available loop variables:

Assignments

You can set variables within templates:

{% set name = 'John' %}
<p>Hello, {{ name }}!</p>

{% set total = 0 %}
{% for item in cart %}
    {% set total = total + item.price %}
{% endfor %}
<p>Total: ${{ total }}</p>

Blocks

Block statements can be used for template inheritance (more on this later):

{% block content %}
    <p>Default content</p>
{% endblock %}

Macros

Macros are reusable pieces of template code, similar to functions:

{% macro input(name, value='', type='text', label='') %}
    <div class="form-group">
        {% if label %}
            <label for="{{ name }}">{{ label }}</label>
        {% endif %}
        <input type="{{ type }}" name="{{ name }}" id="{{ name }}" value="{{ value }}">
    </div>
{% endmacro %}

{# Using the macro #}
{{ input('username', label='Username') }}
{{ input('password', type='password', label='Password') }}
{{ input('remember', type='checkbox', label='Remember me') }}

Whitespace Control

Jinja2 preserves whitespace by default. To control whitespace in the output, you can use:

  • {%- ... %} - Removes whitespace before the block
  • {% ... -%} - Removes whitespace after the block
  • {{- ... }} - Removes whitespace before the expression
  • {{ ... -}} - Removes whitespace after the expression
{% for item in items -%}
    {{ item }}
{%- endfor %}

Template Inheritance

Template inheritance is one of Jinja2's most powerful features. It allows you to build a base "skeleton" template that contains common elements of your site and defines blocks that child templates can override.

Base Template

First, create a base template with common elements and defined blocks:


<!DOCTYPE html>
<html>
<head>
    {% block head %}
    <title>{% block title %}Default Title{% endblock %} - My Website</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {% endblock %}
</head>
<body>
    <header>
        {% block header %}
        <nav>
            <ul>
                <li><a href="{{ url_for('home') }}">Home</a></li>
                <li><a href="{{ url_for('about') }}">About</a></li>
                <li><a href="{{ url_for('contact') }}">Contact</a></li>
                {% if current_user.is_authenticated %}
                    <li><a href="{{ url_for('profile') }}">Profile</a></li>
                    <li><a href="{{ url_for('logout') }}">Logout</a></li>
                {% else %}
                    <li><a href="{{ url_for('login') }}">Login</a></li>
                    <li><a href="{{ url_for('register') }}">Register</a></li>
                {% endif %}
            </ul>
        </nav>
        {% endblock %}
    </header>
    
    <main>
        {% block content %}
        {# Main content goes here #}
        {% endblock %}
    </main>
    
    <footer>
        {% block footer %}
        <p>© {{ current_year }} My Website. All rights reserved.</p>
        {% endblock %}
    </footer>
    
    {% block scripts %}
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
    {% endblock %}
</body>
</html>

Child Templates

Then, create child templates that extend the base template and override specific blocks:


{% extends "base.html" %}

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

{% block content %}
    <h1>Welcome to My Website</h1>
    <p>This is the home page.</p>
    
    <div class="featured-content">
        <h2>Featured Articles</h2>
        <ul>
        {% for article in featured_articles %}
            <li>
                <a href="{{ url_for('article', article_id=article.id) }}">{{ article.title }}</a>
            </li>
        {% endfor %}
        </ul>
    </div>
{% endblock %}

{% extends "base.html" %}

{% block title %}About Us{% endblock %}

{% block content %}
    <h1>About Us</h1>
    <p>We are a company dedicated to...</p>
    
    <h2>Our Team</h2>
    <div class="team-members">
        {% for member in team_members %}
            <div class="member">
                <img src="{{ member.photo }}" alt="{{ member.name }}">
                <h3>{{ member.name }}</h3>
                <p>{{ member.position }}</p>
                <p>{{ member.bio }}</p>
            </div>
        {% endfor %}
    </div>
{% endblock %}

{% block scripts %}
    {{ super() }}  {# Include parent block content #}
    <script src="{{ url_for('static', filename='js/about.js') }}"></script>
{% endblock %}

Nested Inheritance

You can create multiple levels of inheritance for more complex layouts:


{% extends "base.html" %}

{% block title %}Admin Panel{% endblock %}

{% block header %}
    {{ super() }}
    <div class="admin-banner">Admin Panel</div>
{% endblock %}

{% block content %}
    <div class="admin-container">
        <div class="admin-sidebar">
            <ul>
                <li><a href="{{ url_for('admin.dashboard') }}">Dashboard</a></li>
                <li><a href="{{ url_for('admin.users') }}">Users</a></li>
                <li><a href="{{ url_for('admin.posts') }}">Posts</a></li>
                <li><a href="{{ url_for('admin.settings') }}">Settings</a></li>
            </ul>
        </div>
        <div class="admin-content">
            {% block admin_content %}{% endblock %}
        </div>
    </div>
{% endblock %}

{% extends "admin/base.html" %}

{% block title %}Admin - Users{% endblock %}

{% block admin_content %}
    <h1>User Management</h1>
    <table class="user-table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Username</th>
                <th>Email</th>
                <th>Role</th>
                <th>Created</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            {% for user in users %}
                <tr>
                    <td>{{ user.id }}</td>
                    <td>{{ user.username }}</td>
                    <td>{{ user.email }}</td>
                    <td>{{ user.role }}</td>
                    <td>{{ user.created_at|strftime('%Y-%m-%d') }}</td>
                    <td>
                        <a href="{{ url_for('admin.edit_user', user_id=user.id) }}" class="btn btn-edit">Edit</a>
                        <a href="{{ url_for('admin.delete_user', user_id=user.id) }}" class="btn btn-delete" data-confirm="Are you sure?">Delete</a>
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
graph TD A[base.html] --> B[home.html] A --> C[about.html] A --> D[contact.html] A --> E[admin/base.html] E --> F[admin/dashboard.html] E --> G[admin/users.html] E --> H[admin/posts.html] E --> I[admin/settings.html]

Template Includes

For reusing smaller template fragments, you can use the include statement:

Creating Reusable Fragments


{% macro render_pagination(pagination, endpoint) %}
    <div class="pagination">
        {% if pagination.has_prev %}
            <a href="{{ url_for(endpoint, page=pagination.prev_num) }}">« Previous</a>
        {% else %}
            <span class="disabled">« Previous</span>
        {% endif %}
        
        {% for page in pagination.iter_pages() %}
            {% if page %}
                {% if page != pagination.page %}
                    <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
                {% else %}
                    <span class="current">{{ page }}</span>
                {% endif %}
            {% else %}
                <span class="ellipsis">…</span>
            {% endif %}
        {% endfor %}
        
        {% if pagination.has_next %}
            <a href="{{ url_for(endpoint, page=pagination.next_num) }}">Next »</a>
        {% else %}
            <span class="disabled">Next »</span>
        {% endif %}
    </div>
{% endmacro %}

{% if messages %}
    <div class="flash-messages">
        {% for category, message in messages %}
            <div class="alert alert-{{ category }}">
                {{ message }}
                <button type="button" class="close" data-dismiss="alert">×</button>
            </div>
        {% endfor %}
    </div>
{% endif %}

Including Template Fragments


{% extends "base.html" %}

{% block content %}
    <h1>Blog Posts</h1>
    
    {% include "partials/flash_messages.html" %}
    
    <div class="posts-list">
        {% for post in posts.items %}
            <article class="post">
                <h2><a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>
                <div class="meta">
                    By {{ post.author.name }} on {{ post.created_at|strftime('%B %d, %Y') }}
                </div>
                <div class="summary">
                    {{ post.content|truncate(200) }}
                    <a href="{{ url_for('post', post_id=post.id) }}">Read more</a>
                </div>
            </article>
        {% endfor %}
    </div>
    
    {% from "partials/pagination.html" import render_pagination %}
    {{ render_pagination(posts, 'posts') }}
{% endblock %}

Include vs. Import

Jinja2 offers two ways to reuse template code:

  • {% include "template.html" %} - Includes the content of another template directly
  • {% import "macros.html" as macros %} - Imports macros from another template
  • {% from "macros.html" import render_field %} - Imports specific macros

Use include for HTML fragments and import for reusable macros.

Custom Filters and Tests

Flask allows you to create custom Jinja2 filters and tests to extend the template engine's capabilities.

Custom Filters

# app.py
@app.template_filter('strftime')
def _jinja2_filter_datetime(date, fmt=None):
    if fmt is None:
        fmt = '%Y-%m-%d'
    return date.strftime(fmt)

@app.template_filter('timesince')
def _jinja2_filter_timesince(dt, default="just now"):
    """
    Returns string representing "time since" e.g. "3 days ago", "5 minutes ago"
    """
    now = datetime.utcnow()
    diff = now - dt
    
    periods = (
        (diff.days / 365, "year", "years"),
        (diff.days / 30, "month", "months"),
        (diff.days / 7, "week", "weeks"),
        (diff.days, "day", "days"),
        (diff.seconds / 3600, "hour", "hours"),
        (diff.seconds / 60, "minute", "minutes"),
        (diff.seconds, "second", "seconds"),
    )
    
    for period, singular, plural in periods:
        if int(period) > 0:
            return f"{int(period)} {singular if int(period) == 1 else plural} ago"
    
    return default

Custom Tests

# app.py
@app.template_test('admin')
def _jinja2_test_admin(user):
    return user.role == 'admin'

@app.template_test('image')
def _jinja2_test_image(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'}

Using Custom Filters and Tests

<p>Posted {{ post.created_at|timesince }}</p>

{% if user is admin %}
    <div class="admin-controls">
        <a href="{{ url_for('admin.dashboard') }}">Admin Dashboard</a>
    </div>
{% endif %}

{% if filename is image %}
    <img src="{{ url_for('static', filename='uploads/' + filename) }}" alt="Image">
{% else %}
    <a href="{{ url_for('static', filename='uploads/' + filename) }}">Download {{ filename }}</a>
{% endif %}

Real-World Example: Blog Template with Custom Filters

Here's a more complex blog post template using custom filters and tests:


{% extends "base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
    <article class="blog-post">
        <header>
            <h1>{{ post.title }}</h1>
            <div class="meta">
                By <a href="{{ url_for('author', username=post.author.username) }}">{{ post.author.name }}</a> 
                on {{ post.created_at|strftime('%B %d, %Y') }}
                {% if post.created_at != post.updated_at %}
                    (Updated {{ post.updated_at|timesince }})
                {% endif %}
                
                {% if post.tags %}
                    | Tags: 
                    {% for tag in post.tags %}
                        <a href="{{ url_for('tag', tag=tag) }}" class="tag">{{ tag }}</a>
                        {%- if not loop.last %}, {% endif %}
                    {% endfor %}
                {% endif %}
            </div>
        </header>
        
        {% if post.featured_image %}
            <div class="featured-image">
                <img src="{{ post.featured_image }}" alt="{{ post.title }}">
                {% if post.image_caption %}
                    <figcaption>{{ post.image_caption }}</figcaption>
                {% endif %}
            </div>
        {% endif %}
        
        <div class="content">
            {% if post.format == 'markdown' %}
                {{ post.content|markdown|safe }}
            {% else %}
                {{ post.content|safe }}
            {% endif %}
        </div>
        
        <footer>
            <div class="share">
                <h3>Share this post</h3>
                <a href="{{ post|twitter_share_url }}" class="btn btn-twitter" target="_blank">Twitter</a>
                <a href="{{ post|facebook_share_url }}" class="btn btn-facebook" target="_blank">Facebook</a>
                <a href="{{ post|linkedin_share_url }}" class="btn btn-linkedin" target="_blank">LinkedIn</a>
            </div>
            
            {% if current_user is admin or current_user.id == post.author.id %}
                <div class="admin-controls">
                    <a href="{{ url_for('edit_post', post_id=post.id) }}" class="btn btn-edit">Edit</a>
                    <a href="{{ url_for('delete_post', post_id=post.id) }}" class="btn btn-delete" data-confirm="Are you sure you want to delete this post?">Delete</a>
                </div>
            {% endif %}
            
            <div class="author-bio">
                <h3>About the Author</h3>
                <div class="author-details">
                    <img src="{{ post.author.avatar|default(url_for('static', filename='img/default-avatar.png')) }}" alt="{{ post.author.name }}">
                    <div>
                        <h4>{{ post.author.name }}</h4>
                        <p>{{ post.author.bio|default('No bio available.')|truncate(150) }}</p>
                        <a href="{{ url_for('author', username=post.author.username) }}">View all posts</a>
                    </div>
                </div>
            </div>
            
            <div class="related-posts">
                <h3>Related Posts</h3>
                <ul>
                    {% for related_post in related_posts %}
                        <li>
                            <a href="{{ url_for('post', post_id=related_post.id) }}">{{ related_post.title }}</a> 
                            ({{ related_post.created_at|timesince }})
                        </li>
                    {% else %}
                        <li>No related posts found.</li>
                    {% endfor %}
                </ul>
            </div>
        </footer>
    </article>
    
    <section class="comments">
        <h2>{{ post.comments|length }} Comment{% if post.comments|length != 1 %}s{% endif %}</h2>
        
        {% if current_user.is_authenticated %}
            <div class="comment-form">
                <h3>Add a Comment</h3>
                <form action="{{ url_for('add_comment', post_id=post.id) }}" method="post">
                    {{ comment_form.csrf_token }}
                    <div class="form-group">
                        {{ comment_form.content.label }}
                        {{ comment_form.content(class="form-control") }}
                        {% if comment_form.content.errors %}
                            {% for error in comment_form.content.errors %}
                                <span class="error">{{ error }}</span>
                            {% endfor %}
                        {% endif %}
                    </div>
                    <button type="submit" class="btn btn-primary">Submit</button>
                </form>
            </div>
        {% else %}
            <p><a href="{{ url_for('login', next=request.path) }}">Log in</a> to add a comment.</p>
        {% endif %}
        
        <div class="comments-list">
            {% for comment in post.comments|sort(attribute='created_at', reverse=true) %}
                <div class="comment" id="comment-{{ comment.id }}">
                    <div class="comment-header">
                        <img src="{{ comment.author.avatar|default(url_for('static', filename='img/default-avatar.png')) }}" alt="{{ comment.author.name }}" class="avatar">
                        <span class="name">{{ comment.author.name }}</span>
                        <span class="date">{{ comment.created_at|timesince }}</span>
                    </div>
                    <div class="comment-content">
                        {{ comment.content|urlize|nl2br }}
                    </div>
                    {% if current_user.is_authenticated and (current_user.id == comment.author.id or current_user is admin) %}
                        <div class="comment-actions">
                            <a href="{{ url_for('edit_comment', comment_id=comment.id) }}" class="btn-edit">Edit</a>
                            <a href="{{ url_for('delete_comment', comment_id=comment.id) }}" class="btn-delete" data-confirm="Are you sure?">Delete</a>
                        </div>
                    {% endif %}
                </div>
            {% else %}
                <p>No comments yet. Be the first to comment!</p>
            {% endfor %}
        </div>
    </section>
{% endblock %}

{% block scripts %}
    {{ super() }}
    <script src="{{ url_for('static', filename='js/comments.js') }}"></script>
{% endblock %}

Practical Exercise

Build a Blog Template System

Create a template system for a blog application using Jinja2:

Requirements

  1. Create a base template with common elements (header, footer, navigation)
  2. Create templates for the home page, blog post list, single post view, and about page
  3. Implement template inheritance
  4. Create reusable components (pagination, post preview, comments section)
  5. Use filters and conditional logic

Steps

  1. Set up a Flask application with appropriate routes
  2. Create a base.html template with necessary blocks
  3. Implement child templates that extend the base template
  4. Create partial templates for reusable components
  5. Add at least one custom filter
  6. Create some sample data for testing templates

Sample Data

posts = [
    {
        'id': 1,
        'title': 'Getting Started with Flask',
        'content': 'Flask is a lightweight web framework for Python...',
        'author': {'name': 'John Doe', 'username': 'johndoe'},
        'created_at': datetime(2025, 2, 15, 12, 30),
        'tags': ['Flask', 'Python', 'Web Development']
    },
    {
        'id': 2,
        'title': 'Understanding Jinja2 Templates',
        'content': 'Jinja2 is a powerful templating engine...',
        'author': {'name': 'Jane Smith', 'username': 'janesmith'},
        'created_at': datetime(2025, 3, 10, 15, 45),
        'tags': ['Jinja2', 'Templates', 'Flask']
    },
    # Add more posts...
]

Bonus Challenges

  • Add a search feature with a search results template
  • Implement a tag cloud using custom filters
  • Create an admin dashboard template
  • Add a contact form template with validation feedback

Summary

Key Takeaways

Additional Resources

mindmap root((Jinja2 Templating)) Syntax Variables {{ }} Statements {% %} Comments {# #} Expressions Filters Tests Math Operations Logic Control Structures Conditionals Loops Assignments Organization Inheritance Includes Macros Blocks Customization Custom Filters Custom Tests Extensions

Next Lecture

In our next lecture, we'll explore Template Inheritance and Includes in more depth, focusing on building a complete site structure with Jinja2.

Practice Activities

Basic Exercises

  1. Create a base template with header, footer, and navigation
  2. Implement at least three child templates that extend the base template
  3. Create a template with a for loop to display a list of items
  4. Create a template with conditionals to show different content based on variables
  5. Implement a custom filter and use it in your templates

Advanced Project

Build a portfolio website template system: