agentsclimarketplace

Askama

Skill uwuclxdy/agenticat/skills/askama

Some of my Agents & Skills, compatible with most AI coding tools

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.

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 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")]

Keep looking

Skills are one crate of 328,083. 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.