agentleFS
MathiasPaulenkoMathiasPaulenko/ai-toolkit/skills/jinja2-templates/SKILL.md

jinja2-templates

Skill2 starsChanged 3 months agoMITView on GitHub

Production-grade skill for Jinja2 templating in Python. Covers syntax, inheritance, macros, filters, tests, escaping, security (autoescape, XSS), Flask/FastAPI integration, and standalone code generation.

  • Reads credentials
  • Installs packages
Files
SKILL.md

Jinja2 Templates Skill

Description

Comprehensive skill for Jinja2, the dominant templating engine in Python. Covers syntax, template inheritance, macros, custom filters and tests, escaping and XSS prevention, integration with Flask and FastAPI, and standalone usage for email generation, configuration files, and code generation.

Table of Contents

  1. Project Setup
  2. Basic Syntax
  3. Control Flow
  4. Template Inheritance
  5. Includes & Imports
  6. Macros
  7. Filters
  8. Tests
  9. Escaping & Security
  10. Flask Integration
  11. FastAPI Integration
  12. Standalone Usage
  13. Best Practices Checklist
  14. Common Pitfalls
  15. References

When to Invoke

  • Writing or refactoring Jinja2 templates in Flask, FastAPI, or standalone Python
  • Creating reusable template layouts with inheritance (extends, block)
  • Building custom filters or tests for domain-specific logic
  • Preventing XSS through proper escaping and autoescape
  • Generating HTML emails, configuration files, or code via templates
  • Integrating Jinja2 with Flask (render_template, flash, url_for)
  • Integrating Jinja2 with FastAPI/Starlette (Jinja2Templates)
  • Debugging template rendering errors or variable scope issues

1. Project Setup

Installation

pip install Jinja2>=3.1.0

# For Flask (already bundled, but explicit for standalone)
pip install Flask

# For FastAPI
pip install fastapi starlette jinja2

# For i18n support
pip install Babel

Directory Layout

project/
  templates/              # Jinja2 template files
    base.html
    layout_email.txt
    partials/
      header.html
      footer.html
      nav.html
    macros/
      forms.html
      alerts.html
    pages/
      home.html
      profile.html
    emails/
      welcome.txt
      welcome.html
  static/                 # CSS, JS, images (Flask/FastAPI)
  app.py                  # Application entry point

2. Basic Syntax

Expressions

{{ variable }}
{{ variable.attribute }}
{{ variable['key'] }}
{{ function(arg1, arg2) }}
{{ user.name | upper }}
{{ items | length }}
{{ "Hello, %s" | format(name) }}

Statements

{% if user.is_active %}
  <p>Welcome back!</p>
{% endif %}

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

Comments

