Skip to content

Templates

This guide covers template files, routing, and rendering in Starlette Templates.

Template Router

TemplateRouter is an ASGI app that serves HTML templates with automatic routing and file resolution.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from jinja2 import PackageLoader

from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    routes=[
        Mount("/", TemplateRouter()),
    ],
    middleware=[
        # Required for template autodiscovery
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
        )
    ]
)

Features

  • Automatically tries .html, .htm, .jinja, .jinja2 extensions
  • Serves index.html or index.htm for directory requests
  • HTTP caching with ETag and Last-Modified headers with 304 responses
  • Automatic gzip compression for responses over 500 bytes
  • Custom error pages with 404.html, 500.html, error.html
  • Custom context variables with context processors

File Resolution

When a request is made, TemplateRouter resolves templates in this order:

  1. Exact match: /aboutabout.html
  2. With extensions: /about → tries about.html, about.htm, about.jinja, about.jinja2
  3. Index files: /blog/ → tries blog/index.html, blog/index.htm

Configuration

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from starlette.requests import Request
from jinja2 import PackageLoader

from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware

async def add_context(request: Request) -> dict:
    return {"user": await get_current_user(request)}

app = Starlette(
    routes=[
        Mount("/", TemplateRouter(
            context_processors=[add_context],  # Add custom context
            cache_max_age=3600,                # 1 hour cache
            gzip_min_size=500,                 # Compress files > 500 bytes
        )),
    ],
    middleware=[
        # Required for template autodiscovery
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
        )
    ]
)

Route-Specific Context Processors

Add context processors that only run for specific URL patterns using Starlette's Route:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, Route
from starlette.requests import Request
from jinja2 import PackageLoader

from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware

# Global context processor - runs for ALL templates
async def add_user_context(request: Request) -> dict:
    return {"user": await get_current_user(request)}

# Route-specific context processors
async def add_country_context(request: Request) -> dict:
    """Only runs for /country/{code} paths."""
    code = request.path_params["code"]
    return {"country": await get_country(code)}

async def add_post_context(request: Request) -> dict:
    """Only runs for /blog/{post_id} paths."""
    post_id = request.path_params["post_id"]
    return {"post": await get_post(post_id)}

app = Starlette(
    routes=[
        Mount("/", TemplateRouter(
            context_processors=[
                add_user_context,  # Global - runs for all templates
                Route('/country/{code}', add_country_context),  # Only /country/* paths
                Route('/blog/{post_id}', add_post_context),  # Only /blog/* paths
            ]
        )),
    ],
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
        )
    ]
)

In your template at /country/us:

<h1>{{ country.name }}</h1>  <!-- Available from route-specific processor -->
<p>User: {{ user.name }}</p>  <!-- Available from global processor -->

Route-specific processors:

  • Have access to path parameters via request.path_params
  • Only execute when their route pattern matches the current request
  • Can be mixed with global processors in the same list
  • Use Starlette's existing Route class - no new abstractions

Template Response

Use TemplateResponse to render templates in route handlers:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Route
from starlette.requests import Request
from jinja2 import PackageLoader

from starlette_templates.responses import TemplateResponse
from starlette_templates.middleware import JinjaEnvMiddleware

async def homepage(request: Request) -> TemplateResponse:
    return TemplateResponse(
        "home.html",
        context={"title": "Welcome", "items": [1, 2, 3]}
    )

app = Starlette(
    routes=[
        Route("/", homepage),
    ],
    middleware=[
        # Required for template autodiscovery
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
        )
    ]
)

Context Variables

TemplateResponse automatically includes:

  • request - The Starlette Request object
  • All context variables from context processors
  • Any additional context passed to TemplateResponse

Template Context

All templates have access to:

Request Object

<p>Current path: {{ request.url.path }}</p>
<p>Host: {{ request.url.hostname }}</p>
<p>Method: {{ request.method }}</p>
<p>User agent: {{ request.headers.get('user-agent') }}</p>

URL Helpers

url_for

Generate URLs for named routes:

<!-- Generate URL for named route -->
<a href="{{ url_for('user_profile', user_id=123) }}">Profile</a>

