CrawlForge
HomePlaygroundUse CasesIntegrationsPricingDocumentationBlog
Content Migration Automation with CrawlForge
Use Cases
Back to Blog
Use Cases

Content Migration Automation with CrawlForge

C
CrawlForge Team
Engineering Team
April 12, 2026
9 min read
Updated April 14, 2026

On this page

Quick Answer

A WordPress-to-headless-CMS migration that normally takes 3-6 weeks collapses to hours when you extract content programmatically. CrawlForge's crawl_deep, extract_content, and extract_metadata tools pull pages, images, links, and structure so you can transform and reload into any target platform without manual copy-paste.

Migrating 500 pages from WordPress to a headless CMS should take a weekend. In reality, it takes 3-6 weeks -- because someone has to manually copy content, fix formatting, re-link images, and verify every page. Content migration is the single most dreaded task in web development, and it is almost entirely automatable.

CrawlForge extracts your entire site's content programmatically: pages, metadata, images, links, and document structure. This guide shows you how to build a migration pipeline that moves thousands of pages between any two platforms in hours, not weeks.

Table of Contents

  • Why Content Migration Is Painful
  • Architecture Overview
  • Step 1: Inventory Your Source Site
  • Step 2: Extract Content and Metadata
  • Step 3: Preserve Document Structure
  • Step 4: Transform for the Target Platform
  • Step 5: Validate the Migration
  • Credit Cost Analysis
  • Results and Benefits
  • Frequently Asked Questions

Why Content Migration Is Painful

Content migration fails for three reasons:

  1. Volume: Even a small business site has 200-500 pages. Each page needs content, metadata, images, and internal links preserved
  2. Format mismatch: Source and target CMS use different content models (WordPress blocks vs. MDX vs. Contentful rich text)
  3. Hidden complexity: Shortcodes, embedded media, custom fields, redirects -- all need handling

Manual migration costs approximately $5-15 per page in analyst time. A 500-page migration at $10/page costs $5,000 in labor alone. Automated migration with CrawlForge costs under $50 in credits.

Migration MethodCost (500 pages)TimeError Rate
Manual copy-paste$5,000-7,5003-6 weeks5-10%
Semi-automated (scripts)$2,000-3,0001-2 weeks2-5%
CrawlForge pipeline$20-502-4 hours<1%

Architecture Overview

The migration pipeline uses five CrawlForge tools:

StageToolCreditsPurpose
Inventorymap_site3Discover all pages and their structure
Content extractionextract_content2Pull clean content from each page
Metadata captureextract_metadata1Preserve SEO tags and Open Graph data
Link mappingextract_links1Map internal links for rewriting
Batch processingbatch_scrape5Process hundreds of pages efficiently

Step 1: Inventory Your Source Site

Map every page on your source site, including pages that may not be in the navigation.

Typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

const client = new Client({
  name: 'content-migrator',
  version: '1.0.0',
});

interface SiteInventory {
  pages: Array<{
    url: string;
    path: string;
    depth: number;
    type: 'page' | 'post' | 'category' | 'media' | 'other';
  }>;
  totalPages: number;
  pathGroups: Record<string, number>;
}

