Content Collections
In Next.js you often reach for contentlayer or raw fs + gray-matter to load MDX files with type safety. Astro has content collections built in: put MD/MDX files in src/content/<collection>/, define a schema with zod in src/content.config.ts, and use getCollection('blog') anywhere in your project — fully typed, no third-party library required.
Needs a full Astro project to run. Use
npm create astro@latestto scaffold one.
Defining a collection: contentlayer vs Astro
Section titled “Defining a collection: contentlayer vs Astro”// contentlayer.config.ts (Next.js)import { defineDocumentType, makeSource } from 'contentlayer/source-files';
export const Post = defineDocumentType(() => ({ name: 'Post', filePathPattern: 'blog/**/*.mdx', contentType: 'mdx', fields: { title: { type: 'string', required: true }, date: { type: 'date', required: true }, tags: { type: 'list', of: { type: 'string' } }, },}));
export default makeSource({ contentDirPath: 'content', documentTypes: [Post] });
// In a page:// import { allPosts } from 'contentlayer/generated';// src/content.config.ts (Astro)import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), date: z.coerce.date(), tags: z.array(z.string()).optional(), }),});
export const collections = { blog };
// In a page:// import { getCollection } from 'astro:content';// const posts = await getCollection('blog');The schema lives in src/content.config.ts — Astro validates every file in the collection at build time and surfaces type errors in your editor.
content.config.ts
Section titled “content.config.ts”import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ type: 'content', // 'content' for MD/MDX, 'data' for JSON/YAML schema: z.object({ title: z.string(), description: z.string().optional(), date: z.coerce.date(), draft: z.boolean().default(false), tags: z.array(z.string()).default([]), }),});
export const collections = { blog };Listing collection entries
Section titled “Listing collection entries”---import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);const sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());---<html lang="en"> <head><meta charset="utf-8" /><title>Blog</title></head> <body> <h1>Blog</h1> <ul> {sorted.map((post) => ( <li> <a href={`/blog/${post.slug}/`}>{post.data.title}</a> <time>{post.data.date.toLocaleDateString()}</time> </li> ))} </ul> </body></html>The second argument to getCollection is an optional filter function — here it removes drafts. post.data is fully typed from the zod schema.