DEV Community

Samcorp
Samcorp

Posted on

Migrating 40,000 Pages From WordPress to Astro

Migrating 40,000 Pages From WordPress to Astro
Migrating a small WordPress site to Astro is mostly a development task.

Migrating 40,000 pages is something else entirely.

It becomes a content, URL, SEO, media, routing, and deployment migration—all at once.

Our first plan looked simple:

Export WordPress
      ↓
Convert content
      ↓
Build with Astro
      ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

The real process looked more like this:

Inventory
   ↓
Extract
   ↓
Normalize
   ↓
Transform
   ↓
Generate
   ↓
Validate
   ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

The Astro components were not the hardest part.

Preserving years of WordPress history was.

Note: This is a representative large-scale migration post-mortem, not a customer case study.


The First Mistake: Building Before Inventorying

We started by creating Astro components:

Header.astro
ArticleLayout.astro
Footer.astro
RelatedPosts.astro
Enter fullscreen mode Exit fullscreen mode

The first migrated pages looked perfect.

Then we reached older content.

We found:

  • legacy shortcodes,
  • page-builder markup,
  • inline CSS,
  • broken HTML,
  • old CDN URLs,
  • embedded forms,
  • custom blocks,
  • absolute internal links,
  • obsolete plugin output.

That changed the question.

It was no longer:

Can Astro render WordPress content?

It became:

What has 15 years of WordPress actually stored?


Start With Every Existing URL

The most useful artifact in the entire migration became a URL manifest.

For every public URL, we recorded something like:

{
  "source": "/guides/performance/",
  "target": "/guides/performance/",
  "type": "page"
}
Enter fullscreen mode Exit fullscreen mode

This gave us a clear contract:

Old URL exists
      ↓
New URL exists
      ↓
Same path OR redirect exists
      ↓
Correct status
      ↓
Correct canonical
Enter fullscreen mode Exit fullscreen mode

Without this inventory, there was no reliable way to answer:

Did all 40,000 pages actually migrate?


Treat WordPress Export Like an ETL Pipeline

Forty thousand records are not something you should treat as one giant API response.

WordPress REST API collections are paginated, so extraction needs to happen in batches.

Conceptually:

let page = 1;

while (true) {
  const response = await fetch(
    `${baseUrl}/wp-json/wp/v2/pages?per_page=100&page=${page}`
  );

  const records = await response.json();

  if (!records.length) break;

  await saveBatch(records);

  page++;
}
Enter fullscreen mode Exit fullscreen mode

The important change was adding checkpoints.

Fetch
 ↓
Normalize
 ↓
Save
 ↓
Checkpoint
 ↓
Continue
Enter fullscreen mode Exit fullscreen mode

If batch 287 failed, we restarted from batch 287.

Not from zero.


Do Not Couple Astro Directly to WordPress Data

Instead of passing raw WordPress API responses through the application, we created a normalized content model.

For example:

type MigratedPage = {
  sourceId: number;
  path: string;
  title: string;
  description?: string;
  content: string;
  canonical?: string;
  publishedAt?: string;
};
Enter fullscreen mode Exit fullscreen mode

That gave us:

WordPress
   ↓
Extraction
   ↓
Normalized Content
   ↓
Astro
Enter fullscreen mode Exit fullscreen mode

Now WordPress-specific decisions stayed in the migration layer instead of leaking into every Astro component.


Not Everything Should Become Markdown

We originally considered converting all content to Markdown.

That worked for clean pages.

It failed on years of legacy content.

For example:

[pricing_table product="enterprise"]
Enter fullscreen mode Exit fullscreen mode

or:

<div class="legacy-callout">
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

A blind HTML-to-Markdown conversion could silently remove functionality or structure.

So we used three options:

  • convert clean content,
  • transform known legacy patterns,
  • preserve sanitized HTML when conversion would lose information.

The goal was not format purity.

The goal was content fidelity.


Shortcodes Need Explicit Replacement Rules

WordPress understood old shortcodes because plugins interpreted them.

Astro does not.

We therefore classified them as:

Static

Convert during migration.

Dynamic

Replace with a real Astro component or service.

Dead

Remove or rewrite intentionally.

Unknown shortcodes became migration failures.

That was better than discovering this in production:

[old_widget id="32"]
Enter fullscreen mode Exit fullscreen mode

Preserve URLs Unless You Have a Good Reason Not To

A framework migration is already risky.

We did not want to combine it with a complete URL redesign.

If WordPress used:

/guides/astro-migration/
Enter fullscreen mode Exit fullscreen mode

Astro normally kept:

/guides/astro-migration/
Enter fullscreen mode Exit fullscreen mode

When a URL genuinely changed, the redirect was stored as migration data:

{
  "from": "/old-guide/",
  "to": "/guides/new-guide/",
  "status": 301
}
Enter fullscreen mode Exit fullscreen mode

We also standardized:

  • trailing slashes,
  • canonical URLs,
  • internal links,
  • sitemap URLs,
  • redirect destinations.

Routing was not merely a frontend decision.