<!-- Generate URL for static files -->
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">

url

Generate URLs for mounted apps:

<!-- Alternative syntax for mounted apps -->
<link rel="stylesheet" href="{{ url('static', '/css/style.css') }}">

absurl

Generate absolute URLs:

<!-- Generate absolute URL -->
<meta property="og:url" content="{{ absurl('/blog/post-1') }}">
<link rel="canonical" href="{{ absurl(request.url.path) }}">

JSON Serialization

Safely embed Python data in templates with jsonify:

<script id="data" type="application/json">{{ jsonify(data) }}</script>

This properly escapes and serializes Python objects to JSON.

Custom Filters and Globals

Add custom Jinja2 filters and globals through JinjaMiddleware:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import Environment, PackageLoader

from starlette_templates.middleware import JinjaMiddleware

def setup_jinja_env(env: Environment):
    # Add custom filter
    env.filters['uppercase'] = lambda x: x.upper()

    # Add custom global
    env.globals['site_name'] = "My Site"

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
            # Custom setup function
            environment_setup=setup_jinja_env,
        )
    ]
)

Then use in templates:

{{ "hello" | uppercase }}  <!-- HELLO -->
{{ site_name }}            <!-- My Site -->

Template Loaders

Configure template loading with Jinja2 loaders:

PackageLoader

Load templates from a Python package:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import PackageLoader

from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[
                PackageLoader("myapp", "templates"),
            ]
        )
    ]
)

FileSystemLoader

Load templates from filesystem directories:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import FileSystemLoader

from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[
                FileSystemLoader("custom/templates"),
            ]
        )
    ]
)

Multiple Loaders

Use multiple loaders with fallback priority:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import PackageLoader, FileSystemLoader

from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[
                PackageLoader("myapp", "templates"),      # Check here first
                FileSystemLoader("custom/templates"),     # Then here
            ],
            include_default_loader=True,  # Then built-in templates
        )
    ]
)

Error Pages

Create custom error pages by adding templates to your template directory:

404.html

<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
    <h1>{{ status_code }} - {{ error_title }}</h1>
    <p>{{ error_message }}</p>
    <a href="{{ url_for('home') }}">Go Home</a>
</body>
</html>

500.html

<!DOCTYPE html>
<html>
<head><title>500 Server Error</title></head>
<body>
    <h1>{{ status_code }} - {{ error_title }}</h1>
    <p>{{ error_message }}</p>

    {% if structured_errors %}
    <h2>Details:</h2>
    <ul>
        {% for error in structured_errors %}
        <li>{{ error.detail }}</li>
        {% endfor %}
    </ul>
    {% endif %}
</body>
</html>

error.html

Generic fallback error page for any error without a specific template:

<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
    <h1>{{ status_code }} - {{ error_title }}</h1>
    <p>{{ error_message }}</p>
</body>
</html>

Template Inheritance

Use Jinja2's template inheritance for consistent layouts:

base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
</head>
<body>
    <header>
        {% block header %}
        <h1>My Site</h1>
        {% endblock %}
    </header>

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

    <footer>
        {% block footer %}
        <p>&copy; {{ year }} My Site</p>
        {% endblock %}
    </footer>
</body>
</html>

page.html

{% extends "base.html" %}

{% block title %}About - {{ super() }}{% endblock %}

{% block content %}
<h2>About Us</h2>
<p>Welcome to our site!</p>
{% endblock %}

Including Templates

Include reusable template fragments:

<!-- Include a template fragment -->
{% include "partials/navigation.html" %}

<!-- Include with variables -->
{% include "partials/alert.html" with context %}

Macros

Define reusable template functions with macros:

{% macro render_field(field) %}
<div class="form-group">
    <label>{{ field.label }}</label>
    <input type="{{ field.type }}" name="{{ field.name }}" value="{{ field.value }}">
    {% if field.errors %}
    <div class="errors">{{ field.errors }}</div>
    {% endif %}
</div>
{% endmacro %}

<!-- Use the macro -->
{{ render_field(form.name) }}
{{ render_field(form.email) }}