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,.jinja2extensions - Serves
index.htmlorindex.htmfor 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:
- Exact match:
/about→about.html - With extensions:
/about→ triesabout.html,about.htm,about.jinja,about.jinja2 - Index files:
/blog/→ triesblog/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
Routeclass - 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 StarletteRequestobject- 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>© {{ 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) }}