Claude Code Plugins

Community-maintained marketplace

Feedback

maud-syntax-fundamentals

@matthewharwood/engmanager.xyz
1
0

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 server-side rendered pages.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name maud-syntax-fundamentals
description 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 server-side rendered pages.

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

Feature Maud Tera/Handlebars
Compile-time validation ✅ Yes ❌ No
Type safety ✅ Yes ❌ No
Runtime overhead ✅ Minimal (~100 SLoC) ❌ Large
Template hot-reload ❌ No* ✅ Yes
Learning curve Medium Easy
Binary size ✅ Smallest Larger

*Can be achieved with shared library reloading in development

References