zfb

Type to search...

to open search from anywhere

Content Collections

CreatedJun 1, 2026Takeshi Takatsudo

Define typed collections of Markdown content in zfb.config and load them from pages.

A content collection is a directory of Markdown (or MDX) files declared in your project config. zfb scans the directory at build time, parses each file’s frontmatter against a schema you supply, and exposes the entries through a getCollection() helper your pages can call.

Declaring a collection

Collections are configured in zfb.config.ts (or zfb.config.json) under the collections key. Each entry has a name and a path:

export default {
  collections: [
    {
      name: "blog",
      path: "content/blog",
    },
  ],
};

The name is the identifier you pass to getCollection(). The path is the directory (relative to the project root) holding the entries. zfb walks that directory, treats each file as a Markdown document with frontmatter, and exposes its entries through getCollection("blog").

You can additionally supply an optional schema field — a JSON Schema subset that validates each entry’s frontmatter at build time. The supported keywords (type, properties, items, required) are documented on the <code>defineConfig</code> page. The [{ name, path }] form remains supported for projects that don’t need per-field validation.

Loading entries from a page

Pages use getCollection() to enumerate every entry, or getEntry() to look up a single one by slug:

import { getCollection, getEntry } from "zfb/content";

export default function BlogIndex() {
  const posts = getCollection("blog");
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <a href={`/blog/${post.slug}`}>{post.data.title}</a>
        </li>
      ))}
    </ul>
  );
}
import { getEntry } from "zfb/content";

export default function FeaturedPost() {
  const featured = getEntry("blog", "hello-zfb");
  if (!featured) return null;
  return <featured.Content />;
}

Both calls are synchronous. The entire content snapshot is built in Rust before any TSX module runs and embedded on globalThis.__zfb, so there’s no I/O at call time and no await to thread through. The Rust↔JS bridge contract that backs this surface is stable and versioned with the zfb package.

Each entry has three things you can rely on:

  • data — the parsed, validated frontmatter (typed against your schema).
  • Content — a renderable React/Preact component compiled from the body. Render it as <post.Content components={...} /> and pass element-level overrides through the components prop. This is the same contract Astro’s @astrojs/mdx exposes; see MDX Components for details and defaultComponents recipes.
  • slug — derived from the file name (my-first-post.mdmy-first-post). Nested directories become slash-separated slugs.

The function signatures live in <code>getCollection</code> and the matching getEntry.

How parsing works

Under the hood, the zfb-content crate handles three jobs: it walks the configured directory, parses each file’s YAML frontmatter, and compiles the Markdown/MDX body through an mdast → JSX-source emitter that is then handed to the existing SWC TSX → JS pipeline. The result is a JSX module per entry, addressed by a stable mdx://<collection>/<slug> specifier; the page renderer evaluates that module on demand and surfaces it to your page as entry.Content.

The compilation and surface contract are stable. See MDX Components for the rendering side.

Revision History