Askama
Some of my Agents & Skills, compatible with most AI coding tools
npx -y skills add uwuclxdy/agenticat --skill askamaAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Askama Rust templating reference (0.16).
SKILL.md
24.3 KB, as published. Nobody here has run it
Askama (Rust Templating)
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.
1. Current State & Setup
Cargo.toml
[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.
Minimal Example
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).
2. Rendering API: Use the Right Method
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.
3. Web Framework Integration
Two paths. Pick one.
Path A: Manual (Simplest, No Extra Crate)
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.
Path B: askama_web with WebTemplate
One 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) andeprintln. Standalone logging-backend flagslog-0.4/tracing-0.1(one of each, shared across all integrations)
4. #[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.
5. Expressions
Literals and Types
- Struct construction:
{{ MyStruct { field1: 1, field2: "x" }.to_string() }} - Tuple:
{{ (1, 2) }}, array:{{ [1, 2, 3] }}, array-repeat (0.15):{{ [0; 4] }} - Struct expressions are values, not just via
.to_string()(0.15):{{ Point { x: 1, y: 2 } }}
Variable Access
{{ name }}: field on template struct{{ user.name }}: dotted path (fields or methods){{ crate::MAX_USERS }}: use constants from your crate- Reading follows Rust borrow rules. Methods can be called; beware self-recursion.
Operators (Mostly like Rust)
- Bitwise are RENAMED (to avoid filter-pipe conflict):
|->bitor&->bitand^->xor- Precedence unchanged.
{% if my_bitset bitand 1 != 0 %}set!{% endif %}
- Concat
~:{{ a ~ b ~ c }}is shorthand for{{ a }}{{ b }}{{ c }}. Must have spaces around it to disambiguate from whitespace control. ascast:{{ x as i64 }}. Only primitive types. Automatically derefs&&&booletc.
Function and Method Calls
{{ 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.
6. Control Flow
if / else if / else
{% if a %}…{% else if b %}…{% else %}…{% endif %}
for
{% for user in users %}<li>{{ user.name }}</li>{% endfor %}
- Filter clause:
{% for user in users if user.active %} - Else clause (runs if iter was empty or all filtered out):
{% for u in users %}...{% else %}No one here.{% endfor %} - Loop variables (the complete list; Askama exposes fewer of them than Jinja does):
loop.index(1-based)loop.index0(0-based)loop.firstloop.lastloop.cycle([...])(method call, array-literal arg):{{ loop.cycle(["a", "b"]) }}cycles per iteration
- No
loop.length,loop.revindex,loop.changed,loop.previtem,loop.nextitem. If you need these, compute in Rust or use modulo onloop.index.
match / when (for Enums, Options, Results)
{% match item %}
{% when Some with ("foo") %}
Found literal foo
{% when Some with (val) %}
Found {{ val }}
{% when None %}
Nothing
{% endmatch %}
- Tuple variants:
{% when Variant with (a, b) %} - Struct variants:
{% when Variant { field } %}or{% when Variant { field: val } %}to rename (with-less form since 0.11; 0.8 through 0.10 required thewithkeyword:{% when Variant with { field } %}) - Enum path variants:
{% when Self::Circle { radius } %}.#[derive(Template)]works on the enum itself, giving each variant its own#[template(...)]. - Slice patterns with rest:
{% when [first, ..] %} - Wildcard:
{% when _ %}or{% else %} - Literal patterns:
{% when 3 %}
Assignments and Variable Declarations
{% 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 }}
- Shadowing is allowed, same as Rust.
decl(aliasdeclare) is the only valueless declaration since 0.16. Bare{% let x %}/{% set x %}with no=now starts a let/set block instead (below).- let/set blocks (0.16): capture a rendered block into a string variable:
{% let heading %}{{ title }} on {{ site }}{% endlet %} {{ heading }} - Compound assignment uses the
{% mut %}tag (0.16), not barelet:{% mut counter += i %}. Every Rust compound operator (+=,-=,*=, …) works; the target must be amutbinding. - Do not prefix variables with
__askama, name onecaller(reserved since 0.15), or use Rust keywords.
Filter Blocks
Apply one or more filters to a whole block:
{% filter lower | capitalize %}
{{ t }} / HELLO / {{ u }}
{% endfilter %}
7. Whitespace Control: Three Operators, Not One
| 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.)
8. Comments
Nested block comments are supported:
{# outer {# nested #} still in outer #}
9. Inheritance (extends / block / super)
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:
- Multi-layer inheritance is supported.
- Blocks inside
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. - The extending template's top-level content outside blocks is ignored.
- A base template must define at least one block for inheritance to work.
- Askama looks for the extended template relative to the extending one first, then relative to the config'd template dirs.
- Duplicate
{% block %}names in the same template are a hard compile error since 0.16, previously only a warning (see §15).
Single-Block & Per-Block Sub-Templates
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).
10. Includes, Macros, and Composition
{% include %}
{% for item in items %}
{% include "item.html" %}
{% endfor %}
- Path must be a string literal (known at compile time).
- The included template has full access to the caller's scope, including loop locals.
- Lookup: relative to including template first, then template dirs.
Macros
Define with {% macro name(args) %}...{% endmacro %}, call with {% call name(args) %}:
{% macro heading(arg) %}
<h1>{{ arg }}</h1>
{% endmacro %}
{% call heading("Title") %}
- Expression-call syntax (0.15): invoke a macro like a function,
{{ heading("Title") }}(named args allowed too). Only works for macros that don't require acallerbody. - Macro args can carry type annotations and default-value generics (0.15/0.16).
- Optional named endmacro:
{% endmacro heading %} - Named arguments (must come AFTER positional):
{% macro h(arg, bold) %}<h1>{{arg}}<b>{{bold}}</b></h1>{% endmacro %} {% call h(bold="x", arg="y") %}
Importing Macros from Another File
{%- import "macros.html" as m -%}
{% call m::heading("hi") %}
Call-Block Syntax (Macros with Inner Content)
{% 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.
Render-in-Place (Nested Template Structs)
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 }
Recursive Rendering
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.)
11. Configuration: askama.toml
Place 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:
- HTML escaper:
askama,html,htm,j2,jinja,jinja2,rinja,svg,xml - Text (no escape):
md,yml,none,txt(and the empty extension)
Escape modes override via #[template(escape = "html")] or escape = "none".
12. Auto-Escaping
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 context
13. Built-in Filters
All filters use value | filter_name(args). Named arguments are supported: {{ count | pluralize(plural = "gies") }}.
Always Available
| 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) |
Feature-Gated
| 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"] }
Filters Removed Since Older Askama
humansize: always available now, no flag neededmarkdown: removed, usecomrakdirectlyserde-yaml: removed, useyaml-rust2directlyserde-jsonrenamed toserde_json
14. Custom Filters
Since 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:
- Required args are just more params:
fn repeat(s: impl Display, _: &dyn Values, n: usize)invoked as{{ s | repeat(4) }}. - Optional args (must follow every required one) declare their default with the
#[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> { /* ... */ } - Named-argument invocation works (named must follow positional):
{{ x | f(b = "yo") }}. Lifetimes on the fn are allowed since 0.15.1,wherebounds since 0.15.2.
Notes:
- Built-in filter names take precedence over custom ones; be explicit with
{{ x | filters::myfilter }}if you shadow one. - Migrating from ≤0.14: the old bare-fn form (no attribute) no longer compiles. Add
#[askama::filter_fn]; it's what unlocked the named/optional args the old form couldn't express. - Filter fns and
render()/render_into()are synchronous only. No async support; anasync fndoesn't satisfy the#[askama::filter_fn]signature.
15. Gotchas and Footguns
askama_axum/askama_actix/ etc. are gone: use#[derive(WebTemplate)](askama_web) or manual.render()+Html(...).|,&,^are not bitwise in templates: usebitor,bitand,xor.- Self-include doesn't work: recurse via an embedded child struct (Display) or explicit
{{ child.render()? }}. - Blocks inside
if/for: book forbids it, 0.16.0 compiles it (§9); top-level/block-nested is safe. - Whitespace control on
{% extends %}is a silent no-op, not a compile error (§7). - Variable names starting with
__askama, the reservedcaller, or Rust keywords are banned. - A method returning
selfinfinite-loops via Display; don't write{{ self }}. - Rust macro calls don't infer field names (§5):
{{ my_macro!(field) }}seesfield, notself.field. - Prefer
.render()/.render_into()over.to_string()(2-3x slower). ~is concat (with spaces) vs. whitespace-minimize (no spaces): keep spaces around concat.- Auto-escape depends on extension (
.htmlescaped,.txtnot); inlinesourceneeds explicitext/escape. - Rinja is dead:
rinja/rinja_axum/rinja_derive->askama/askama_web(see intro). - Custom filters need
#[askama::filter_fn]since 0.15; a barefninmod filtersno longer compiles (§14). - 0.16 upgrade breakage: valueless
{% 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/sourcetake string literals only (§4), same as{% include %}(§10). No picking the template from a variable or computed string at runtime.- Filter fns and
render()/render_into()are sync only (§14). No async filter or async render path exists.
16. Debugging Tips
- Playground: https://askama.rs has a web playground that shows the generated code for any template.
- Compile errors: Askama's error messages point at the template's
file:line:columnlocation. Lifetime / ownership errors in generated code usually mean a field got moved. Take a reference, or use&in the template. - Book: https://askama.rs for the canonical syntax reference.
- docs.rs: https://docs.rs/askama for API docs and the full filter list.
17. Quick Reference Card
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")]