Content Collections
A content collection is a folder of Markdown, JSON or YAML files that your code reads as typed data. Use it for posts, docs or products. The build reads each file once. A request parses nothing.
Declare a collection
Put the files in content/<name>/. Declare the entry type with #[collection("<name>")] on a struct in a file of src/, for example src/content.rs.
#[collection("posts")]
struct Post {
title: &'static str,
date: &'static str,
summary: Option<&'static str>,
tags: &'static [&'static str],
draft: bool,
slug: &'static str,
body: &'static str,
}
A file content/posts/hello.md is one entry:
---
title: Hello
date: 2026-11-01
tags: [wisp, content]
draft: false
---
# Hello
The text of the post.
Fields
- A field type is static data:
&'static str,bool, a number,&'static [&'static str], orOptionof one of them.StringandVecfail the build. - A front matter line is
name: value. A list is[a, b].og-imageis the fieldog_image, as in a Markdown page. slugis the file name without.md.bodyis the Markdown as HTML. The build sets both, so the front matter does not.- A missing
titleis the text of the first# h1. - An
Optionfield can be missing. Any other field must be in the front matter.
Errors
The build fails with the file and the line. It checks these cases:
- A field that the front matter lacks:
content/posts/a.md:1: Post needsdatein the front matter (date is text). - A name that the struct lacks, such as a typo:
content/posts/a.md:2:titelis not a field of Post. Its fields are title, date, tags, .... Add it to the struct in <file>, or fix the name. - A value of the wrong type:
content/posts/a.md:5:draft: maybeis not true or false (Post.draft: bool). write true or false. - A type that cannot be static data, and a folder
content/<name>that does not exist.
Query
content is the file src/content.rs. Each collection is a module in it with these functions.
let posts = content::posts::all(); // &'static [Post]
let hello = content::posts::get("hello"); // Option<&'static Post>
let live = content::posts::filter(|p| !p.draft); // Vec<&'static Post>
let top = content::posts::first(|p| p.tags.contains(&"rust"));
let n = content::posts::count(|p| p.draft); // usize
let by_title = content::posts::sorted(|p| p.title);
let longest = content::posts::sorted_desc(|p| p.body.len());
let second_page = content::posts::page(10, 10); // &'static [Post]
let live_page = content::posts::page_of(|p| !p.draft, 10, 10); // Vec<&'static Post>
let (newer, older) = content::posts::around("hello"); // (Option<&Post>, Option<&Post>)
filter, first and count take a test and read the entries in the order of all(). sorted puts the smallest key first and sorted_desc the largest key first. Equal keys keep the order of all(). page(skip, limit) returns at most limit entries after the first skip, and it returns an empty slice when skip is past the end. page_of does the same for the entries that pass a test, and it returns a Vec. around(slug) returns the entry before and the entry after slug in the order of all(). A value is None at an end of the list and for an unknown slug. The test of filter, first, count and page_of can be FnMut. No function skips drafts by itself: draft is a field of your own, so write filter(|p| !p.draft).
all() is newest first when the struct has a date field. Else it is in slug order. A date of text must be a real YYYY-MM-DD day (2026-02-31 fails), with an optional time (2026-01-02T10:30, then optional seconds and a Z or +02:00 zone). Other text fails the build, because 2026-9-1 would sort after 2026-10-01. A symbolic link in content/ fails the build. A file has at most 1 MiB and a collection has at most 10000 entries. A folder counts as an entry. get is a binary search of the slugs.
In a page, show the body with {@html post.body}. The body is the text of your own files, so treat it as trusted. The example examples/blog has an index page and a page for each post.
Cost
The build parses each file once and writes the entries as a static array in the binary. A request does not read a file and does not parse Markdown. In wisp dev, a save in content/ builds the app again.
Other file kinds and folders
- A
.jsonfile is one JSON object. A list is a JSON array. Anullvalue counts as missing. - A
.yamlor.ymlfile is flatname: valuelines, with the same rules as front matter. Nesting and- itemlists are errors. - JSON and YAML entries have no body. Their
bodyis an empty string. - Nested folders work. The file
content/docs/guide/start.mdhas the slugguide/start. A route[...slug]can take it. - A
README.mdis not an entry. Names that start with.and other file kinds are skipped. - Two files with one slug, such as
a.mdanda.json, fail the build.
Is This Page Useful?