async function inventorySite(siteUrl: string): Promise<SiteInventory> {
  const mapResult = await client.callTool({
    name: 'map_site',
    arguments: {
      url: siteUrl,
      max_urls: 2000,
      include_sitemap: true,
      group_by_path: true,
      include_metadata: true,
    },
  });

  const result = JSON.parse(mapResult.content[0].text);

  // Classify pages by URL pattern
  const pages = result.urls.map((url: string) => {
    const path = new URL(url).pathname;
    let type: 'page' | 'post' | 'category' | 'media' | 'other' = 'other';

    if (path.match(/\/\d{4}\/\d{2}\//)) type = 'post';       // WordPress date URLs
    else if (path.match(/\/blog\//)) type = 'post';
    else if (path.match(/\/category\//)) type = 'category';
    else if (path.match(/\.(pdf|doc|jpg|png)/)) type = 'media';
    else type = 'page';

    return { url, path, depth: path.split('/').length - 1, type };
  });

  return {
    pages,
    totalPages: pages.length,
    pathGroups: result.groupedByPath || {},
  };
}

Step 2: Extract Content and Metadata

Extract clean content and all metadata from every page, preserving heading structure and formatting.

Typescript
interface MigratedPage {
  sourceUrl: string;
  slug: string;
  title: string;
  content: string;        // Clean markdown or HTML
  excerpt: string;
  metadata: {
    title: string;
    description: string;
    ogImage: string;
    canonical: string;
    publishedAt: string;
    author: string;
  };
  internalLinks: string[];
  images: string[];
}

async function extractPage(url: string): Promise<MigratedPage> {
  // Extract clean content (readability-optimized)
  const contentResult = await client.callTool({
    name: 'extract_content',
    arguments: { url },
  });

  const content = JSON.parse(contentResult.content[0].text);

  // Extract metadata separately for completeness
  const metaResult = await client.callTool({
    name: 'extract_metadata',
    arguments: { url },
  });

  const meta = JSON.parse(metaResult.content[0].text);

  // Extract links for internal link mapping
  const linksResult = await client.callTool({
    name: 'extract_links',
    arguments: { url },
  });

  const links = JSON.parse(linksResult.content[0].text);
  const internalLinks = (links.internal || []).map(
    (l: { href: string }) => l.href
  );

  // Extract image URLs from content
  const imageRegex = /!\[.*?\]\((.*?)\)/g;
  const images: string[] = [];
  let match;
  while ((match = imageRegex.exec(content.content)) !== null) {
    images.push(match[1]);
  }

  const slug = new URL(url).pathname
    .replace(/^\//,'')
    .replace(/\/$/,'')
    .replace(/\//g, '-');

  return {
    sourceUrl: url,
    slug,
    title: meta.title || content.title || '',
    content: content.content,
    excerpt: meta.description || content.excerpt || '',
    metadata: {
      title: meta.title || '',
      description: meta.description || '',
      ogImage: meta.ogImage || '',
      canonical: meta.canonical || url,
      publishedAt: meta.publishedDate || '',
      author: meta.author || '',
    },
    internalLinks,
    images,
  };
}

Step 3: Preserve Document Structure

For large sites, use batch processing and build a complete link map for URL rewriting.

Typescript
type UrlMap = Map<string, string>; // old URL -> new slug

async function batchExtract(
  inventory: SiteInventory
): Promise<{ pages: MigratedPage[]; urlMap: UrlMap }> {
  const pages: MigratedPage[] = [];
  const urlMap: UrlMap = new Map();

  // Process in batches of 20
  const batchSize = 20;
  const urls = inventory.pages
    .filter(p => p.type === 'page' || p.type === 'post')
    .map(p => p.url);

  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    console.log(
      `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(urls.length / batchSize)}`
    );

    // Extract each page in the batch
    for (const url of batch) {
      try {
        const page = await extractPage(url);
        pages.push(page);
        urlMap.set(url, page.slug);
      } catch (error) {
        console.error(`Failed to extract ${url}:`, error);
      }
    }
  }

  return { pages, urlMap };
}

Step 4: Transform for the Target Platform

Rewrite internal links and transform content to match your target CMS format.

Typescript
function transformForTarget(
  page: MigratedPage,
  urlMap: UrlMap,
  targetFormat: 'mdx' | 'contentful' | 'sanity'
): Record<string, unknown> {
  // Rewrite internal links to new URL structure
  let content = page.content;
  for (const [oldUrl, newSlug] of urlMap) {
    content = content.replace(
      new RegExp(oldUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
      `/${newSlug}`
    );
  }

  switch (targetFormat) {
    case 'mdx':
      return {
        slug: page.slug,
        title: page.title,
        description: page.metadata.description,
        date: page.metadata.publishedAt || new Date().toISOString(),
        content: content, // Already markdown from extract_content
      };

    case 'contentful':
      return {
        fields: {
          title: { 'en-US': page.title },
          slug: { 'en-US': page.slug },
          body: { 'en-US': content },
          description: { 'en-US': page.metadata.description },
          publishedAt: { 'en-US': page.metadata.publishedAt },
        },
        contentType: 'blogPost',
      };

    case 'sanity':
      return {
        _type: 'post',
        title: page.title,
        slug: { current: page.slug },
        body: content,
        excerpt: page.excerpt,
        publishedAt: page.metadata.publishedAt,
      };

    default:
      return { ...page, content };
  }
}

Step 5: Validate the Migration

After transforming, verify that every page was migrated correctly.

Typescript
interface ValidationResult {
  totalPages: number;
  successCount: number;
  failedCount: number;
  warnings: Array<{
    slug: string;
    issue: string;
  }>;
}

function validateMigration(
  original: SiteInventory,
  migrated: MigratedPage[]
): ValidationResult {
  const warnings: Array<{ slug: string; issue: string }> = [];

  for (const page of migrated) {
    // Check for empty content
    if (!page.content || page.content.trim().length < 50) {
      warnings.push({
        slug: page.slug,
        issue: 'Content appears empty or very short',
      });
    }

    // Check for missing metadata
    if (!page.metadata.title) {
      warnings.push({ slug: page.slug, issue: 'Missing title' });
    }
    if (!page.metadata.description) {
      warnings.push({ slug: page.slug, issue: 'Missing meta description' });
    }

    // Check for broken internal links
    for (const link of page.internalLinks) {
      const isLinkMigrated = migrated.some(
        p => p.sourceUrl === link || link.includes(p.slug)
      );
      if (!isLinkMigrated) {
        warnings.push({
          slug: page.slug,
          issue: `Internal link may be broken: ${link}`,
        });
      }
    }
  }

  const contentPages = original.pages.filter(
    p => p.type === 'page' || p.type === 'post'
  );

  return {
    totalPages: contentPages.length,
    successCount: migrated.length,
    failedCount: contentPages.length - migrated.length,
    warnings,
  };
}

Credit Cost Analysis

For a 500-page website migration:

OperationToolCreditsQuantitySubtotal
Site inventorymap_site313
Content extractionextract_content2500 pages1,000
Metadata extractionextract_metadata1500 pages500
Link extractionextract_links1500 pages500
Total2,003 credits

A complete 500-page migration costs about 2,000 credits. The Hobby plan ($19/month, 5,000 credits) handles this with room to spare. For larger sites (1,000+ pages), the Professional plan ($99/month, 50,000 credits) provides plenty of headroom.

Results and Benefits

Automated content migration delivers:

  • Speed: Migrate 500 pages in 2-4 hours instead of 3-6 weeks
  • Accuracy: No copy-paste errors, broken formatting, or missed pages
  • Completeness: Every page, every meta tag, every internal link captured
  • Cost savings: $5,000+ in manual labor replaced by $19-99 in tool credits

CrawlForge is best for content migrations where you need to preserve SEO equity -- meta tags, internal links, canonical URLs, and content structure all transfer cleanly.

Frequently Asked Questions

Can CrawlForge handle WordPress shortcodes?

CrawlForge's extract_content tool processes the rendered HTML, not raw WordPress source. Shortcodes are already expanded to their output HTML when CrawlForge extracts them. You get the rendered content, which is what you want for migration.

What about images and media files?

CrawlForge extracts image URLs from content. You will need a separate step to download and re-host images on your target platform. The fetch_url tool (1 credit) can download individual media files.

How do I handle redirects after migration?

The urlMap generated in Step 3 gives you a complete old-URL-to-new-slug mapping. Export this as a redirect map for your hosting platform (Vercel vercel.json, Netlify _redirects, or nginx config).


Migrate your site this weekend. Start free with 1,000 credits -- enough to migrate 250+ pages. No credit card required.

Related resources:

  • CrawlForge Documentation
  • 18 Web Scraping Tools Overview
  • Use Cases
  • Pricing Plans

Try this yourself — no signup needed

Run any of CrawlForge's 27 scraping and extraction tools in the playground, then start free with 1,000 credits.

1,000 free credits • Refills monthly • No credit card required

Tags

content-migrationcmsweb-scrapingautomationwordpressheadless-cmsmcp

About the Author

C

CrawlForge Team

Engineering Team

Building the most comprehensive web scraping MCP server. We create tools that help developers extract, analyze, and transform web data for AI applications.

Stay updated with the latest insights

Get tutorials, product updates, and web scraping tips delivered to your inbox.

No spam. Unsubscribe anytime.

Put this into practice

Test CrawlForge's tools on any URL — free, no signup.

On this page

Frequently Asked Questions

How much does content migration automation cost with CrawlForge?+

A complete 500-page migration costs about 2,000 credits (map_site + extract_content + extract_metadata + extract_links). The Hobby plan at $19/month with 5,000 credits handles this with room to spare. For larger sites of 1,000+ pages, the Professional plan at $99/month provides plenty of headroom.

Can CrawlForge handle WordPress shortcodes during migration?+

CrawlForge's extract_content tool processes the rendered HTML, not raw WordPress source. Shortcodes are already expanded to their output HTML when CrawlForge extracts them. You get the rendered content, which is what you want for migration.

What about images and media files during content migration?+

CrawlForge extracts image URLs from content. You will need a separate step to download and re-host images on your target platform. The fetch_url tool (1 credit) can download individual media files.

How do I handle redirects after a content migration?+

The urlMap generated during migration gives you a complete old-URL-to-new-slug mapping. Export this as a redirect map for your hosting platform -- Vercel vercel.json, Netlify _redirects, or nginx config -- so SEO equity transfers cleanly.

Related Articles

Build a Research Agent with CrawlForge Deep Research
Use Cases

Build a Research Agent with CrawlForge Deep Research

Create an AI research agent that gathers, verifies, and synthesizes information from dozens of sources in minutes using CrawlForge deep_research.

C
CrawlForge Team
|
Apr 16
|
10m
Build a Lead Enrichment Engine with CrawlForge
Use Cases

Build a Lead Enrichment Engine with CrawlForge

Enrich sales leads with company data, tech stacks, and contact details automatically. Scrape public business data to qualify leads and prioritize outreach.

C
CrawlForge Team
|
Apr 14
|
10m
Real-Time Competitive Intelligence with AI Agents
Use Cases

Real-Time Competitive Intelligence with AI Agents

Build an AI-powered competitive intelligence system using CrawlForge and Claude. Monitor competitors, track changes, and generate strategic insights weekly.

C
CrawlForge Team
|
Apr 8
|
9m

Footer

CrawlForge

Enterprise web scraping for AI Agents. 27 specialized MCP tools designed for modern developers building intelligent systems.

Product

  • Features
  • Playground
  • Pricing
  • Use Cases
  • Integrations
  • Alternatives
  • Changelog

Resources

  • Getting Started
  • API Reference
  • Templates
  • Guides
  • Blog
  • Glossary
  • FAQ
  • Sitemap

Developers

  • MCP Protocol
  • Claude Desktop
  • Cursor IDE
  • LangChain
  • LlamaIndex

Company

  • About
  • Contact
  • Privacy
  • Terms
  • Acceptable Use
  • Cookies

Stay updated

Get the latest updates on new tools and features.

Built with Next.js and MCP protocol

© 2025-2026 CrawlForge. All rights reserved.