{# This is a comment and will not appear in the output #}

Whitespace Control

{%- if true -%}  {# Trims leading and trailing whitespace #}
  No extra whitespace
{%- endif -%}

Configure globally in the environment:

from jinja2 import Environment

env = Environment(trim_blocks=True, lstrip_blocks=True)

3. Control Flow

If / Elif / Else

{% if user.role == 'admin' %}
  <div class="admin-panel">...</div>
{% elif user.role == 'editor' %}
  <div class="editor-panel">...</div>
{% else %}
  <div class="user-panel">...</div>
{% endif %}

Inline If (Ternary)

{{ 'Active' if user.is_active else 'Inactive' }}

For Loop

{% for user in users %}
  <tr class="{{ loop.cycle('odd', 'even') }}">
    <td>{{ loop.index }}</td>
    <td>{{ user.name }}</td>
  </tr>
{% else %}
  <tr><td colspan="2">No users found</td></tr>
{% endfor %}

Loop Variables

VariableDescription
loop.index1-based counter
loop.index00-based counter
loop.firstTrue on first iteration
loop.lastTrue on last iteration
loop.lengthTotal number of items
loop.cycle(...)Cycle through values
loop.revindex1-based counter from end

Set Variable

{% set total = items | sum %}
{% set alert_class = 'danger' if error else 'success' %}

Block Assignments (Set with content)

{% set content %}
  <p>This is a multi-line block assignment.</p>
  <p>Useful for capturing HTML into a variable.</p>
{% endset %}

4. Template Inheritance

Base Template

{# templates/base.html #}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}Default Title{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
  {% block extra_css %}{% endblock %}
</head>
<body>
  {% include "partials/header.html" %}

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

  {% include "partials/footer.html" %}

  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
  {% block extra_js %}{% endblock %}
</body>
</html>

Child Template

{# templates/pages/home.html #}
{% extends "base.html" %}

{% block title %}Home — My App{% endblock %}

{% block extra_css %}
  <link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}">
{% endblock %}

{% block content %}
  <h1>Welcome, {{ user.name }}</h1>
  {% include "partials/post_list.html" %}
{% endblock %}

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

Multiple Levels of Inheritance

{# templates/layouts/dashboard.html #}
{% extends "base.html" %}

{% block content %}
  <div class="dashboard-layout">
    {% block sidebar %}{% endblock %}
    <div class="dashboard-content">
      {% block dashboard_content %}{% endblock %}
    </div>
  </div>
{% endblock %}

super() — Call Parent Block Content

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

5. Includes & Imports

Include

{% include "partials/nav.html" %}

{% include "partials/alert.html" ignore missing %}

{% include "partials/widget.html" with context %}
{% include "partials/widget.html" without context %}

Import Macros

{% from "macros/forms.html" import render_field, render_submit %}

<form method="POST">
  {{ render_field(form.username) }}
  {{ render_field(form.email) }}
  {{ render_submit("Save") }}
</form>

Import with Namespace

{% import "macros/forms.html" as forms %}

{{ forms.render_field(form.username) }}
{{ forms.render_field(form.email) }}

6. Macros

Defining Macros

{# templates/macros/forms.html #}
{% macro render_field(field, label=None, extra_class="") %}
  <div class="form-group {{ extra_class }}">
    <label for="{{ field.id }}">{{ label or field.label }}</label>
    <input
      type="{{ field.type }}"
      id="{{ field.id }}"
      name="{{ field.name }}"
      value="{{ field.value | default('') }}"
      class="form-control {% if field.errors %}is-invalid{% endif %}"
    >
    {% if field.errors %}
      <div class="invalid-feedback">
        {{ field.errors | join(', ') }}
      </div>
    {% endif %}
  </div>
{% endmacro %}

{% macro render_submit(text="Submit", variant="primary") %}
  <button type="submit" class="btn btn-{{ variant }}">{{ text }}</button>
{% endmacro %}

Macro with caller (Block Macros)

{% macro modal(title, id="modal") %}
  <div class="modal" id="{{ id }}">
    <div class="modal-header">
      <h5>{{ title }}</h5>
    </div>
    <div class="modal-body">
      {{ caller() }}
    </div>
  </div>
{% endmacro %}
{% call modal("Confirm Delete", id="delete-modal") %}
  <p>Are you sure you want to delete this item?</p>
  <button class="btn btn-danger">Delete</button>
{% endcall %}

7. Filters

Built-in Filters

{{ name | upper }}
{{ name | lower }}
{{ name | capitalize }}
{{ name | title }}
{{ name | trim }}
{{ name | replace("old", "new") }}
{{ items | length }}
{{ items | join(", ") }}
{{ items | sort }}
{{ items | reverse }}
{{ items | first }}
{{ items | last }}
{{ items | unique }}
{{ number | round(2) }}
{{ price | default(0.00) }}
{{ html_content | safe }}              {# Dangerous — use with care #}
{{ user_input | escape }}              {# Force escaping #}

Custom Filters (Python)

# filters.py
from datetime import datetime
import markdown

def format_datetime(value, fmt="%Y-%m-%d %H:%M"):
    """Format a datetime object."""
    if value is None:
        return ""
    return value.strftime(fmt)

def markdown_filter(value):
    """Convert Markdown to HTML."""
    return markdown.markdown(value, extensions=["fenced_code"])

def currency(value, symbol="$", decimals=2):
    """Format as currency."""
    return f"{symbol}{value:,.{decimals}f}"

Register in Flask

from flask import Flask
from filters import format_datetime, markdown_filter, currency

app = Flask(__name__)
app.jinja_env.filters["datetime"] = format_datetime
app.jinja_env.filters["markdown"] = markdown_filter
app.jinja_env.filters["currency"] = currency

Register in Standalone Jinja2

from jinja2 import Environment, FileSystemLoader
from filters import format_datetime

env = Environment(loader=FileSystemLoader("templates"))
env.filters["datetime"] = format_datetime

Chaining Filters

{{ text | trim | lower | replace(" ", "-") }}
{{ items | selectattr("active") | list | length }}

8. Tests

Built-in Tests

{% if user is defined %}
{% if value is none %}
{% if items is iterable %}
{% if items is sequence %}
{% if items is mapping %}
{% if number is number %}
{% if text is string %}
{% if value is odd %}
{% if value is even %}
{% if value is divisibleby(3) %}
{% if value is sameas(other) %}

Custom Tests

# tests.py
def is_email(value):
    """Test if value looks like an email."""
    import re
    return bool(re.match(r"^[^@]+@[^@]+\.[^@]+$", str(value)))

def is_strong_password(value):
    """Test if password meets strength criteria."""
    return (
        len(value) >= 8
        and any(c.isupper() for c in value)
        and any(c.isdigit() for c in value)
    )
# Register in Flask
app.jinja_env.tests["email"] = is_email
app.jinja_env.tests["strong_password"] = is_strong_password
{% if form.email.data is email %}
  <span class="valid">Valid email</span>
{% endif %}

9. Escaping & Security

Autoescape

Jinja2 escapes all output by default in HTML contexts. Never disable this unless you explicitly trust the source.

{# SAFE — autoescaped by default #}
{{ user_input }}

{# SAFE — explicit escaping #}
{{ user_input | escape }}

{# DANGEROUS — marks as safe, bypasses escaping #}
{{ user_input | safe }}

{# SAFE — if content is from a trusted Markdown parser #}
{{ markdown_content | markdown | safe }}

MarkupSafe Objects

from markupsafe import Markup

# Mark a string as safe programmatically
trusted_html = Markup("<strong>Hello</strong>")

Disabling Autoescape (Dangerous)

{% autoescape false %}
  {{ raw_html }}
{% endautoescape %}
# Flask: Disable for a specific template (avoid if possible)
from flask import render_template_string
render_template_string("...", autoescape=False)

Security Checklist

  • Never pass raw user input to | safe
  • Use Markup only for output from trusted parsers (Markdown, Bleach)
  • Keep autoescape enabled globally
  • Sanitize HTML before marking safe (use bleach.clean())
  • Validate all variables before rendering

10. Flask Integration

Rendering Templates

from flask import Flask, render_template, render_template_string

app = Flask(__name__)

@app.route("/")
def home():
    return render_template("pages/home.html", user=current_user, posts=posts)

@app.route("/greeting/<name>")
def greeting(name):
    return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)

Template Context Processors

@app.context_processor
def inject_globals():
    return {
        "app_name": "My App",
        "current_year": datetime.now().year,
    }

Custom Jinja2 Environment

class CustomFlask(Flask):
    def create_jinja_environment(self):
        env = super().create_jinja_environment()
        env.trim_blocks = True
        env.lstrip_blocks = True
        env.filters["datetime"] = format_datetime
        return env

URL Generation in Templates

<a href="{{ url_for('user.profile', user_id=user.id) }}">Profile</a>
<a href="{{ url_for('static', filename='css/main.css') }}">CSS</a>

11. FastAPI Integration

Setup with Starlette

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

templates = Jinja2Templates(directory="templates")

@app.get("/")
def home(request: Request):
    return templates.TemplateResponse(
        "pages/home.html",
        {"request": request, "title": "Home", "items": [1, 2, 3]}
    )

Adding Custom Filters to FastAPI

templates = Jinja2Templates(directory="templates")
templates.env.filters["datetime"] = format_datetime
templates.env.tests["email"] = is_email

Jinja2Context in FastAPI

from starlette.middleware.base import BaseHTTPMiddleware

class TemplateContextMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        request.state.template_context = {
            "app_name": "My FastAPI App",
        }
        return await call_next(request)

12. Standalone Usage

Email Generation

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("templates/emails"))
template = env.get_template("welcome.txt")

body = template.render(
    user_name="John",
    activation_link="https://example.com/activate/abc123"
)
{# templates/emails/welcome.txt #}
Hello {{ user_name }},

Welcome to {{ app_name }}. Please activate your account:
{{ activation_link }}

Thanks,
The {{ app_name }} Team

Configuration File Generation

env = Environment(loader=FileSystemLoader("templates/configs"))
template = env.get_template("nginx.conf.j2")

config = template.render(
    server_name="api.example.com",
    port=8000,
    workers=4
)

Code Generation

template = env.from_string("""
class {{ class_name }}:
    def __init__(self{% for field in fields %}, {{ field.name }}: {{ field.type }}{% endfor %}):
        {% for field in fields %}
        self.{{ field.name }} = {{ field.name }}
        {% endfor %}
""")

code = template.render(
    class_name="User",
    fields=[
        {"name": "id", "type": "int"},
        {"name": "email", "type": "str"},
    ]
)

In-Memory Templates

from jinja2 import DictLoader

env = Environment(loader=DictLoader({
    "hello.html": "<h1>Hello, {{ name }}!</h1>",
    "goodbye.html": "<p>Goodbye, {{ name }}.</p>",
}))

template = env.get_template("hello.html")
output = template.render(name="World")

13. Best Practices Checklist

  • Use template inheritance (extends + block) for all pages; no duplication
  • Place reusable components in partials/ and macros in macros/
  • Keep business logic out of templates; use custom filters/tests in Python
  • Enable trim_blocks and lstrip_blocks to control whitespace
  • Pass only necessary data to templates; avoid leaking entire objects
  • Use | default('') for optional variables to prevent UndefinedError
  • Keep autoescape enabled; never use | safe on user input
  • Sanitize HTML with bleach before marking | safe
  • Use {% include ... ignore missing %} for optional partials
  • Define macros in dedicated files and import with {% from ... import %}
  • Use loop.cycle for alternating row classes in tables
  • Use caller() for macros that wrap variable content
  • Register filters/tests in application setup, not per-request
  • Use url_for() (Flask) or request.url_for() (FastAPI) for URLs
  • Pre-compile templates in production (compiled_cache or bytecode cache)
  • Add {% set %} for complex logic to avoid inline repetition
  • Use super() to extend parent blocks without replacing them entirely
  • Separate email text and HTML templates (welcome.txt + welcome.html)
  • Use without context in includes when full context is unnecessary

14. Common Pitfalls

ProblemCauseSolution
UndefinedErrorVariable not passed to templateUse `default('')` or pass the variable
XSS injection| safe on untrusted inputRemove | safe or sanitize with bleach
Whitespace noiseDefault whitespace preservationEnable trim_blocks and lstrip_blocks
Missing partialWrong include path or missing fileUse ignore missing or verify path
Macro not foundMissing import or wrong namespaceCheck {% import %} path and namespace
block not renderingMissing {% extends %}Ensure child template extends base
Double escapingAlready escaped + autoescapeTrust autoescape; don't manually escape
Context leakIncluded partial sees all variablesUse without context or pass explicit vars
Slow renderingNo template cachingEnable bytecode cache (FileSystemBytecodeCache)
Mutable default argsdefault([]) with mutableUse default([]) is safe in Jinja2; check Python

15. References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.