Skip to content
maud-syntax-fundamentals logo

Maud Syntax Fundamentals

maud-syntax-fundamentals

Compile-time HTML templating with Maud using the html! macro for type-safe markup generation. Covers syntax patterns including elements, attributes, classes, IDs, content splicing, toggles, control flow (if/match/for), and DOCTYPE. Use when generating HTML in Rust, creating templates, or building...

SKILL.md

Full skill instructions

Maud Syntax Fundamentals

Compile-time HTML templating for Rust with type safety and zero runtime overhead

Version Context

  • Maud: 0.27.0 (latest stable)
  • Rust Edition: 2021
  • Runtime: ~100 SLoC (minimal overhead)

When to Use This Skill

  • Generating HTML from Rust code
  • Building server-side rendered templates
  • Creating type-safe HTML components
  • Working with the html! macro
  • Replacing runtime templating engines (Tera, Handlebars)
  • Email template generation
  • RSS/​Atom feed generation

Core Philosophy

  1. Compile-time validation - Template errors caught by rustc, not at runtime
  2. Type safety - Leverages Rust's type system for HTML generation
  3. Zero overhead - Templates compile to optimized Rust code
  4. Auto-escaping - All text content is HTML-escaped by default
  5. No external dependencies - Everything links into your binary

Syntax Formula

