Astro content collections migration illustration

Migrating to Astro v5 Content Collections: A Practical Guide

Why I Migrated to Astro v5 Collections

I’ll admit it: I put off upgrading my Astro content collections for a while. The old API worked, and I had a lot of blog posts already humming along in src/content/blog/. But with Astro v5, the new Content Layer API isn’t just a nice-to-have—it’s the future. Type safety, custom loaders, and a more flexible config? Count me in.

What Changed in v5

Astro v5 introduces a new way to define content collections. Instead of the legacy src/content/config.ts and type: 'content', you now use a root-level src/content.config.ts and a loader-based approach. This unlocks more power and future-proofs your content.

Key differences:

  • Collections are defined in src/content.config.ts (not inside a folder).
  • Use the loader property with glob from astro/loaders.
  • No more type: 'content' or entries.

Step-by-Step Migration

Here’s how I migrated my blog collection:

1. Move Markdown Files (Optional)

I moved my blog posts from the legacy src/content/blog/ to a flatter src/blog/ directory for clarity. You can keep your structure, but I found this cleaner.

mkdir src/blog
mv src/content/blog/*.md src/blog/

2. Create the New Config

I replaced my old config with a new src/content.config.ts:

import { defineCollection, z } from 'astro:content'
import { glob } from 'astro/loaders'
 
const blog = defineCollection({
	loader: glob({ pattern: '*.md', base: './src/blog' }),
	schema: z.object({
		title: z.string(),
		description: z.string(),
		pubDate: z.date(),
		updatedDate: z.date().optional(),
		image: z
			.object({
				url: z.string(),
				alt: z.string(),
			})
			.optional(),
		tags: z.array(z.string()).default([]),
		draft: z.boolean().default(false),
	}),
})
 
export const collections = {
	blog,
}

3. Update Your Imports

If you were using the old collections export, switch to the new getCollection('blog') API from astro:content. Luckily, my pages already used this, so no changes were needed.

4. Test Everything

I ran a build and checked my blog index, tag, and post pages. Everything loaded, and Astro’s type safety caught a missing field in one post—exactly what I wanted!

Lessons Learned

  • The new loader API is more explicit and flexible.
  • Type errors are surfaced early, making content safer for future edits.
  • The migration was smoother than expected—most of my code didn’t need to change.

Resources


If you’re still on the legacy API, I highly recommend making the switch. It’s a small investment for a much better developer experience. Let me know if you run into any snags—I’m happy to help!