Astro Content Collections — A 30-Minute Getting-Started

A focused walkthrough of Astro Content Collections: schemas, type safety, frontmatter, and the workflow that makes a 500-article site maintainable.

Content Collections turn a folder of Markdown files into a typed database. Half an hour of setup pays back every time you add a field or rename a slug.

Background

Without Content Collections, an Astro site treats each Markdown file as loose text. With them, you get schema validation, autocompletion, and a single query API for all articles. For sites past 30 posts, this is the difference between confidence and chaos.

How to tell

  • You have or expect more than 30 Markdown / MDX files.
  • You want compile-time errors when frontmatter is wrong.
  • Multiple templates need to read the same content (homepage, hub page, sitemap, RSS).
  • You plan to maintain the site over years, not weeks.

Quick verdict

If you’re starting a content site in Astro in 2026, use Content Collections from day one. Retrofitting later costs more than learning them now.

Step by step

  1. Create src/content/articles/ and drop a few MDX files with frontmatter:
---
title: "Hello world"
description: "A test post to verify content collections work."
publishedAt: 2026-05-22
tags: ["intro", "test"]
author: "alice"
---

# Hello

This is the body.
  1. Define the schema at src/content/config.ts. The schema is the contract — every file must satisfy it or the build fails loudly:
import { defineCollection, reference, z } from 'astro:content';

const articles = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string().min(10).max(80),
    description: z.string().min(120).max(160),
    publishedAt: z.date(),
    updatedAt: z.date().optional(),
    tags: z.array(z.string()).min(1),
    cover: image().optional(),
    author: reference('authors'),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
  }),
});

const authors = defineCollection({
  type: 'data',                       // JSON / YAML, not markdown
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    twitter: z.string().optional(),
  }),
});

export const collections = { articles, authors };
  1. Run npm run dev. Astro validates every file and tells you exactly which one is malformed — fix everything red before continuing:
npm run dev
# Could not parse content collection 'articles':
# src/content/articles/old-post.mdx
#   description: String must contain at most 160 character(s)
  1. Add the author data files at src/content/authors/:
# src/content/authors/alice.yaml
name: Alice Chen
bio: Indie developer writing about content sites.
twitter: alicewrites
  1. Render a list page. Notice the result is typed — your editor will autocomplete entry.data.title:
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

const posts = (await getCollection('articles', e => !e.data.draft))
  .sort((a, b) => +b.data.publishedAt - +a.data.publishedAt);
---
<ul>
  {posts.map(p => (
    <li>
      <a href={`/blog/${p.slug}/`}>{p.data.title}</a>
      <time datetime={p.data.publishedAt.toISOString()}>
        {p.data.publishedAt.toLocaleDateString()}
      </time>
    </li>
  ))}
</ul>
  1. Add the per-article route at src/pages/blog/[...slug].astro and resolve the author reference:
---
import { getCollection, getEntry } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('articles', e => !e.data.draft);
  return posts.map(p => ({ params: { slug: p.slug }, props: { entry: p } }));
}

const { entry } = Astro.props;
const author = await getEntry(entry.data.author);
const { Content } = await entry.render();
---
<article>
  <h1>{entry.data.title}</h1>
  <p>By {author.data.name}</p>
  <Content />
</article>
  1. Production sanity check — build and confirm no schema errors slipped through:
npm run build
# Generating static routes
#  ✓ generated 247 routes

Common pitfalls

  • Skipping the schema and using loose Markdown — you lose every benefit Content Collections provide.
  • Putting required fields as optional to “make builds pass”, which lets bad data through.
  • Storing image references as strings instead of using the image() schema helper, which breaks optimization.
  • Forgetting to restart the dev server after editing the schema — old types stick around.
  • Treating Content Collections like a CMS for non-technical editors — they still write Markdown.

Who this is for

Astro builders making content-heavy sites who want type safety and reliability.

When to skip this

Tiny sites with under 10 pages where the schema overhead doesn’t pay back.

FAQ

  • Do Content Collections work with MDX?: Yes — they support .md and .mdx out of the box. MDX files can include components alongside frontmatter.
  • Can I store non-text content like JSON?: Yes, define a collection with type: "data" for JSON or YAML entries. Useful for authors, tags, and config.
  • What happens when frontmatter is invalid?: The build fails with a clear message pointing to the file and field. This is the feature, not a bug.
  • How do I migrate an existing folder of Markdown?: Move files into src/content/<name>/, write the schema, then run npm run dev. Fix every reported file before shipping.

Tags: #Indie dev #Astro #Content Collections #MDX #Getting started