# plain.templates
**Render HTML templates using Jinja2.**
- [Overview](https://plainframework.com/docs/plain-templates/plain/templates/?llm#overview)
- [Template files](https://plainframework.com/docs/plain-templates/plain/templates/?llm#template-files)
- [Template-rendering views](https://plainframework.com/docs/plain-templates/plain/templates/?llm#template-rendering-views)
- [TemplateView](https://plainframework.com/docs/plain-templates/plain/templates/?llm#templateview)
- [FormView](https://plainframework.com/docs/plain-templates/plain/templates/?llm#formview)
- [DetailView, CreateView, UpdateView, DeleteView, ListView](https://plainframework.com/docs/plain-templates/plain/templates/?llm#object-views)
- [Error views](https://plainframework.com/docs/plain-templates/plain/templates/?llm#error-views)
- [Template context](https://plainframework.com/docs/plain-templates/plain/templates/?llm#template-context)
- [Built-in globals](https://plainframework.com/docs/plain-templates/plain/templates/?llm#built-in-globals)
- [Built-in filters](https://plainframework.com/docs/plain-templates/plain/templates/?llm#built-in-filters)
- [Custom globals and filters](https://plainframework.com/docs/plain-templates/plain/templates/?llm#custom-globals-and-filters)
- [Custom template extensions](https://plainframework.com/docs/plain-templates/plain/templates/?llm#custom-template-extensions)
- [Rendering templates manually](https://plainframework.com/docs/plain-templates/plain/templates/?llm#rendering-templates-manually)
- [Custom Jinja environment](https://plainframework.com/docs/plain-templates/plain/templates/?llm#custom-jinja-environment)
- [Forms](https://plainframework.com/docs/plain-templates/plain/templates/?llm#forms)
- [FAQs](https://plainframework.com/docs/plain-templates/plain/templates/?llm#faqs)
- [Installation](https://plainframework.com/docs/plain-templates/plain/templates/?llm#installation)
## Overview
Plain uses Jinja2 for template rendering. You can refer to the [Jinja documentation](https://jinja.palletsprojects.com/en/stable/) for all of the features available.
Templates are typically used with [`TemplateView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#TemplateView) or one of its subclasses (see [Template-rendering views](https://plainframework.com/docs/plain-templates/plain/templates/?llm#template-rendering-views)).
```python
# app/views.py
from plain.templates.views import TemplateView
class ExampleView(TemplateView):
template_name = "example.html"
def get_template_context(self):
context = super().get_template_context()
context["message"] = "Hello, world!"
return context
```
```html
{% extends "base.html" %}
{% block content %}
{{ message }}
{% endblock %}
```
## Template files
Template files can live in two locations:
1. **`app/templates/`** - Your app's templates (highest priority)
2. **`{package}/templates/`** - Templates inside any installed package
All template directories are merged together, so you can override templates from installed packages by creating a file with the same name in `app/templates/`.
## Template-rendering views
`plain.templates.views` ships the view classes that render templates. The base [`View`](https://plainframework.com/docs/plain/plain/views/README.md?llm) class lives in core `plain.views` and doesn't know about templates — install `plain.templates` to use any of these.
### TemplateView
[`TemplateView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#TemplateView) renders a Jinja template:
```python
from plain.templates.views import TemplateView
class ExampleView(TemplateView):
template_name = "example.html"
def get_template_context(self):
context = super().get_template_context()
context["message"] = "Hello, world!"
return context
```
For simple pages that don't need custom context, configure `TemplateView` directly in your URL routes:
```python
from plain.templates.views import TemplateView
from plain.urls import path, Router
class AppRouter(Router):
routes = [
path("/example/", TemplateView.as_view(template_name="example.html")),
]
```
### FormView
[`FormView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#FormView) handles displaying and processing [forms](https://plainframework.com/docs/plain/plain/forms/README.md?llm). The form is automatically available in your template as `form`:
```python
from plain.templates.views import FormView
from .forms import ExampleForm
class ExampleView(FormView):
template_name = "example.html"
form_class = ExampleForm
success_url = "."
def form_valid(self, form):
return super().form_valid(form)
```
### Object views
[`DetailView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#DetailView), [`CreateView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#CreateView), [`UpdateView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#UpdateView), [`DeleteView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#DeleteView), and [`ListView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#ListView) provide standard CRUD scaffolding. Each requires you to implement `get_object()` or `get_objects()`:
```python
from plain.templates.views import DetailView
class ExampleDetailView(DetailView):
template_name = "detail.html"
def get_object(self):
return MyObjectClass.query.get(
id=self.url_kwargs["id"],
user=self.request.user,
)
```
The single object is exposed in templates as `object`; list views expose `objects`. Set `context_object_name` for a more descriptive name.
## Error views
`TemplateView` overrides `handle_exception` to render `{status}.html` for any exception that escapes the handler — `404.html` for `NotFoundError404`, `500.html` for unhandled errors, etc. The context is `{request, status_code, exception, DEBUG}`. On `TemplateFileMissing` the view returns a plain-text status response (`404 Not Found`, `500 Internal Server Error`); on any other render failure it logs and returns a bare-status `Response` so `_respond_to_exception` can still attach `response.exception` for observability.
Plain core's exception handler — the one that catches pre-view failures like URL resolution and middleware errors — returns plain text. To get a styled 404 for unmatched URLs, mount [`NotFoundView`](https://plainframework.com/docs/plain-templates/plain/templates/views.py?llm#NotFoundView) as the last route:
```python
from plain.templates.views import NotFoundView
from plain.urls import Router, path
class AppRouter(Router):
urls = [
# ... your routes ...
path("", NotFoundView),
]
```
`NotFoundView.before_request` raises `NotFoundError404` before method dispatch, so every HTTP method produces a 404 instead of a 405.
The resolver recognizes a sole-segment terminal `` as a **catchall**: it handles both `/missing` and `/missing/` from one mount, and it yields to trailing-slash redirects from specific routes. So `path("login/", LoginView)` followed by `path("", NotFoundView)` still 308's `/login` to `/login/` rather than serving the 404 — the catchall only fires when nothing else came close.
Your `500.html` template should be self-contained — avoid extending base templates or accessing the database/session, since 500s can fire during middleware or template-rendering errors. `404.html` and `403.html` can safely extend base templates since they happen after middleware runs.
## Template context
When using `TemplateView`, you pass data to templates by overriding `get_template_context()`:
```python
from plain.templates.views import TemplateView
class ProductView(TemplateView):
template_name = "product.html"
def get_template_context(self):
context = super().get_template_context()
context["product"] = Product.objects.get(id=self.url_kwargs["id"])
context["related_products"] = Product.objects.filter(category=context["product"].category)[:5]
return context
```
The context is then available in your template:
```html
{{ product.name }}
{% for item in related_products %}
{{ item.name }}
{% endfor %}
```
`get_template_context()` is a _pull_ — the framework calls it at render time, so the data has to be reachable from `self`. When a view writes its own handlers (a `.get()` and `.post()` that render the same template), `render(**context)` is the _push_ alternative: the handler passes context straight in, and gets the `Response` back.
```python
class ProductView(TemplateView):
template_name = "product.html"
def get(self):
return self.render(product=Product.query.get(id=self.url_kwargs["id"]))
```
`render(**context)` layers `context` over `get_template_context()`, so the base context (`request`, `DEBUG`, `template_names`) and anything the view's `get_template_context()` adds are still present.
## Built-in globals
Plain provides several [global functions](https://plainframework.com/docs/plain-templates/plain/templates/jinja/globals.py?llm) available in all templates:
| Global | Description |
| ---------------------------- | ---------------------------------- |
| `asset(path)` | Returns the URL for a static asset |
| `url(name, *args, **kwargs)` | Reverses a URL by name |
| `Paginator` | The Paginator class for pagination |
| `now()` | Returns the current datetime |
| `timedelta` | The timedelta class for date math |
| `localtime(dt)` | Converts a datetime to local time |
```html
View
Generated at {{ now() }}
```
## Built-in filters
Plain includes several [filters](https://plainframework.com/docs/plain-templates/plain/templates/jinja/filters.py?llm) for common operations:
| Filter | Description |
| ----------------------------- | ------------------------------------ |
| `strftime(format)` | Formats a datetime |
| `strptime(format)` | Parses a string to datetime |
| `fromtimestamp(ts)` | Creates datetime from timestamp |
| `fromisoformat(s)` | Creates datetime from ISO string |
| `localtime(tz)` | Converts to local timezone |
| `timeuntil` | Human-readable time until a date |
| `timesince` | Human-readable time since a date |
| `json_script(id)` | Outputs JSON safely in a script tag |
| `islice(stop)` | Slices iterables (useful for dicts) |
| `pluralize(singular, plural)` | Returns plural suffix based on count |
```html
{{ data|json_script("page-data") }}
```
## Custom globals and filters
You can register your own globals and filters in `app/templates.py` (or `{package}/templates.py`). These files are automatically imported when the template environment loads.
```python
# app/templates.py
from plain.templates import register_template_filter, register_template_global
@register_template_filter
def camel_case(value):
"""Convert a string to CamelCase."""
return value.replace("_", " ").title().replace(" ", "")
@register_template_global
def app_version():
"""Return the current app version."""
return "1.0.0"
```
Now you can use these in templates:
```html
{{ "my_variable"|camel_case }}
```
You can also register non-callable values as globals by providing a name:
```python
from plain.templates import register_template_global
register_template_global("1.0.0", name="APP_VERSION")
```
## Custom template extensions
For more complex template features, you can create Jinja extensions. The [`InclusionTagExtension`](https://plainframework.com/docs/plain-templates/plain/templates/jinja/extensions.py?llm#InclusionTagExtension) base class makes it easy to create custom tags that render their own templates.
```python
# app/templates.py
from plain.templates import register_template_extension
from plain.templates.jinja.extensions import InclusionTagExtension
from plain.runtime import settings
@register_template_extension
class AlertExtension(InclusionTagExtension):
tags = {"alert"}
template_name = "components/alert.html"
def get_context(self, context, *args, **kwargs):
return {
"message": args[0] if args else "",
"type": kwargs.get("type", "info"),
}
```
```html
{{ message }}
```
```html
{% alert "Something happened!" type="warning" %}
```
## Rendering templates manually
You can render templates outside of views using the [`Template`](https://plainframework.com/docs/plain-templates/plain/templates/core.py?llm#Template) class.
```python
from plain.templates import Template
html = Template("email/welcome.html").render({
"user_name": "Alice",
"activation_url": "https://example.com/activate/abc123",
})
```
If the template file doesn't exist, a [`TemplateFileMissing`](https://plainframework.com/docs/plain-templates/plain/templates/core.py?llm#TemplateFileMissing) exception is raised.
## Custom Jinja environment
By default, Plain uses a [`DefaultEnvironment`](https://plainframework.com/docs/plain-templates/plain/templates/jinja/environments.py?llm#DefaultEnvironment) that configures Jinja2 with sensible defaults:
- **Autoescaping** enabled for security
- **StrictUndefined** so undefined variables raise errors
- **Auto-reload** in debug mode
- **Loop controls** extension (`break`, `continue`)
- **Debug** extension
You can customize the environment by creating your own class and pointing to it in settings:
```python
# app/jinja.py
from plain.templates.jinja.environments import DefaultEnvironment
class CustomEnvironment(DefaultEnvironment):
def __init__(self):
super().__init__()
# Add your customizations here
self.globals["CUSTOM_SETTING"] = "value"
```
```python
# app/settings.py
TEMPLATES_JINJA_ENVIRONMENT = "app.jinja.CustomEnvironment"
```
## FAQs
#### Why am I getting "undefined variable" errors?
Plain uses Jinja's `StrictUndefined` mode, which raises an error when you reference a variable that doesn't exist in the context. This helps catch typos and missing data early. Make sure you're passing all required variables in `get_template_context()`.
#### Why does my template show an error about a callable?
Plain's template environment prevents accidentally rendering callables (functions, methods) directly. If you see an error like "X is callable, did you forget parentheses?", you probably need to add `()` to call the function:
```html
{{ user.get_full_name }}
{{ user.get_full_name() }}
```
#### How do I use Jinja's loop controls?
Plain enables the `loopcontrols` extension by default, so you can use `break` and `continue` in loops:
```html
{% for item in items %}
{% if item.skip %}
{% continue %}
{% endif %}
{% if item.stop %}
{% break %}
{% endif %}
{{ item.name }}
{% endfor %}
```
#### Where can I learn more about Jinja2?
The [Jinja2 documentation](https://jinja.palletsprojects.com/en/stable/) covers all the template syntax, including conditionals, loops, macros, and inheritance.
## Forms
Forms are rendered manually using the bound field attributes:
```html
```
Each bound field provides: `html_name`, `html_id`, `value`, `errors`, `field`, `initial`.
## Installation
Install the `plain.templates` package:
```bash
uv add plain.templates
```
Then add it to `INSTALLED_PACKAGES`:
```python
# app/settings.py
INSTALLED_PACKAGES = [
"plain.templates",
# ...
]
```