Claude-skill-registry-data 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 server-side rendered pages.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/maud-syntax-fundamentals" ~/.claude/skills/majiayu000-claude-skill-registry-data-maud-syntax-fundamentals && rm -rf "$T"
manifest:
data/maud-syntax-fundamentals/SKILL.mdsource content
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
macrohtml! - Replacing runtime templating engines (Tera, Handlebars)
- Email template generation
- RSS/Atom feed generation
Core Philosophy
- Compile-time validation - Template errors caught by rustc, not at runtime
- Type safety - Leverages Rust's type system for HTML generation
- Zero overhead - Templates compile to optimized Rust code
- Auto-escaping - All text content is HTML-escaped by default
- 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 <world> // 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: <b>Bold</b> // ✅ 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
- Compile-time: Templates compile to optimized Rust code
- Zero allocations: Static strings embedded in binary
- Minimal runtime: ~100 SLoC runtime library
- No parsing: No template parsing at runtime
- 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
- Always escape user input - Use
for splicing, never()
with untrusted dataPreEscaped - Use
for complex expressions - Improves readability@let - Prefer pattern matching - Use
over nested@match@if - Explicit types - Specify types for collections to avoid inference issues
- 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
- Official Docs: https://maud.lambda.xyz
- API Docs: https://docs.rs/maud
- Crates.io: https://crates.io/crates/maud