ELEMENT[.CLASS][#ID][ ATTRIBUTES] { CONTENT }

Key Symbols:

  • {} = Container elements with content
  • ; = Void/​self-closing elements
  • () = Runtime value splicing (escaped)
  • [] = Conditional attributes/​classes (toggles)
  • @ = Control flow (if, match, for, while, let)

Elements

Container Elements

use maud::html;

html! {
    h1 { "Hello, world!" }
    p { "Paragraph text" }
    div { span { "Nested content" } }
    article {
        h2 { "Title" }
        p { "Content goes here" }
    }
}

Void Elements (Self-Closing)

html! {
    br;
    hr;
    input type="text" name="email";
    img src="photo.jpg" alt="Description";
    link rel="stylesheet" href="styles.css";
    meta charset="UTF-8";
}

Important: Terminate with ; - renders as <br> not <br />

DOCTYPE Declaration

use maud::{html, DOCTYPE};

html! {
    (DOCTYPE)
    html lang="en" {
        head {
            meta charset="UTF-8";
            title { "My Page" }
        }
        body {
            h1 { "Content" }
        }
    }
}

Classes and IDs

Classes (Chainable)

html! {
    div.container { }
    div.row.justify-center { }
    p.text-lg.font-bold { "Styled text" }

    // Quoted for special characters (hyphens, numbers)
    div."col-sm-2" { }
    div."bg-blue-500" { }
}

IDs (Space Required in Rust 2021+)

html! {
    div #main { }              // ✅ Rust 2021+
    section #content { }
    article #"post-123" { }    // Quoted for hyphens/​numbers
}

// ❌ Error in Rust 2021+
// div#main { }  // Missing space before #

Implicit Divs

html! {
    #header { }           // <div id="header">
    .container { }        // <div class="container">
    .card.shadow { }      // <div class="card shadow">
}

Attributes

Standard Attributes

html! {
    a href="https://example.com" title="Link" { "Click here" }
    input type="text" placeholder="Enter name" name="username";
    img src="photo.jpg" alt="Description" width="300";
}

Boolean Attributes

html! {
    input type="checkbox" checked;
    input type="text" disabled;
    option value="1" selected;
    script src="app.js" defer;
    video controls autoplay;
}

Data Attributes and ARIA

html! {
    article data-id="12345" data-category="tech" {
        h2 { "Article Title" }
    }

    button
        aria-label="Close dialog"
        aria-pressed="true"
        role="button"
    {
        "Close"
    }
}

Custom Elements

html! {
    tag-cloud { }
    custom-widget data-id="123" {
        "Custom content"
    }
}

Dynamic Content (Splices)

Basic Splicing

let name = "Alice";
let count = 42;

html! {
    p { "Hello, " (name) "!" }        // Hello, Alice!
    p { "Count: " (count) }            // Count: 42
}

Important: Values in () are automatically HTML-escaped

Expression Blocks

html! {
    p {
        "Result: "
        ({
            let x = 10;
            let y = 20;
            x + y
        })
    }
    // Outputs: Result: 30
}

Attribute Splicing

let url = "https://example.com";
let id = "post-123";

html! {
    a href=(url) { "Link" }
    div id=(id) { "Content" }
}

Multiple Values in Attributes

let base_url = "https://example.com";
let path = "/​page";

html! {
    a href={ (base_url) (path) } { "Link" }
    // Outputs: href="https://example.com/​page"
}

Class and ID Splicing

let class_name = "active";
let element_id = "main-section";

html! {
    // Class splicing (two equivalent forms)
    div.(class_name) { "Content" }
    div class=(class_name) { "Content" }

    // ID splicing
    section #(element_id) { "Content" }
}

Raw HTML (PreEscaped)

use maud::PreEscaped;

html! {
    div {
        // Auto-escaped (safe)
        p { "Hello <world>" }  // Outputs: Hello &lt;world&gt;

        // Raw HTML (dangerous - use with caution)
        (PreEscaped("<strong>Bold text</​strong>"))
    }
}

Security Warning: Only use PreEscaped with trusted, sanitized content. Never use with user input.

Toggles (Conditional Rendering)

Boolean Attributes

let is_checked = true;
let is_disabled = false;

html! {
    input type="checkbox" checked[is_checked];
    button disabled[is_disabled] { "Submit" }
}

Conditional Classes

let is_active = true;
let has_error = false;

html! {
    div.base-class[is_active].error[has_error] { }
    // Renders: <div class="base-class active">
}

Optional Attributes

let maybe_title: Option<String> = Some("Tooltip".to_string());
let no_title: Option<String> = None;

html! {
    button title=[maybe_title] { "Hover" }
    // Renders: <button title="Tooltip">

    button title=[no_title] { "No tooltip" }
    // Renders: <button> (attribute completely omitted)
}

Control Flow

If/​Else Conditionals

let logged_in = true;

html! {
    @if logged_in {
        p { "Welcome back!" }
    } @else {
        p { "Please log in" }
    }
}

If Let (Pattern Matching)

let user: Option<User> = Some(User { name: "Alice".to_string() });

html! {
    @if let Some(user) = &user {
        p { "Hello, " (user.name) }
    } @else {
        p { "Guest" }
    }
}

Match Expressions

enum Status { Active, Pending, Inactive }
let status = Status::Active;

html! {
    @match status {
        Status::Active => {
            span.badge.green { "Active" }
        }
        Status::Pending => {
            span.badge.yellow { "Pending" }
        }
        Status::Inactive => {
            span.badge.gray { "Inactive" }
        }
    }
}

For Loops

let items = vec!["Apple", "Banana", "Cherry"];

html! {
    ul {
        @for item in &items {
            li { (item) }
        }
    }
}

For Loop with Index

html! {
    ol {
        @for (i, item) in items.iter().enumerate() {
            li { (i + 1) ". " (item) }
        }
    }
}

While Loops

let mut count = 0;

html! {
    @while count < 5 {
        p { "Count: " (count) }
        ({ count += 1; })
    }
}

Let Bindings

html! {
    @let user_name = "Alice";
    @let greeting = format!("Hello, {}", user_name);

    h1 { (greeting) }

    @for i in 0..3 {
        @let doubled = i * 2;
        p { (i) " × 2 = " (doubled) }
    }
}

Common Patterns

Navigation Link with Active State

fn nav_link(href: &str, text: &str, is_current: bool) -> Markup {
    html! {
        a.nav-link[is_current] href=(href) { (text) }
    }
}

Conditional Error Message

fn form_field(name: &str, error: Option<&str>) -> Markup {
    html! {
        div.form-group {
            input type="text" name=(name);
            @if let Some(err) = error {
                span.error { (err) }
            }
        }
    }
}

List Rendering

fn render_list(items: &[String]) -> Markup {
    html! {
        ul {
            @for item in items {
                li { (item) }
            }
        }
    }
}

Type Conversion

Rendering Custom Types

Any type implementing Display can be spliced:

struct UserId(u64);

impl std::fmt::Display for UserId {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

let id = UserId(123);
html! {
    p { "User ID: " (id) }
}

Common Gotchas

Missing Space Before ID (Rust 2021+)

// ❌ Error
html! {
    div#myid { }
}

// ✅ Correct
html! {
    div #myid { }
}

Unescaped HTML

// ❌ Wrong - outputs escaped HTML entities
html! {
    { "<b>Bold</​b>" }
}
// Outputs: &lt;b&gt;Bold&lt;/​b&gt;

// ✅ Correct - renders as HTML
use maud::PreEscaped;
html! {
    (PreEscaped("<b>Bold</​b>"))
}
// Outputs: <b>Bold</​b>

Type Inference Issues

// ❌ May fail - compiler can't infer type
let items = vec![];

html! {
    @for item in &items {
        li { (item) }
    }
}

// ✅ Correct - explicit type
let items: Vec<String> = vec![];

Performance Characteristics

  1. Compile-time: Templates compile to optimized Rust code
  2. Zero allocations: Static strings embedded in binary
  3. Minimal runtime: ~100 SLoC runtime library
  4. No parsing: No template parsing at runtime
  5. Type-safe: Compiler validates all markup

Conversion from String-Based Templates

// ❌ Runtime template (Tera, Handlebars)
// template.html: <h1>{{ title }}</​h1>
// context.insert("title", &title);
// tera.render("template.html", &context)?

// ✅ Compile-time template (Maud)
html! {
    h1 { (title) }
}

Integration with Rust Types

use serde::Serialize;

#[derive(Serialize)]
struct Post {
    id: u64,
    title: String,
    published: bool,
}

let post = Post {
    id: 1,
    title: "Hello".to_string(),
    published: true,
};

html! {
    article data-id=(post.id) {
        h2 { (post.title) }
        @if post.published {
            span.badge { "Published" }
        }
    }
}

Best Practices

  1. Always escape user input - Use () for splicing, never PreEscaped with untrusted data
  2. Use @let for complex expressions - Improves readability
  3. Prefer pattern matching - Use @match over nested @if
  4. Explicit types - Specify types for collections to avoid inference issues
  5. Small, focused functions - Break complex templates into reusable functions

Key Advantages Over Runtime Templates

FeatureMaudTera/​Handlebars
Compile-time validation✅ Yes❌ No
Type safety✅ Yes❌ No
Runtime overhead✅ Minimal (~100 SLoC)❌ Large
Template hot-reload❌ No*✅ Yes
Learning curveMediumEasy
Binary size✅ SmallestLarger

*Can be achieved with shared library reloading in development

References

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View