RSTRust-First Site ToolkitA Rust-first static site generator with typed collections, explicit routes, and a format-agnostic asset pipeline.

Solve one concrete task

Validate frontmatter and entry references

Add semantic validation after content load and resolve typed slug references safely at render time.

Validate frontmatter after deserialization

Serde answers the structural question: "can this YAML be parsed into the struct?"

Many sites also need semantic validation:

  • dates must follow a collection rule

  • one field must reference another collection by slug

  • a field becomes required only under certain conditions

Malformed frontmatter fences and invalid YAML are reported before this step, while collections are loading. validate_collection(...) is for semantic rules that only make sense after deserialization has already succeeded.

Define typed entry references

Use EntryRef<T> when a frontmatter field points at another collection entry:

#[derive(Clone, Debug, Deserialize)]
struct BlogPost {
    title: String,
    date: String,
    #[serde(default)]
    author: Option<EntryRef<Author>>,
}

#[derive(Clone, Debug, Deserialize)]
struct Author {
    name: String,
}

This keeps the frontmatter model honest: the field is not "some string that happens to be a slug". It is a reference.

Register a collection validator

Use validate_collection(...) to enforce the rules that go beyond deserialization:

.validate_collection::<BlogPost, _>("blog", |ctx, entries| {
    for entry in entries {
        if !looks_like_iso_date(&entry.frontmatter.date) {
            return Err(ctx
                .invalid_frontmatter_field(entry, "date", "blog dates must use YYYY-MM-DD")
                .with_help("use an ISO calendar date such as `2026-03-27`")
                .into_error());
        }

        let Some(author) = entry.frontmatter.author.as_ref() else {
            continue;
        };

        if ctx.entry_by_ref("authors", author).is_err() {
            return Err(ctx
                .invalid_frontmatter_field(
                    entry,
                    "author",
                    format!("blog post author `{}` does not exist", author.slug()),
                )
                .into_error());
        }
    }

    Ok(())
})

Validators run after all collections load and before the build succeeds, so failures stop the build early and point back to the right source document.

Resolve the reference during rendering

After validation, route code stays simple:

let author_name = entry
    .frontmatter
    .author
    .as_ref()
    .map(|author| {
        ctx.entry_by_ref("authors", author)
            .map(|entry| entry.frontmatter.name.clone())
    })
    .transpose()?;

The validation pass removes the need for defensive lookups all over your templates.