with one click
askama
Askama Rust templating reference (0.16).
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Askama Rust templating reference (0.16).
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Captures the current session into a continuation prompt for a fresh one, or resumes from a pasted handoff. Use when context is running low, when ending a session mid-task, or when told to 'hand off' or write a handoff. `/handoff reusable` instead maintains a persistent multi-session task-runner state file (`docs/handoff-state.md`) that a fresh session reads, advances a few tasks via agent pairs, and re-saves.
Authors and normalizes tasks into `docs/todo.md`. Use when writing a todo list, capturing audit findings, or turning loose notes into pickup-cold-ready tasks.
Reconciles `README`, `docs/*`, `CLAUDE.md`, and agent/skill/plugin prompt files with what the code actually does: fixes stale, missing, or overpromising claims and verifies every quoted command/flag/path. Use after a change alters documented behavior, when a tool's output shape or fields change, or to sweep all docs.
Tunes a Rust `Cargo.toml` or `.cargo/config.toml`: profiles, features, workspaces, dependency hygiene. Use when cutting compile time or binary size, trimming deps, or configuring a workspace.
Boots and drives Android AVDs and iOS simulators from the CLI (adb, `xcrun simctl`, Flutter `integration_test`, Alchemist goldens). Use when running headless app tests, verifying screenshots, or debugging emulator boot/GPU issues.
Suggests project/crate/plugin names and checks availability across registries (crates.io, npm, PyPI, AUR, GitHub, domains; needs `bun`). Use before naming, renaming, or publishing anything.
| name | askama |
| description | Askama Rust templating reference (0.16). |
| metadata | {"author":"uwuclxdy","version":"1.5"} |
Captured 2026-07-10 (askama 0.16.0). To update: re-verify against the askama book + docs.rs/askama, then diff for changes.
Askama is a compile-time, Jinja-like template engine for Rust. Templates are parsed at build time and generate type-checked Rust code. This skill targets the unified askama crate, version 0.16.x (MSRV 1.88, raised in 0.15.0 from 1.83; was 1.81 through 0.13.x, then 1.83 in 0.14.0). Anything older than 0.13 wired web frameworks through dedicated askama_* crates that no longer exist; see the breaking-changes section if migrating.
Quick history: Askama was forked into
rinjain 2024, then the two projects re-unified asaskamain early 2025. If the user mentionsrinjaorrinja_axum, those are deprecated. Migrate toaskama/askama_web.
[dependencies]
askama = "0.16"
# For web frameworks, ADD askama_web separately (see §3):
askama_web = { version = "0.16", features = ["axum-0.8"] }
Do not depend on askama_axum, askama_actix, askama_warp, or askama_rocket (deprecated when 0.13 unified them), nor askama_gotham / askama_tide (never deprecated; just abandoned since 2023 on askama ^0.12). All dead now. There is no askama_actix_web crate.
templates/hello.html:
Hello, {{ name }}!
use askama::Template;
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> { name: &'a str }
HelloTemplate { name: "world" }.render()? // -> Result<String, askama::Error>
Templates live in templates/ at the crate root (next to Cargo.toml) by default. Override via askama.toml (see §11).
Prefer these over .to_string() or format!(), which route through a vtable at runtime and run 100-200% slower:
| Method | Output | When to use |
|---|---|---|
tmpl.render() | Result<String, askama::Error> | Default. Allocates a new String. |
tmpl.render_into(&mut writer) | writes into impl fmt::Write | Reusing a buffer, nested rendering. |
tmpl.write_into(&mut writer) | writes into impl io::Write | Writing directly to a socket / file. |
The Template trait exposes render_with_values(&dyn Values) for runtime-injected values that aren't on the struct.
The derive auto-computes a SIZE_HINT const from the template's static text; render() / render_with_values preallocate their String with it (no effect on the caller-owned writers of render_into / write_into). There is no size_hint attribute; #[template(size_hint = ...)] is a compile error (unsupported template attribute). Override the hint only by hand-implementing Template with a custom const SIZE_HINT.
Removed in 0.13+: Template::EXTENSION and Template::MIME_TYPE associated fields no longer exist. Do not reference them.
Two paths. Pick one.
Call .render() and wrap the string yourself.
// axum
use axum::response::{Html, IntoResponse};
async fn index() -> impl IntoResponse {
Html(IndexTemplate { title: "Home" }.render().unwrap())
}
For proper error handling, return Result<Html<String>, AppError> where AppError: IntoResponse and AppError::from(askama::Error) exists.
askama_web with WebTemplateOne derive implements the response trait for whichever feature flag you turned on (see the list below).
askama_web = { version = "0.16", features = ["axum-0.8"] }
use askama::Template;
use askama_web::WebTemplate;
#[derive(Template, WebTemplate)]
#[template(path = "hello.html")]
struct HelloTemplate { name: String }
async fn hello() -> HelloTemplate {
HelloTemplate { name: "world".into() }
}
Returns 200 OK, Content-Type: text/html; charset=utf-8. Render errors become 500.
Per-target feature flags for askama_web:
axum-0.8 / axum-0.7 / axum-core-0.5 / axum-core-0.4actix-web-4rocket-0.5warp-0.4 / warp-0.3poem-3trillium-0.2cot-0.3 / cot-0.4 / cot-0.5 / cot-0.6 / cot_core-0.6derive (WebTemplate derive, on by default) and eprintln. Standalone logging-backend flags log-0.4 / tracing-0.1 (one of each, shared across all integrations)#[template(...)] Attribute Options| Key | Example | Purpose |
|---|---|---|
path | path = "hello.html" | File in templates dir. Extension drives escaping. |
source | source = "Hi {{ name }}" | Inline template; requires ext. |
ext | ext = "html" | Content-type hint; "jinja" / "jinja2" aliases for editor syntax highlighting. |
escape | escape = "none" or "html" | Override auto-escape. |
whitespace | whitespace = "suppress" | Per-template WS mode ("preserve" / "suppress" / "minimize"); overrides askama.toml. |
syntax | syntax = "foo" | Use a custom syntax defined in askama.toml. |
config | config = "config.toml" | Override config path (default askama.toml). |
print | print = "code" / "ast" / "all" / "none" | Compile-time debug: prints generated code to stdout. |
block | block = "hello" | Render just one block. Fields outside that block aren't required. Good for HTMX fragments. |
blocks | blocks = ["title", "content"] | Generates as_<block>() accessor methods on the struct, one per block. |
in_doc | in_doc = true | Source is in the struct's doc comment (inside an ```askama fenced block). Requires ext. |
askama | askama = $crate::__askama | Override the askama crate path. Needed when re-exporting from a macro-defining crate. |
Cannot combine path and source.
path and source must be string literals known at compile time, the same constraint {% include %} paths have (§10). The template can't be chosen at runtime from a variable or computed string.
{{ MyStruct { field1: 1, field2: "x" }.to_string() }}{{ (1, 2) }}, array: {{ [1, 2, 3] }}, array-repeat (0.15): {{ [0; 4] }}.to_string() (0.15): {{ Point { x: 1, y: 2 } }}{{ name }}: field on template struct{{ user.name }}: dotted path (fields or methods){{ crate::MAX_USERS }}: use constants from your crate| -> bitor& -> bitand^ -> xor{% if my_bitset bitand 1 != 0 %}set!{% endif %}~: {{ a ~ b ~ c }} is shorthand for {{ a }}{{ b }}{{ c }}. Must have spaces around it to disambiguate from whitespace control.as cast: {{ x as i64 }}. Only primitive types. Automatically derefs &&&bool etc.{{ method() }} -> self.method() (method on the template struct){{ self::function() }} -> free function in the current module{{ super::b::f() }} -> function in another module{{ (closure)(12) }} -> calling a closure stored in a field; parens required{{ some_macro!(field) }} -> Rust macro. Askama won't infer field references inside macro args. Pass them explicitly, or the macro sees the literal token instead.{% if a %}…{% else if b %}…{% else %}…{% endif %}
{% for user in users %}<li>{{ user.name }}</li>{% endfor %}
{% for user in users if user.active %}{% for u in users %}...{% else %}No one here.{% endfor %}
loop.index (1-based)loop.index0 (0-based)loop.firstloop.lastloop.cycle([...]) (method call, array-literal arg): {{ loop.cycle(["a", "b"]) }} cycles per iterationloop.length, loop.revindex, loop.changed, loop.previtem, loop.nextitem. If you need these, compute in Rust or use modulo on loop.index.{% match item %}
{% when Some with ("foo") %}
Found literal foo
{% when Some with (val) %}
Found {{ val }}
{% when None %}
Nothing
{% endmatch %}
{% when Variant with (a, b) %}{% when Variant { field } %} or {% when Variant { field: val } %} to rename (with-less form since 0.11; 0.8 through 0.10 required the with keyword: {% when Variant with { field } %}){% when Self::Circle { radius } %}. #[derive(Template)] works on the enum itself, giving each variant its own #[template(...)].{% when [first, ..] %}{% when _ %} or {% else %}{% when 3 %}{% let name = user.name %} {# immutable binding #}
{% let mut it = xs.iter() %} {# mutable binding #}
{% set x = 4 %} {# alias for let (Jinja compat) #}
{% decl val %} {# declare WITHOUT value; let/set can't since 0.16 #}
{% if cond %}
{% let val = "a" %}
{% else %}
{% let val = "b" %}
{% endif %}
{{ val }}
decl (alias declare) is the only valueless declaration since 0.16. Bare {% let x %} / {% set x %} with no = now starts a let/set block instead (below).{% let heading %}{{ title }} on {{ site }}{% endlet %}
{{ heading }}
{% mut %} tag (0.16), not bare let: {% mut counter += i %}. Every Rust compound operator (+=, -=, *=, …) works; the target must be a mut binding.__askama, name one caller (reserved since 0.15), or use Rust keywords.Apply one or more filters to a whole block:
{% filter lower | capitalize %}
{{ t }} / HELLO / {{ u }}
{% endfilter %}
| Operator | Name | Effect |
|---|---|---|
- | suppress | Remove all whitespace on that side |
~ | minimize | Collapse to a single space/newline |
+ | preserve | Keep whitespace as-is (overrides config) |
Usage: put right after {%/{{ or right before %}/}}.
<div>
{%- if x %} {# strip before this tag #}
<p>{{ x }}</p>
{%- endif %}
</div>
{% for x in xs ~%} {# minimize trailing whitespace #}
{{ x }}
{%~ endfor %}
Global default comes from askama.toml's whitespace = "preserve" | "suppress" | "minimize". Inline operators override globals. When two inline controls point at the same span, Suppress always wins; between Minimize and Preserve, whichever operator sits on the tag AFTER the gap wins (positional, not a fixed priority). The book's flat Suppress > Minimize > Preserve table is wrong vs 0.16.0 for the Minimize/Preserve case.
Whitespace-control operators (-/+/~) on {% extends %} are accepted but a no-op, not a compile error. The child's top-level content is dropped anyway, so a trim has nothing to act on. (The book still describes this as "rejected"; that's stale vs 0.16.0.)
Nested block comments are supported:
{# outer {# nested #} still in outer #}
base.html:
<title>{% block title %}{{ title }} · Site{% endblock %}</title>
<main>{% block content %}<p>default</p>{% endblock %}</main>
page.html:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
{{ super() }} {# inject the parent block's contents #}
<h1>Hello!</h1>
{% endblock %}
Rules:
if/else/for: the book forbids this, but 0.16.0 compiles it and inheritance overrides correctly (verified empirically). Top-level or block-nested stays the safe form.{% block %} names in the same template are a hard compile error since 0.16, previously only a warning (see §15).block / blocks (see §4): block = "content" renders one block (fields outside it not required, good for HTMX partials); blocks = ["title", "content"] generates as_<block>() accessor methods on the struct itself, e.g. page.as_title().render() / page.as_content().render() (no new struct types).
{% include %}{% for item in items %}
{% include "item.html" %}
{% endfor %}
Define with {% macro name(args) %}...{% endmacro %}, call with {% call name(args) %}:
{% macro heading(arg) %}
<h1>{{ arg }}</h1>
{% endmacro %}
{% call heading("Title") %}
{{ heading("Title") }} (named args allowed too). Only works for macros that don't require a caller body.{% endmacro heading %}{% macro h(arg, bold) %}<h1>{{arg}}<b>{{bold}}</b></h1>{% endmacro %}
{% call h(bold="x", arg="y") %}
{%- import "macros.html" as m -%}
{% call m::heading("hi") %}
{% macro card() %}
<div class="card">{{ caller() }}</div>
{% endmacro %}
{% call card() %}
<p>Body passed into the macro.</p>
{% endcall %}
caller() inside the macro renders the block passed to {% call %}...{% endcall %}. To pass arguments, the call-block declares them up front: {% call(user) dump_users(list) %}...{% endcall %} makes the macro's caller(user) render the body once per invocation. Guard an optional body with {% if caller is defined %}. caller is a reserved variable name since 0.15.
Embed a Template-derived type as a field; it auto-renders via its Display impl:
#[derive(Template)]
#[template(source = "Section 1: {{ s1 }}", ext = "txt")]
struct Outer { s1: Inner }
#[derive(Template)]
#[template(source = "A={{ a }}", ext = "txt")]
struct Inner { a: String }
Self-include does NOT work. Do recursion with an explicit .render() call:
#[derive(Template)]
#[template(source = r#"
{{ name }} {
{% for child in children %}
{{ child.render()? }}
{% endfor %}
}
"#, ext = "txt", escape = "none")]
struct Item<'a> {
name: &'a str,
children: &'a [Item<'a>],
}
(Prefer a custom iterator + plain loop when possible.)
askama.tomlPlace at crate root (next to Cargo.toml). All sections are optional.
[general]
dirs = ["templates"] # default; globs OK (0.16): ["templates/*"], ["templates/**"]
whitespace = "preserve" # or "suppress" | "minimize"
# Custom delimiters (e.g. to avoid collisions with LaTeX or Vue)
[[syntax]]
name = "vue"
block_start = "[%"
block_end = "%]"
expr_start = "[["
expr_end = "]]"
comment_start = "[#"
comment_end = "#]"
# Custom escaper: apply askama::filters::Text (no escaping) to .js files
[[escaper]]
path = "askama::filters::Text"
extensions = ["js"]
Use a custom syntax per-template with #[template(path = "...", syntax = "vue")].
Default escaping extensions:
askama, html, htm, j2, jinja, jinja2, rinja, svg, xmlmd, yml, none, txt (and the empty extension)Escape modes override via #[template(escape = "html")] or escape = "none".
Askama escapes <, >, &, ", ' per OWASP recommendations when the extension implies HTML.
Per-expression bypass:
{{ value | safe }}: don't escape this value{{ value | escape }} or {{ value | e }}: force escape in an unescaped contextAll filters use value | filter_name(args). Named arguments are supported: {{ count | pluralize(plural = "gies") }}.
| Filter | Purpose |
|---|---|
capitalize | First char upper, rest lower |
center | Center in a field of given width |
escape / e | Force HTML escape |
filesizeformat | Bytes -> "1.4 KB" |
fmt(fmtstr) | Apply Rust format string |
format(fmtstr, ...) | Like format!() |
indent(n) / indent(prefix) | Indent each line |
join(sep) | Join iterable into string |
linebreaks | Plain text -> <p>/<br> |
linebreaksbr | Newlines -> <br> |
lower / lowercase | Lowercase |
paragraphbreaks | Only \n\n -> <p> |
pluralize(singular="", plural="s") | Suffix based on ±1 |
reject / reject_with | Filter iterator |
safe | Mark as HTML-safe |
title / titlecase | Title Case |
trim | Strip whitespace |
truncate(n) | Truncate with "…" |
unique | Dedup iterator (requires std) |
upper / uppercase | Uppercase |
wordcount | Count words |
assigned_or(fallback) | Fallback if the value equals its type default (0.15) |
defined_or(fallback) | Fallback if the identifier is undefined; LHS must be an identifier (0.15) |
default(val[, bool]) | Jinja-compat: acts as defined_or, or as assigned_or when 2nd arg is true. Prefer the two above (0.15) |
| Filter | Feature flag |
|---|---|
json (indent arg selects pretty: `{{ value | json(2) }}`) |
urlencode (does NOT encode /) | urlencode |
urlencode_strict (encodes /) | urlencode |
Add to Cargo.toml: askama = { version = "0.16", features = ["serde_json", "urlencode"] }
humansize: always available now, no flag neededmarkdown: removed, use comrak directlyserde-yaml: removed, use yaml-rust2 directlyserde-json renamed to serde_jsonSince 0.15 a custom filter is a plain fn annotated with #[askama::filter_fn]. Put it in a module named filters in scope of the #[derive(Template)] struct, or call it via an explicit path ({{ x | mymod::myfilter }}).
Mandatory signature: first arg is the piped-in value (any type; impl Display or a generic bound is typical, accepting owned and borrowed), second is &dyn askama::Values (askama's runtime-values env). Returns askama::Result<T>. In a chain only the last filter's T must be Display; earlier ones may return any T.
use askama::Template;
#[derive(Template)]
#[template(source = "{{ s | shout }}", ext = "txt")]
struct Msg<'a> { s: &'a str }
mod filters {
#[askama::filter_fn]
pub fn shout<T: std::fmt::Display>(
s: T,
_env: &dyn askama::Values,
) -> askama::Result<String> {
Ok(s.to_string().to_uppercase() + "!")
}
}
Extra arguments come after the two mandatory ones:
fn repeat(s: impl Display, _: &dyn Values, n: usize) invoked as {{ s | repeat(4) }}.#[optional(...)] attribute:
#[askama::filter_fn]
pub fn f(
value: impl Display,
_env: &dyn askama::Values,
#[optional(None)] a: Option<&str>, // omitted -> None
#[optional("hi")] b: &str, // omitted -> "hi"
) -> askama::Result<String> { /* ... */ }
{{ x | f(b = "yo") }}. Lifetimes on the fn are allowed since 0.15.1, where bounds since 0.15.2.Notes:
{{ x | filters::myfilter }} if you shadow one.#[askama::filter_fn]; it's what unlocked the named/optional args the old form couldn't express.render() / render_into() are synchronous only. No async support; an async fn doesn't satisfy the #[askama::filter_fn] signature.askama_axum / askama_actix / etc. are gone: use #[derive(WebTemplate)] (askama_web) or manual .render() + Html(...).|, &, ^ are not bitwise in templates: use bitor, bitand, xor.{{ child.render()? }}.if/for: book forbids it, 0.16.0 compiles it (§9); top-level/block-nested is safe.{% extends %} is a silent no-op, not a compile error (§7).__askama, the reserved caller, or Rust keywords are banned.self infinite-loops via Display; don't write {{ self }}.{{ my_macro!(field) }} sees field, not self.field..render() / .render_into() over .to_string() (2-3x slower).~ is concat (with spaces) vs. whitespace-minimize (no spaces): keep spaces around concat..html escaped, .txt not); inline source needs explicit ext / escape.rinja / rinja_axum / rinja_derive -> askama / askama_web (see intro).#[askama::filter_fn] since 0.15; a bare fn in mod filters no longer compiles (§14).{% let x %} / {% set x %} now opens a block (use {% decl x %}); compound assignment moved to {% mut %}; duplicate block names are now a hard compile error (was a warning).path / source take string literals only (§4), same as {% include %} (§10). No picking the template from a variable or computed string at runtime.render() / render_into() are sync only (§14). No async filter or async render path exists.file:line:column location. Lifetime / ownership errors in generated code usually mean a field got moved. Take a reference, or use & in the template.Expressions: {{ expr }} Tags: {% stmt %} Comments: {# ... #}
WS control: {%- strip -%} {%~ min ~%} {%+ keep +%}
Inheritance: {% extends "base.html" %} {% block x %}...{% endblock %} {{ super() }}
Loops: {% for x in xs if cond %}...{% else %}...{% endfor %} loop.{index,index0,first,last,cycle([…])}
Conditionals: {% if %}...{% else if %}...{% else %}...{% endif %}
Match: {% match v %}{% when Some with (x) %}...{% when None %}...{% endmatch %}
Vars: {% let x = y %} {% let mut it = … %} {% set x = 1 %} {% decl x %} {% mut x += 1 %}
Let block: {% let s %}…rendered…{% endlet %} (captures block output into `s`)
Macros: {% macro m(a) %}...{% endmacro %} {% call m(a) %} expr-call: {{ m(a) }}
{% call(x) m(a) %}body{% endcall %} inside macro: {{ caller() }} / {{ caller(x) }}
Imports: {% import "m.html" as ns %} {% call ns::m(a) %}
Include: {% include "part.html" %}
Filters: {{ x | lower | trim }} block: {% filter lower %}...{% endfilter %}
Concat: {{ a ~ b ~ c }} (spaces required)
Cast: {{ x as i64 }} (primitives only)
Bitwise: a bitor b, a bitand b, a xor b (NOT |, &, ^)
Crate consts: {{ crate::MY_CONST }}
Struct ctor: {{ Foo { a: 1 }.to_string() }}
Escape: {{ x | safe }} {{ x | e }} or #[template(escape = "none"|"html")]