agentsclimarketplace

Askama

Skill uwuclxdy/agenticat/skills/askama

Askama Rust templating reference (0.16).From its SKILL.md

Install
npx -y skills add uwuclxdy/agenticat --skill askama

Assembled 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.

SKILL.md

24.3 KB, ~7.2k tokens by cl100k_base, 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 rinja in 2024, then the two projects re-unified as askama in early 2025. If the user mentions rinja or rinja_axum, those are deprecated. Migrate to askama / 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:

MethodOutputWhen to use
tmpl.render()Result<String, askama::Error>Default. Allocates a new String.
tmpl.render_into(&mut writer)writes into impl fmt::WriteReusing a buffer, nested rendering.
tmpl.write_into(&mut writer)writes into impl io::WriteWriting 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.4
  • actix-web-4
  • rocket-0.5
  • warp-0.4 / warp-0.3
  • poem-3
  • trillium-0.2
  • cot-0.3 / cot-0.4 / cot-0.5 / cot-0.6 / cot_core-0.6
  • derive (WebTemplate derive, on by default) and eprintln. Standalone logging-backend flags log-0.4 / tracing-0.1 (one of each, shared across all integrations)

4. #[template(...)] Attribute Options

KeyExamplePurpose
pathpath = "hello.html"File in templates dir. Extension drives escaping.
sourcesource = "Hi {{ name }}"Inline template; requires ext.
extext = "html"Content-type hint; "jinja" / "jinja2" aliases for editor syntax highlighting.
escapeescape = "none" or "html"Override auto-escape.
whitespacewhitespace = "suppress"Per-template WS mode ("preserve" / "suppress" / "minimize"); overrides askama.toml.
syntaxsyntax = "foo"Use a custom syntax defined in askama.toml.
configconfig = "config.toml"Override config path (default askama.toml).
printprint = "code" / "ast" / "all" / "none"Compile-time debug: prints generated code to stdout.
blockblock = "hello"Render just one block. Fields outside that block aren't required. Good for HTMX fragments.
blocksblocks = ["title", "content"]Generates as_<block>() accessor methods on the struct, one per block.
in_docin_doc = trueSource is in the struct's doc comment (inside an ```askama fenced block). Requires ext.
askamaaskama = $crate::__askamaOverride 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.
  • as cast: {{ x as i64 }}. Only primitive types. Automatically derefs &&&bool etc.

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.first
    • loop.last
    • loop.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 on loop.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 the with keyword: {% 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 (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/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 bare let: {% mut counter += i %}. Every Rust compound operator (+=, -=, *=, …) works; the target must be a mut binding.
  • Do not prefix variables with __askama, name one caller (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

OperatorNameEffect
-suppressRemove all whitespace on that side
~minimizeCollapse to a single space/newline
+preserveKeep 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 a caller body.
  • 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

FilterPurpose
capitalizeFirst char upper, rest lower
centerCenter in a field of given width
escape / eForce HTML escape
filesizeformatBytes -> "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
linebreaksPlain text -> <p>/<br>
linebreaksbrNewlines -> <br>
lower / lowercaseLowercase
paragraphbreaksOnly \n\n -> <p>
pluralize(singular="", plural="s")Suffix based on ±1
reject / reject_withFilter iterator
safeMark as HTML-safe
title / titlecaseTitle Case
trimStrip whitespace
truncate(n)Truncate with "…"
uniqueDedup iterator (requires std)
upper / uppercaseUppercase
wordcountCount 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

FilterFeature flag
json (indent arg selects pretty: `{{ valuejson(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 needed
  • markdown: removed, use comrak directly
  • serde-yaml: removed, use yaml-rust2 directly
  • serde-json renamed to serde_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, where bounds 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; an async fn doesn't satisfy the #[askama::filter_fn] signature.

15. Gotchas and Footguns

  1. askama_axum / askama_actix / etc. are gone: use #[derive(WebTemplate)] (askama_web) or manual .render() + Html(...).
  2. |, &, ^ are not bitwise in templates: use bitor, bitand, xor.
  3. Self-include doesn't work: recurse via an embedded child struct (Display) or explicit {{ child.render()? }}.
  4. Blocks inside if/for: book forbids it, 0.16.0 compiles it (§9); top-level/block-nested is safe.
  5. Whitespace control on {% extends %} is a silent no-op, not a compile error (§7).
  6. Variable names starting with __askama, the reserved caller, or Rust keywords are banned.
  7. A method returning self infinite-loops via Display; don't write {{ self }}.
  8. Rust macro calls don't infer field names (§5): {{ my_macro!(field) }} sees field, not self.field.
  9. Prefer .render() / .render_into() over .to_string() (2-3x slower).
  10. ~ is concat (with spaces) vs. whitespace-minimize (no spaces): keep spaces around concat.
  11. Auto-escape depends on extension (.html escaped, .txt not); inline source needs explicit ext / escape.
  12. Rinja is dead: rinja / rinja_axum / rinja_derive -> askama / askama_web (see intro).
  13. Custom filters need #[askama::filter_fn] since 0.15; a bare fn in mod filters no longer compiles (§14).
  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).
  15. path / source take string literals only (§4), same as {% include %} (§10). No picking the template from a variable or computed string at runtime.
  16. 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:column location. 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")]

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.