Astro is the best WordPress replacement for content-heavy sites in 2026 -- blog, docs, marketing pages -- because it ships zero JavaScript by default and beats WordPress on Core Web Vitals by 60-80 points. I've migrated a dozen WordPress sites to Astro over the past two years, ranging from 50-post personal blogs to 3,000-page documentation portals, and the results are consistently dramatic: sub-second load times, Lighthouse scores above 95, and hosting bills that drop from $99/month to literally zero.

This isn't a "just export XML and pray" guide. I'm going to walk you through the exact process I use, including the gotchas that'll bite you if you don't plan for them -- broken URLs, missing images, orphaned shortcodes, and the comment system question that trips everyone up.

Table of Contents

Why Astro Instead of WordPress

WordPress is overengineered for most content sites. You're running PHP, MySQL, a web server, a caching layer, and probably a dozen plugins just to serve what's fundamentally static content. Every page load hits a database. Every plugin is a potential security hole. Every update is a prayer that nothing breaks.

Astro flips this model. It pre-renders your pages to static HTML at build time. No database. No server-side runtime. No PHP. The output is plain HTML, CSS, and -- only when you explicitly opt in -- JavaScript.

Here's what I consistently see across migrations:

Metric WordPress (Managed) Astro (Static) Improvement
Lighthouse Performance 34-55 95-100 +60-80 pts
First Contentful Paint 2.8-4.2s 0.4-0.8s ~80% faster
Total Blocking Time 200-800ms 0-10ms ~98% reduction
Page Size (typical blog post) 1.5-3.5 MB 80-250 KB ~90% smaller
Time to Interactive 4-8s 0.5-1.0s ~85% faster
HTTP Requests 60-130 8-15 ~85% fewer

These aren't cherry-picked. They're averages from real migrations. WordPress sites with caching plugins and CDNs still can't touch a static Astro build because they're fundamentally doing more work per request.

The Security Angle

WordPress is the most attacked CMS on the internet. Not because it's bad, but because it's everywhere and it has a massive attack surface -- PHP execution, database access, file uploads, XML-RPC, REST API endpoints, admin login pages. Every month brings new plugin vulnerabilities.

Astro sites deployed to a CDN have essentially zero attack surface. There's no server to exploit, no admin panel to brute-force, no database to inject into. Your site is a folder of HTML files sitting on a global edge network.

The Developer Experience

If you've ever tried to customize a WordPress theme, you know the pain: PHP template tags mixed with HTML, the template hierarchy that requires memorization, functions.php files that grow into unmaintainable monsters, constant plugin conflicts.

Astro uses a component-based architecture with a file format that looks like HTML with superpowers. You can use React, Vue, Svelte, or Solid components inside Astro pages -- but only when you actually need interactivity. For a blog or marketing site, you probably don't.

---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<BlogLayout title={post.data.title} description={post.data.excerpt}>
  <article>
    <h1>{post.data.title}</h1>
    <time datetime={post.data.date.toISOString()}>
      {post.data.date.toLocaleDateString()}
    </time>
    <Content />
  </article>
</BlogLayout>

That's a complete dynamic blog post page. Try doing that in WordPress with the same clarity.

Astro vs Next.js vs Gatsby for WordPress Migration

Astro

Astro was purpose-built for content sites. Its "islands architecture" means you ship zero JS unless a specific component needs it. Content collections give you type-safe markdown handling with built-in schema validation. Build times are fast, the mental model is simple, and you don't need to understand React to use it. For blogs, docs, and marketing sites, it's the obvious choice in 2026. If you're exploring this route, our Astro development team has handled migrations at every scale.

Next.js

