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.