with one click
maud
Maud Rust `html!` macro reference (0.27).
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
Maud Rust `html!` macro reference (0.27).
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 | maud |
| description | Maud Rust `html!` macro reference (0.27). |
| metadata | {"author":"uwuclxdy","version":"1.5"} |
Captured 2026-07-10 (maud 0.27.0). To update: re-verify against maud.lambda.xyz + docs.rs/maud, then diff for changes.
Maud is a compile-time HTML macro (html!); markup lives inline in Rust, type-checked at compile time. This skill targets maud 0.27.x (0.27.0, released 2025-02-02).
Maud runs on stable Rust and nightly (better error messages on nightly). Stable-Rust-capable since 0.22.1 (2020-11-02); earlier it needed nightly-only compiler features. MSRV is not documented.
[dependencies]
maud = "0.27"
html! { ... } returns a Markup value. Markup is a type alias for PreEscaped<String>, a wrapper around an already-escaped HTML string.
| Operation | Result | Notes |
|---|---|---|
html! { ... } | Markup | The macro expression's value. |
markup.into_string() | String | Consumes the Markup, hands back the built string. |
(DOCTYPE) inside html! | emits <!DOCTYPE html> | maud::DOCTYPE is a constant equal to the literal string <!DOCTYPE html>. |
use maud::{html, DOCTYPE};
let page = html! {
(DOCTYPE)
html {
head { title { "My site" } }
body { p { "Hello" } }
}
};
Rendering a Display value: maud::display(x) is a free function that renders any value through its Display impl inside a template. It replaced the old blanket Render for Display impl removed in 0.24.0.
use maud::{html, display};
let n = 42;
html! { p { (display(n)) } };
Note: Markup/PreEscaped has no Display impl (checked against docs.rs 0.27; it implements Render, not Display). Convert to String with .into_string() when you need the raw string. There is no html_to! / write-into-buffer macro (html_debug! was removed in 0.25.0). To write into an existing buffer, use the Render trait's render_to (see §9).
Turn on the matching feature flag from the table below and Markup implements the corresponding trait. A handler can then just return Markup directly.
| Framework | Feature flag | Trait Markup implements | Dep pins |
|---|---|---|---|
| default | — | — | nothing enabled |
| Actix-web | actix-web | actix_web::Responder | actix-web-dep + futures-util |
| Axum | axum | IntoResponse | axum-core ^0.5 + http ^1 |
| Rocket | rocket | Rocket's Responder | rocket ^0.5 |
| Warp | warp | warp::Reply | warp ^0.3.6 |
| Poem | poem | poem::IntoResponse | poem ^3 |
| Tide | tide | From<PreEscaped<String>> for tide::Response | tide ^0.16.0 |
| Submillisecond | submillisecond (new in 0.27.0) | IntoResponse | submillisecond ^0.4.1 |
| Rouille | manual, see below | none | — |
Dependency version pins came from the docs.rs feature graph, single source. Verify against Cargo.toml if exact ranges matter.
maud = { version = "0.27", features = ["axum"] }
Rocket:
#[get("/<name>")]
fn hello(name: &str) -> Markup {
html! { h1 { "Hello, " (name) "!" } }
}
Actix-web and Tide handlers wrap the return in a Result: Actix async fn index() -> AwResult<Markup> { Ok(html! { ... }) }; Tide returns Ok(html! { ... }) from a closure inferred as tide::Result<impl Into<Response>> (its feature adds From<PreEscaped<String>>, not IntoResponse, so a bare -> Markup handler won't compile). Axum, Poem, Submillisecond return Markup directly from a sync or async handler. The trait impl converts it to the response type each one expects.
Call .into_string() and wrap the string yourself.
// Rouille has no trait impl:
Response::html(html! { h1 { "Hello, " (name) "!" } })
Elements with content use braces; text and nested elements go inside.
Void elements end with ; and take no body. Maud emits HTML5 syntax (<br>), not XHTML (<br />).
html! {
link rel="stylesheet" href="poetry.css";
p {
"Rock, you are a rock."
br;
"Gray, you are gray,"
}
}
Elements and attributes with hyphens are supported (custom elements, data-*, ARIA).
"Type-checked at compile time" means Rust splice-expression types only. Maud does not validate element or attribute names against the HTML5 spec. It does not check content-model nesting either (a <div> inside a <p> compiles fine). Any valid Rust ident compiles as a tag, typos included.
Plain double-quoted Rust string literals are bare expressions inside html!. Raw string literals handle long or special-character text.
html! {
pre {
r#"
Rocks, these are my rocks.
Smooth and round,
Asleep in the ground.
"#
}
}
Value attribute: name="value".
html! {
a href="about:blank" { "Apple Bloom" }
li class="lower-middle" { "Sweetie Belle" }
}
Boolean / empty attribute: bare name, no =.
html! {
input type="checkbox" name="cupcakes" checked;
label for="cupcakes" { "Do you like cupcakes?" }
}
#id / .class Shorthandhtml! {
input #cannon .big.scary.bright-red type="button" value="Launch";
div."col-sm-2" { "Bootstrap column!" }
}
.big.scary.bright-red.."col-sm-2".# must be preceded by a space (input #pinkie;). Older editions allowed input#pinkie;.divOmitting the tag name when a #id / .class shorthand is present defaults the element to div.
html! {
#main {
"Main content!"
.tip { "Storing food in a refrigerator can make it 20% cooler." }
}
}
[condition]A bracketed bool expression toggles a boolean attribute or a class on or off.
let allow_editing = true;
html! {
p contenteditable[allow_editing] { "Edit me." }
}
let cuteness = 95;
html! {
p.cute[cuteness > 50] { "Squee!" }
}
attr=[Option<T>]With =, a bracketed Option sets the attribute's value only when Some. None omits the attribute entirely. This differs from bare [cond], which toggles presence.
html! {
p title=[Some("Good password")] { "Correct horse" }
@let value = Some(42);
input value=[value];
@let title: Option<&str> = None;
p title=[title] { "Battery staple" } // title omitted
}
Splice a computed id with #(...). Build a class from a literal plus splice inside .{ }.
let name = "rarity";
let severity = "critical";
html! {
aside #(name) {
p.{ "color-" (severity) } { "This is the worst! Possible! Thing!" }
}
}
(expr) inserts a runtime value into markup. HTML special characters in the value are escaped by default.
let best_pony = "Pinkie Pie";
let numbers = [1, 2, 3, 4];
html! {
p { "Hi, " (best_pony) "!" }
p {
"I have " (numbers.len()) " numbers, "
"and the first one is " (numbers[0])
}
}
Splice any type that has a maud::Render impl. Most primitives (str, i32, ...) already do.
Block-expression splice for arbitrary Rust logic:
html! {
p {
({
let f: Foo = something_convertible_to_foo()?;
f.time().format("%H%Mh")
})
}
}
Single value with =:
let secret_message = "Surprise!";
html! {
p title=(secret_message) { "Nothing to see here." }
}
Concatenating literal plus splice needs { } wrapping:
const GITHUB: &'static str = "https://github.com";
html! {
a href={ (GITHUB) "/lambda-fairy/maud" } { "Fork me on GitHub" }
}
Every control keyword takes an @ prefix. Bare if / for / match / let are not recognized as control flow inside html!. Braces are mandatory on every branch.
@if / @else if / @else#[derive(PartialEq)]
enum Princess { Celestia, Luna, Cadance, TwilightSparkle }
let user = Princess::Celestia;
html! {
@if user == Princess::Luna {
h1 { "Super secret woona to-do list" }
ul { li { "Evil laugh" } }
} @else if user == Princess::Celestia {
p { "Sister, please stop reading my diary." }
} @else {
p { "Nothing to see here; move along." }
}
}
@if let works (the condition is a plain Rust expression, so let PAT = EXPR parses natively):
let user = Some("Pinkie Pie");
html! {
p {
"Hello, "
@if let Some(name) = user { (name) } @else { "stranger" }
"!"
}
}
@forlet names = ["Applejack", "Rarity", "Fluttershy"];
html! {
ol {
@for name in &names {
li { (name) }
}
}
}
@whilePresent in the parser AST (WhileExpr), absent from the published book. No documented example exists. Structure matches @if, so @while cond { ... } and by analogy @while let PAT = EXPR { ... } are expected to work. Treat @while let as unverified until you compile it.
@matchhtml! {
@match user {
Princess::Luna => h1 { "Woona's here" },
Princess::Celestia | Princess::Cadance => p { "A princess." },
_ => p { "Nothing to see here; move along." }
}
}
Reuses the Princess enum and user binding from @if above. Match arms accept full Rust patterns per the parser source: or-patterns (A | B, shown above), bindings, ranges, destructuring, guards (pat if cond =>). The book shows only basic arms, so verify guard / or-pattern arms by compiling.
@letDeclares a variable inside html!, most useful inside loops. Accepts any Rust let pattern including type ascription.
let names = ["Applejack", "Rarity", "Fluttershy"];
html! {
@for name in &names {
@let first_letter = name.chars().next().unwrap();
p {
"The first letter of " b { (name) } " is " b { (first_letter) } "."
}
}
}
Splices and text are escaped by default. Maud escapes exactly four characters: &, <, >, ". Single quotes pass through unescaped.
Bypass escaping with maud::PreEscaped, which renders its inner value verbatim.
use maud::PreEscaped;
html! {
"<script>alert(\"XSS\")</script>" // <script>...
(PreEscaped("<script>alert(\"XSS\")</script>")) // <script>...
}
Only wrap content you have already sanitized. PreEscaped on untrusted input is an XSS hole. The AsRef<str> bound restriction on PreEscaped was removed in 0.26.0, so it wraps a wider range of inner types now.
Render TraitWrite a maud::Render impl to teach maud how to splice your own type. The trait has two methods, both with default impls that call each other:
pub trait Render {
fn render(&self) -> Markup {
let mut buffer = String::new();
self.render_to(&mut buffer);
PreEscaped(buffer)
}
fn render_to(&self, buffer: &mut String) {
buffer.push_str(&self.render().into_string());
}
}
Override at least one. Overriding neither causes infinite recursion (each default calls the other). Override render for the simple case. Override render_to when you want to write straight into the output buffer, for example to append multiple pieces without an intermediate Markup.
A render_to override is responsible for its own escaping. Raw writes into buffer are not escaped for you. Need that inside one? Reach for maud::Escaper, it escapes as it writes.
render/render_to are sync-only. There is no async Render trait or streaming variant. Resolve async data (a DB or HTTP call) to a value before entering html! and splice the resolved value.
A partial or component is a plain function returning Markup. Compose by splicing the call.
use maud::{html, Markup, DOCTYPE};
fn header(title: &str) -> Markup {
html! {
head { title { (title) } }
}
}
fn page(title: &str, body: Markup) -> Markup {
html! {
(DOCTYPE)
html {
(header(title))
body { (body) }
}
}
}
// usage
let markup = page("Home", html! { h1 { "Hello" } });
Splicing a Markup value never re-escapes it, so nesting components is safe. Maud does not currently export a RenderOnce trait (it had one from 0.8.0 through 0.15.0, removed 2017-01-26); don't confuse the name with horrorshow's still-existing RenderOnce trait.
Each is flagged inline at its section; §11 consolidates. Index:
; not self-close; control flow needs @ (every branch braced); §4, §7[cond] (toggle) vs attr=[Option<T>] (value only when Some); attr concat needs { }; §5, §6PreEscaped is an XSS hole on untrusted input; only & < > " escaped (single quote not); §6, §8# spacing (input #pinkie;); §5Render with neither method overridden recurses forever; a render_to override does its own escaping (maud::Escaper); §9render/render_to are sync-only; no async Render trait, no streaming variant; resolve async values before html! (§9)Markup has no Display impl (.into_string() / maud::display(x)); no template files, no html_to!; §2Value: html! { ... } -> Markup Get String: .into_string()
Doctype: (DOCTYPE) -> <!DOCTYPE html>
Element: p { "text" nested { } }
Void element: br; link rel="x" href="y"; (trailing ; , renders <br>)
Text: "literal" r#"raw literal"#
Attr: name="value"
Bool attr: checked (bare name, no =)
Shorthand: input #id .class1.class2; .class -> implies <div> if no tag
Toggle: p contenteditable[cond] { } p.cute[n > 50] { }
Optional attr: input value=[Some(42)]; title=[opt] (None omits attr)
Splice: (expr) escapes by default
Attr splice: title=(x) href={ (a) "/" (b) }
Dyn name: #(id_expr) .{ "color-" (sev) }
Block splice: ({ let x = f()?; x })
If: @if c { } @else if d { } @else { }
If let: @if let Some(x) = opt { (x) } @else { }
For: @for x in &xs { li { (x) } }
Match: @match v { A => { }, _ => p { } } (guards / or-pats via Rust patterns)
While: @while cond { } (@while let unverified)
Let: @let x = expr; @let y: Option<&str> = None;
Raw HTML: (PreEscaped(safe_html)) disables escaping
Display value: (display(x))
Component: fn c(...) -> Markup { html! { } } splice with (c(...))
Render trait: fn render(&self) -> Markup fn render_to(&self, buf: &mut String) (both have default impls, both sync-only)
Escapes: & < > " (single quote NOT escaped)
Book: https://maud.lambda.xyz API docs: https://docs.rs/maud