Next.js is a full application framework. It handles authentication, server-side rendering, API routes, middleware, and a hundred other things you don't need for a blog. You'll ship the React runtime to every visitor whether they need interactivity or not (App Router's server components help, but the baseline bundle is still heavier). Next.js makes sense when you're building a SaaS product or a site with heavy dynamic functionality -- user dashboards, e-commerce, real-time features. For a content migration from WordPress? It's overkill. That said, if your site does need those features, our Next.js team can help you figure out the right architecture.

Gatsby

Gatsby is effectively in maintenance mode. Netlify acquired it in 2023, and development has slowed to a crawl. The GraphQL data layer that once seemed clever now feels like unnecessary complexity. Build times for large sites are painful. The plugin ecosystem is stale. I'd strongly recommend against starting a new Gatsby project in 2026. If you're currently on Gatsby, migrating to Astro is actually easier than migrating from WordPress because your content is likely already in markdown.

Feature Astro Next.js Gatsby
Default JS shipped 0 KB ~85-100 KB ~70-90 KB
Content collections Built-in, type-safe Manual setup GraphQL layer
Build time (1000 posts) ~30-45s ~60-90s ~120-300s
Learning curve Low Medium-High Medium
Best for Content sites Web apps Legacy projects
Active development Very active Very active Minimal
SSR support Optional Default Limited

7-Step Migration Playbook

Here's the exact process I follow. No hand-waving.

Step 1: Audit Your WordPress Site

Before you touch any code, you need to know what you're working with. Log into your WordPress admin and take inventory.

# If you have WP-CLI installed (you should)
wp post list --post_type=post --format=csv --fields=ID,post_title,post_name,post_date > posts.csv
wp post list --post_type=page --format=csv --fields=ID,post_title,post_name,post_date > pages.csv
wp plugin list --format=table
wp theme list --format=table

Document every plugin and what it does. You'll need to find Astro equivalents or drop them. Common ones:

  • Yoast SEO → Astro's built-in <head> management + @astrojs/sitemap
  • Contact Form 7 → Formspree, Formspark, or a serverless function
  • WP Super Cache / W3 Total Cache → Not needed (your site is already static)
  • Wordfence / Sucuri → Not needed (no server to protect)
  • Google Analytics plugin → Direct script tag or Partytown integration
  • WooCommerce → Snipcart, Shopify Buy Button, or a dedicated e-commerce platform

Step 2: Export WordPress Content

You've got two options: XML export or REST API. I recommend the XML export for most sites because it captures everything in one shot.

In WordPress admin: Tools → Export → All Content → Download Export File

This gives you a .xml file containing every post, page, comment, custom field, category, tag, and media reference.

For larger sites (1000+ posts), the REST API approach is more reliable:

// scripts/export-wp.mjs
import fs from 'fs/promises';
import path from 'path';

const WP_URL = 'https://your-wordpress-site.com/wp-json/wp/v2';
const PER_PAGE = 100;

async function fetchAllPosts() {
  let page = 1;
  let allPosts = [];
  
  while (true) {
    const res = await fetch(
      `${WP_URL}/posts?per_page=${PER_PAGE}&page=${page}&_embed`
    );
    
    if (!res.ok) break;
    
    const posts = await res.json();
    if (posts.length === 0) break;
    
    allPosts = allPosts.concat(posts);
    console.log(`Fetched page ${page} (${allPosts.length} posts total)`);
    page++;
  }
  
  return allPosts;
}

const posts = await fetchAllPosts();
await fs.writeFile('wp-posts.json', JSON.stringify(posts, null, 2));
console.log(`Exported ${posts.length} posts`);

Step 3: Scaffold Your Astro Project

npm create astro@latest my-new-site
cd my-new-site

# Add the integrations you'll need
npx astro add mdx
npx astro add sitemap
npx astro add tailwind

# Install additional dependencies
npm install sharp @astrojs/rss

I recommend starting with a minimal setup rather than a theme. Themes add complexity you don't need during migration. Get the content working first, then style it.

Step 4: Convert Content to Markdown

This is where the real work happens. You need to convert WordPress HTML content into clean markdown with proper frontmatter.

I use a custom Node.js script with turndown for HTML-to-markdown conversion:

npm install turndown @wordpress/block-serialization-default-parser
// scripts/convert-posts.mjs
import TurndownService from 'turndown';
import fs from 'fs/promises';
import path from 'path';

const turndown = new TurndownService({
  headingStyle: 'atx',
  codeBlockStyle: 'fenced',
});

// Handle WordPress-specific HTML
turndown.addRule('wpCaption', {
  filter: (node) => {
    return node.nodeName === 'DIV' && 
           node.className.includes('wp-caption');
  },
  replacement: (content, node) => {
    const img = node.querySelector('img');
    const caption = node.querySelector('.wp-caption-text');
    return `![${caption?.textContent || ''}](${img?.src || ''})\n`;
  },
});

const posts = JSON.parse(await fs.readFile('wp-posts.json', 'utf-8'));

for (const post of posts) {
  const slug = post.slug;
  const markdown = turndown.turndown(post.content.rendered);
  
  const frontmatter = `---
title: "${post.title.rendered.replace(/"/g, '\\"')}"
date: ${post.date}
excerpt: "${(post.excerpt.rendered || '').replace(/<[^>]*>/g, '').trim().replace(/"/g, '\\"')}"
categories: [${(post._embedded?.['wp:term']?.[0] || []).map(c => `"${c.name}"`).join(', ')}]
tags: [${(post._embedded?.['wp:term']?.[1] || []).map(t => `"${t.name}"`).join(', ')}]
featuredImage: "${post._embedded?.['wp:featuredmedia']?.[0]?.source_url || ''}"
draft: false
---`;

  const content = `${frontmatter}\n\n${markdown}\n`;
  
  await fs.mkdir('src/content/blog', { recursive: true });
  await fs.writeFile(`src/content/blog/${slug}.md`, content);
  console.log(`Converted: ${slug}`);
}

Step 5: Download and Organize Media

WordPress stores media in wp-content/uploads/YYYY/MM/ directories. You need to download everything and update references.

# Quick and dirty: wget mirror of your uploads directory
wget -r -np -nH --cut-dirs=2 -P public/uploads \
  https://your-wordpress-site.com/wp-content/uploads/

# Then find-and-replace old URLs in your markdown files
find src/content/blog -name '*.md' -exec sed -i '' \
  's|https://your-wordpress-site.com/wp-content/uploads/|/uploads/|g' {} +

For production migrations, I use Astro's image optimization with the sharp library to convert everything to WebP and generate responsive sizes at build time. This alone can cut image payload by 40-60%.

Step 6: Build Layouts and Components

Create your Astro layouts to match (or improve upon) your WordPress theme structure. At minimum you'll need:

  • src/layouts/BaseLayout.astro -- HTML shell, <head>, nav, footer
  • src/layouts/BlogLayout.astro -- Single post template
  • src/pages/blog/index.astro -- Blog listing with pagination
  • src/pages/blog/[...slug].astro -- Dynamic post pages
  • src/pages/index.astro -- Homepage

Step 7: Test, Redirect, Deploy

Build locally and verify everything:

npm run build
npm run preview

# Check for broken links
npx linkinator http://localhost:4321 --recurse

Set up redirects (covered in detail below), configure your DNS, and deploy. I'll cover hosting options in the cost comparison section.

WordPress Data Export to Astro Content Collections

Astro's content collections are one of its best features. They give you type-safe access to your markdown content with schema validation. Here's how to set them up properly for migrated WordPress content.

Defining Your Schema

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

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

export const collections = { blog };

The beauty of this: if any of your migrated posts have missing or malformed frontmatter, Astro will tell you at build time with a clear error message. No more silent failures.

Handling WordPress Shortcodes

This is the gotcha that most migration guides skip. If your WordPress posts use shortcodes like [gallery], [caption], [embed], or any custom shortcodes from plugins, they'll show up as raw text in your markdown.

You've got three options:

  1. Pre-process during conversion -- Write regex rules in your conversion script to transform shortcodes into markdown or HTML equivalents
  2. Use MDX -- Convert affected posts to .mdx and create Astro components that replace shortcodes
  3. Find and replace manually -- For small sites, sometimes the fastest approach
// Example: converting WordPress gallery shortcode during conversion
function convertShortcodes(content) {
  // [gallery ids="1,2,3"]
  content = content.replace(
    /\[gallery ids="([^"]+)"\]/g,
    (match, ids) => {
      const imageIds = ids.split(',');
      return imageIds.map(id => `![](/uploads/gallery/${id.trim()}.jpg)`).join('\n');
    }
  );
  
  // [youtube url="..."]
  content = content.replace(
    /\[youtube[^\]]*url="([^"]+)"[^\]]*\]/g,
    (match, url) => `<iframe src="${url}" width="560" height="315" frameborder="0"></iframe>`
  );
  
  return content;
}