It was an SEO requirement.


Media Was More Than /uploads

Images existed in more places than expected:

  • page HTML,
  • featured images,
  • metadata,
  • srcset,
  • custom fields,
  • CSS,
  • old CDN URLs.

So assets received their own manifest:

{
  "source": "https://old.example.com/uploads/hero.jpg",
  "destination": "/media/hero.jpg"
}
Enter fullscreen mode Exit fullscreen mode

This allowed us to detect:

  • missing files,
  • old WordPress URLs,
  • failed rewrites,
  • broken image references.

At 40,000 pages, guessing whether every asset migrated was not acceptable.


40,000 Static Pages Change Build Economics

Astro can generate large numbers of static routes.

But static pages are not free to build.

Every generated page requires work.

At this size, scalable web development also means thinking about build times, routing strategy, caching, and deployment economics—not only frontend rendering.

So we had to ask:

Does every page need to be regenerated on every deployment?

The answer depends on the site.

Possible approaches include:

Fully static
Enter fullscreen mode Exit fullscreen mode

or:

Static important routes
+
on-demand rendering where appropriate
Enter fullscreen mode Exit fullscreen mode

The key lesson was simple:

Fast runtime performance does not automatically mean fast builds.

Build time became an architecture constraint.


SEO Metadata Is Content Too

Moving the visible article was not enough.

We also needed to preserve:

  • titles,
  • meta descriptions,
  • canonical URLs,
  • publication dates,
  • authors,
  • structured data,
  • social metadata where needed.

A page could render perfectly and still become an SEO migration failure.

That made metadata part of our normalized content model rather than an afterthought.

A large migration needs an SEO-first web architecture so clean URLs, metadata, structured data, redirects, and crawlability survive the platform change.


Validation Became More Important Than Conversion

Eventually, most of our confidence came from automated validation.

For every expected URL, we checked:

Route exists?
      ↓
Correct status?
      ↓
Title present?
      ↓
Canonical correct?
      ↓
Content present?
      ↓
Images valid?
      ↓
Internal links valid?
Enter fullscreen mode Exit fullscreen mode

We also made certain errors fail the build:

  • missing expected routes,
  • duplicate slugs,
  • unknown shortcodes,
  • broken internal links,
  • missing canonicals,
  • redirect loops.

Our rule became:

If a machine can prove the migration is broken, a human should not need to discover it after launch.


The Cutover Needed a Delta Migration

A large migration does not happen instantly.

Editors may continue publishing while the migration runs.

That means the original export quickly becomes outdated.

Our cutover flow looked roughly like:

Full migration
      ↓
Validate
      ↓
Sync recent changes
      ↓
Short publishing freeze
      ↓
Final delta sync
      ↓
Deploy
      ↓
Production validation
Enter fullscreen mode Exit fullscreen mode

We also kept WordPress available during the rollback window.

A migration is safer when the previous production system remains recoverable.


What Went Wrong

The biggest mistakes were straightforward.

We started with components instead of inventory

Building UI felt productive, but understanding the existing site was more important.

We underestimated legacy content

Forty thousand pages created over many years do not follow one clean format.

We assumed Markdown should be the destination

Sometimes preserving HTML was safer.

We treated redirects as configuration

They were actually migration data.

We assumed static generation solved every performance problem

Runtime performance and build performance are different concerns.

We manually checked pages instead of validating the system

Automation gave us much more confidence than random visual QA.


What Worked

The strongest decisions were:

  • inventory every public URL,
  • normalize WordPress data before Astro sees it,
  • migrate page types instead of individual pages,
  • preserve URLs whenever possible,
  • track assets separately,
  • validate automatically,
  • fail on unknown legacy content,
  • plan the final content sync early.

The final architecture became:

WordPress
    ↓
Extract
    ↓
Normalize
    ↓
 ┌─────────────┐
 │   Content   │
 │   Assets    │
 │  Redirects  │
 └─────────────┘
       ↓
      Astro
       ↓
   Validation
       ↓
    Production
Enter fullscreen mode Exit fullscreen mode

The Biggest Lesson

The hardest part of a WordPress to Astro migration was not Astro.

It was WordPress history.

The old system represented much more than PHP and a database.

It contained:

40,000 URLs
years of content
SEO history
redirects
media
plugins
shortcodes
custom markup
undocumented assumptions
Enter fullscreen mode Exit fullscreen mode

Astro did not need to reproduce WordPress.

It needed to preserve the parts users and search engines depended on.


Final Takeaway

For a large WordPress to Astro migration, do not start with:

npm create astro@latest
Enter fullscreen mode Exit fullscreen mode

Start with three questions:

What exists today?

What must survive?

How will we prove it survived?
Enter fullscreen mode Exit fullscreen mode

Then build the migration around:

Inventory
   ↓
Extract
   ↓
Normalize
   ↓
Transform
   ↓
Generate
   ↓
Validate
   ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

The framework was the new delivery layer.

The real engineering work was migrating 40,000 content contracts without breaking them.

Top comments (0)