1 min read

Content collections: the feature that keeps a blog honest

Typed frontmatter sounds like busywork until the day a typo silently drops a post from your index.

Three abstract document cards on a dark background

Here’s a failure mode I’ve hit more than once: you write a post, it renders fine, and it never shows up in the list. The cause is one missing character in the frontmatter — date written as published, or a draft flag spelled differently. The page builds. The index query returns nothing. Silence.

Content collections fix this by moving frontmatter from “stringly typed” to an actual schema.

The schema

// src/content.config.ts
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog/en' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()).default([]),
  }),
});

Now a missing title fails the build with a useful message instead of producing a page with an empty heading. z.coerce.date() accepts 2026-09-28 and hands you a real Date, so sorting has nothing to guess about.

Two collections for two languages

I run English and Chinese as separate collections rather than one collection with a lang field. It costs a little duplication in the config, and buys a lot of simplicity in the pages:

export const collections = {
  'blog-en': defineCollection({ /* ... */ }),
  'blog-zh': defineCollection({ /* ... */ }),
};

Each language’s index queries only its own collection, so there’s no filtering to forget, and no chance of a Chinese post appearing on the English index because someone forgot a predicate.

What I put in the schema

Only fields the templates actually read. Every field you add is a field you have to fill in on every post, and a field that can drift out of sync with what the page uses. title, description, date, draft, tags, cover — that’s it. Reading time and excerpts are derived from the body at build time, not stored.

The rule I follow: if a value can be computed from the post, don’t ask the author to type it.

astromarkdowntypes