Astro Content Collections for Migrating WordPress Content
When you move a heavy WordPress site toward Astro, the hardest part is rarely the frontend. The tricky part is the content. You need a way to preserve structure, keep metadata intact, and avoid turning the migration into a pile of loose Markdown files with inconsistent frontmatter.
That is exactly where Astro Content Collections become useful. They give you a typed, validated, predictable shape for your content while you are migrating. Instead of treating the move as a one-off export, you can build a proper landing zone for WordPress content and gradually bring the old site into a cleaner structure.
Why Content Collections Help
WordPress content usually arrives with a lot of shape-shifting:
- titles are in one place
- excerpts are in another
- custom fields may live in ACF or post meta
- featured images are often referenced separately
- categories and tags may be nested inconsistently
If you import that directly into Astro pages without a schema, you are asking for trouble later. Content collections solve that by making the contract explicit.
You define the fields you expect, validate them at build time, and let Astro tell you when the content no longer matches the rules.
That matters during migration because migrations are messy by nature. A typed collection gives you a safe place to clean things up.
Start With a Schema That Mirrors the WordPress Shape
The first thing I like to do is decide what the new content model should look like. Not every field from WordPress deserves to survive. Some fields get dropped. Some get renamed. Some become computed values.
Here is a simple collection schema for blog posts:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
sourceUrl: z.string().url().optional(),
wordpressId: z.number().optional(),
}),
});
export const collections = { blog };
That schema does a few useful things:
- keeps frontmatter consistent
- lets you store migration metadata
- gives you a place to track which WordPress post became which Astro post
- lets you spot bad data before it ships
Build an Import Step, Not a Manual Copy Job
One of the easiest mistakes is to copy and paste content manually from WordPress into Markdown files. It works for a few posts and then gets painful fast.
Instead, I prefer a tiny import script that transforms WordPress JSON into Astro-friendly Markdown. You can run it once, run it again, or improve it over time without redoing the whole migration.
import fs from 'node:fs/promises';
type WordPressPost = {
id: number;
slug: string;
title: { rendered: string };
excerpt: { rendered: string };
content: { rendered: string };
date: string;
modified: string;
featured_media?: number;
};
function stripHtml(value: string) {
return value.replace(/<[^>]+>/g, '').trim();
}
function toFrontmatter(post: WordPressPost) {
return `---
title: '${stripHtml(post.title.rendered).replace(/'/g, "''")}'
description: '${stripHtml(post.excerpt.rendered).replace(/'/g, "''")}'
pubDate: '${new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}'
updatedDate: '${new Date(post.modified).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}'
wordpressId: ${post.id}
sourceUrl: 'https://example.com/${post.slug}'
draft: false
---
`;
}
async function writePost(post: WordPressPost) {
const markdown = `${toFrontmatter(post)}${stripHtml(post.content.rendered)}\n`;
await fs.writeFile(`./src/content/blog/${post.slug}.md`, markdown, 'utf8');
}
That example is simplified, but the pattern is the part that matters. You make the import step deterministic, and you keep the rules in one place.
Use Collections to Separate Migration from Presentation
The best part of Content Collections is that they let you stop thinking of content as “whatever the CMS gives me” and start thinking of it as “data I control”.
Once the content is in Astro, the frontend becomes simpler:
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<ul class="space-y-6">
{posts.map((post) => (
<li>
<a href={`/blog/${post.slug}/`} class="block rounded-lg border p-4 hover:bg-gray-50">
<h2 class="text-xl font-semibold">{post.data.title}</h2>
<p class="text-gray-600 mt-2">{post.data.description}</p>
</a>
</li>
))}
</ul>
That clean split is important. WordPress becomes the source system you are migrating from. Astro Content Collections become the source system you are migrating into. The frontend just reads the typed result.
Migration Strategy That Actually Works
I would not recommend migrating an entire WordPress site in one giant pass unless the site is tiny. A better approach is to do it in layers:
- pick one content type, usually posts or pages
- define the Astro schema
- build the import script
- validate a handful of posts
- render them in Astro
- compare the new output to the old site
That gives you a repeatable path instead of a one-off conversion.
You can also keep some content in WordPress longer than others. For example:
- blog posts can move first
- landing pages can follow
- complex post types can stay in WordPress until you are ready
This is where hybrid sites get practical. You do not need a perfect all-or-nothing migration. You need a safe path from a messy old stack to a cleaner new one.
What to Preserve, What to Drop
When you import WordPress content, not every field is worth carrying across.
Usually I preserve:
- title
- description or excerpt
- publish date
- updated date
- featured image
- tags or categories
- source URL or ID for traceability
Usually I drop or rewrite:
- shortcodes
- plugin-specific markup
- inline styling from the editor
- accidental HTML clutter
This is one of the hidden advantages of using Astro collections during migration. It forces you to make content quality decisions instead of preserving every artifact from the old CMS.
Final Thought
Astro Content Collections are more than a nice content feature. In a WordPress migration, they become the structure that keeps the project sane.
If you treat them as a proper landing zone, they can help you:
- validate imported content
- normalize metadata
- reduce frontend complexity
- keep the migration incremental
- ship a better site without a full rewrite
That is why I keep coming back to them. They turn a migration from a vague “we should modernize this” idea into an actual workflow.
If your WordPress site is getting too heavy and you want a cleaner front end, Content Collections are one of the best starting points in Astro.