Querying Content

Once your content is in collections, querying it is straightforward:

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

const posts = (await getCollection('blog'))
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---

<BlogLayout title="Blog">
  <ul>
    {posts.map(post => (
      <li>
        <a href={`/blog/${post.slug}/`}>
          <h2>{post.data.title}</h2>
          <time>{post.data.date.toLocaleDateString()}</time>
          <p>{post.data.excerpt}</p>
        </a>
      </li>
    ))}
  </ul>
</BlogLayout>

Preserving SEO: 301 Redirects, Sitemap, and Schema

This section is critical. A botched migration can destroy years of SEO equity overnight. Don't skip any of this.

URL Structure and 301 Redirects

WordPress defaults to URLs like /2024/03/my-post-title/ or /?p=123. You need to map every old URL to its new Astro equivalent.

First, generate a complete list of old URLs:

# Using WP-CLI
wp post list --post_type=post,page --field=url > old-urls.txt

# Or crawl the sitemap
curl -s https://your-site.com/sitemap.xml | grep -oP '<loc>\K[^<]+'

For Vercel, create a vercel.json in your project root:

{
  "redirects": [
    { "source": "/2024/03/my-old-post/", "destination": "/blog/my-old-post/", "permanent": true },
    { "source": "/:year(\\d{4})/:month(\\d{2})/:slug/", "destination": "/blog/:slug/", "permanent": true },
    { "source": "/category/:slug/", "destination": "/blog/category/:slug/", "permanent": true },
    { "source": "/feed/", "destination": "/rss.xml", "permanent": true }
  ]
}

