Content Collections

A ReeWeb content collection is a folder of Markdown entries with a _schema.ts file. The schema validates each entry's frontmatter during a static build, so a missing title, malformed date, or incorrect author structure stops a broken content release before it reaches dist/.

There is no central registry. A folder becomes a collection as soon as it contains _schema.ts exporting a Zod schema.

Create a Collection

For a blog under src/public/blog/, add this file:

// src/public/blog/_schema.ts
import { z } from "$vendor/zod.min.js";

import { required_date } from "$root/src/lib/schema_helpers";

export const schema = z.object({
    title: z.string().min(1),
    published_at: required_date("published_at"),
    description: z.string().min(1),
    authors: z.array(z.union([
        z.string(),
        z.object({ name: z.string() }).passthrough(),
    ])).optional(),
}).passthrough();

Then give each Markdown entry frontmatter that satisfies it:

---
title: "A reliable build"
published_at: 2026-07-21
description: "Validate frontmatter before a static site ships."
authors:
    - name: "Ada Lovelace"
---

required_date() (from src/lib/schema_helpers.ts, shipped with the starter) accepts a normal YAML date or a date string, and requires the field. Use its sibling optional_date() for a date field that isn't required on every entry - or .optional() on any other field type. .passthrough() preserves framework and project-specific frontmatter keys instead of rejecting them.

You could use Zod's own z.coerce.date() directly instead, but reach for the helper for any required date field: z.coerce.date() coerces before validating, so a missing or unparseable value becomes an Invalid Date object rather than staying undefined, and Zod's default message for that case reads "expected date, received Date" - confusing, since both sides say "date". required_date() / optional_date() replace it with a message that says what to fix.

What Is Validated

bun ssg discovers every _schema.ts under src/public/, imports its schema, then validates the Markdown records below that folder. A collection listing file at its own root, such as blog/index.md or blog/01_index.md, is excluded from entry validation. A nested blog/my-post/index.md is an entry and is validated.

All validation problems are collected before the build exits. This makes one repair pass possible instead of stopping at the first bad file.

✗ Content collection validation failed (2 issue(s)):
    blog/02_announcement.md - title: Invalid input: expected string, received undefined
    blog/02_announcement.md - published_at: published_at is required and must be a valid date (e.g. "2026-01-15")

_schema.ts is build tooling only. It is not copied to dist/ and creates no runtime dependency; ReeWeb uses its vendored Zod copy at $vendor/zod.min.js.

Validate Without Building

An MCP client can run the read-only validate_content tool to check collection schemas and .ree/.md route collisions without creating a site build. See MCP Server.

For normal local and CI verification, run the production build:

bun ssg

The build exits non-zero when any collection entry violates its schema. Keep the schema next to the content it governs and version it together with the entries, so an editorial change and its constraints always travel together.

Collections, Listings, and Visibility

Collections validate content; they do not create an index page or automatically enable pagination. Use a sibling index.ree and, when needed, configure Pagination for the collection route.

The normal Content Visibility policy still applies after validation. Draft and future-dated posts may be valid collection entries while remaining out of listings, feeds, and the sitemap.