For Netlify, use _redirects in your public/ folder:

/2024/03/my-old-post/  /blog/my-old-post/  301
/category/*            /blog/category/:splat  301
/feed/                 /rss.xml  301

For Cloudflare Pages, use _redirects (same format as Netlify) or Bulk Redirects in the dashboard.

Sitemap Generation

The @astrojs/sitemap integration handles this automatically:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://your-new-site.com',
  integrations: [sitemap()],
});

After deploying, submit your new sitemap in Google Search Console immediately. Also submit a removal request for any URLs that no longer exist and aren't redirected.

Structured Data / Schema Markup

If Yoast was handling your schema markup, you need to replicate it. Create a reusable component:

---
// src/components/ArticleSchema.astro
const { title, date, excerpt, image, author = 'Your Name' } = Astro.props;

const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": title,
  "datePublished": date,
  "description": excerpt,
  "image": image,
  "author": {
    "@type": "Person",
    "name": author
  }
};
---

<script type="application/ld+json" set:html={JSON.stringify(schema)} />

RSS Feed

Don't forget your RSS subscribers:

// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: 'Your Site',
    description: 'Your description',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.date,
      description: post.data.excerpt,
      link: `/blog/${post.slug}/`,
    })),
  });
}

Hosting Cost Comparison

This is where the business case for migration becomes impossible to ignore.

Hosting Scenario Monthly Cost Annual Cost Notes
WP Engine (Managed WordPress) $30-60 $360-720 Startup-Growth plans
Kinsta (Managed WordPress) $35-70 $420-840 Starter-Business plans
Flywheel (Managed WordPress) $15-30 $180-360 Tiny-Personal plans
Self-hosted (DigitalOcean + maintenance) $12-24 $144-288 Plus your time for updates
Astro on Vercel (Hobby) $0 $0 100GB bandwidth/mo
Astro on Netlify (Free) $0 $0 100GB bandwidth/mo
Astro on Cloudflare Pages (Free) $0 $0 Unlimited bandwidth
Astro on Vercel (Pro) $20 $240 1TB bandwidth, team features

For the vast majority of content sites -- blogs, portfolios, documentation sites, marketing sites -- the free tiers on Vercel, Netlify, or Cloudflare Pages are more than sufficient. You're serving static files from a global CDN. A site getting 100,000 pageviews per month will barely scratch the free tier limits.

Let me spell out the math. If you're paying $50/month for managed WordPress hosting, that's $600/year. Over three years, that's $1,800. Add premium plugin licenses (Yoast Premium at $99/year, Elementor Pro at $59/year, WP Rocket at $59/year), and you're looking at $800-$1,000 annually for a content site that could be hosted for free.

The migration itself is a one-time cost. If you handle it yourself, it's your time. If you hire a team like ours through our headless CMS development services, the ROI typically breaks even within 6-12 months just from hosting savings alone -- not counting the performance and SEO improvements. Check our pricing page for specifics.

But What About Dynamic Features?

The most common objection: "My WordPress site has contact forms, search, comments, and e-commerce."

  • Contact forms: Formspree (free tier: 50 submissions/month), Formspark, or a simple serverless function
  • Search: Pagefind (free, runs entirely client-side, built for static sites) -- it's incredible
  • Comments: Giscus (free, GitHub-backed), or Disqus if you must
  • E-commerce: Snipcart, Shopify Lite, or Stripe Checkout
  • Newsletter signups: Direct API calls to ConvertKit, Mailchimp, or Buttondown

None of these require a server. None of them add meaningful cost.

FAQ

How long does WordPress to Astro migration take?

For a typical blog with 50-200 posts, expect 1-2 weeks of part-time work if you're doing it yourself. The content conversion is usually a day's work with a good script. Building the Astro layouts and components takes 2-4 days depending on design complexity. Testing, redirect setup, and deployment take another 1-2 days. For larger sites (500+ posts) or sites with complex custom post types and plugin dependencies, budget 3-6 weeks. If you'd rather hand it off, reach out to our team -- we typically complete migrations in 2-4 weeks.

Can Astro handle a WordPress blog with 1000+ posts?

Absolutely. I've migrated sites with over 3,000 posts and build times stayed under 2 minutes. Content collections are optimized for large datasets, and since everything compiles to static HTML, there's no runtime performance penalty regardless of content volume. The build step is the only place where scale matters, and Astro's build performance is excellent -- significantly faster than Gatsby or Next.js for static content at scale.

Does Astro support WordPress comments?

Not natively, and honestly that's a feature, not a bug. WordPress comments are a spam magnet that requires constant moderation. For Astro sites, the best options are: Giscus (uses GitHub Discussions -- free, no tracking, great for developer audiences), Disqus (the old standby, has ads on free tier), Commento (privacy-focused, $5/month), or a custom solution using a database like Turso or Supabase with an Astro API route. If you want to preserve existing WordPress comments, export them and display them as static content, then use one of these services for new comments going forward.

Is Astro faster than WordPress?

It's not even close. A static Astro site will outperform even the most optimized WordPress setup because it eliminates the fundamental bottleneck: server-side processing. WordPress has to execute PHP, query a database, assemble the page, and send it -- for every request (unless cached). Astro pre-builds everything. Your visitors receive pre-rendered HTML directly from a CDN edge node near them. Typical improvements are 80-90% faster load times and 60-80 point Lighthouse score improvements.

What happens to my WordPress admin and WYSIWYG editor?

You lose the WordPress admin panel. For most developers, that's a relief. You'll edit markdown files directly, which is faster and more portable. If you've got non-technical content editors who need a visual interface, consider the headless approach: keep WordPress running as a content backend and use Astro as the frontend. Or pair Astro with a headless CMS like Sanity, Contentful, Storyblok, or Keystatic (which has a GitHub-based editor that's genuinely nice). Our headless CMS development services can help you pick the right content backend for your team.

Will my Google rankings drop after migrating?

They shouldn't, and in most cases they improve -- but only if you handle redirects correctly. Every old URL must either still work or 301-redirect to its new equivalent. Submit your new sitemap to Google Search Console the same day you launch. Monitor the Index Coverage report for the first 30 days. I've seen sites gain 15-30% organic traffic within 2-3 months of migration purely from Core Web Vitals improvements, since Google uses page experience signals as a ranking factor.

Can I keep WordPress as a headless CMS and use Astro for the frontend?

Yes, and this is a great middle-ground approach. You keep the WordPress admin and editor experience but ditch the PHP frontend entirely. Astro fetches content from the WordPress REST API (or WPGraphQL) at build time and generates static pages. You still need to host WordPress somewhere, so you won't save on hosting costs, but you get all the frontend performance benefits. For teams that are heavily invested in the WordPress editing workflow, this is often the pragmatic choice.

Do I need to know React or any JavaScript framework to use Astro?

No. Astro components use a .astro file format that looks like HTML with a frontmatter script block. If you can write HTML and CSS, you can build an Astro site. JavaScript frameworks (React, Vue, Svelte) are optional -- you'd only bring them in if you need interactive client-side components like a search widget, a form with validation, or an image carousel. For a blog or marketing site, you can build the entire thing without touching React.