Saltar al contenido

CrawlForge TeamEquipo de Ingeniería

9 min de lecturaActualizado

Automatización de migración de contenido con CrawlForge

Migrar 500 páginas de WordPress a un CMS headless debería llevar un fin de semana. En realidad, lleva de 3 a 6 semanas, porque alguien tiene que copiar el contenido manualmente, corregir el formato, reenlazar las imágenes y verificar cada página. La migración de contenido es la tarea más temida del desarrollo web, y es casi totalmente automatizable.

CrawlForge extrae todo el contenido de tu sitio de forma programática: páginas, metadatos, imágenes, enlaces y estructura de los documentos. Esta guía te muestra cómo crear un pipeline de migración que mueve miles de páginas entre dos plataformas cualesquiera en horas, no en semanas.

Tabla de contenidos

Por qué la migración de contenido es dolorosa

La migración de contenido falla por tres razones:

  1. Volumen: incluso el sitio de una pequeña empresa tiene de 200 a 500 páginas. Cada página necesita que se preserven su contenido, metadatos, imágenes y enlaces internos
  2. Desajuste de formato: el CMS de origen y el de destino usan modelos de contenido distintos (bloques de WordPress vs. MDX vs. texto enriquecido de Contentful)
  3. Complejidad oculta: shortcodes, medios incrustados, campos personalizados, redirecciones: todo necesita su tratamiento

La migración manual cuesta aproximadamente entre 5 y 15 USD por página en tiempo de analista. Una migración de 500 páginas a 10 USD por página cuesta 5,000 USD solo en mano de obra. La migración automatizada con CrawlForge cuesta menos de 50 USD en credits.

Método de migraciónCosto (500 páginas)TiempoTasa de error
Copiar y pegar manual5,000-7,500 USD3-6 semanas5-10 %
Semiautomatizado (scripts)2,000-3,000 USD1-2 semanas2-5 %
Pipeline de CrawlForge20-50 USD2-4 horas<1 %

Visión general de la arquitectura

El pipeline de migración usa cinco herramientas de CrawlForge:

EtapaHerramientaCreditsPropósito
Inventariomap_site3Descubrir todas las páginas y su estructura
Extracción de contenidoextract_content2Extraer contenido limpio de cada página
Captura de metadatosextract_metadata1Preservar las etiquetas SEO y los datos de Open Graph
Mapeo de enlacesextract_links1Mapear enlaces internos para reescribirlos
Procesamiento por lotesbatch_scrape5Procesar cientos de páginas de forma eficiente

Paso 1: inventaría tu sitio de origen

Mapea cada página de tu sitio de origen, incluidas las páginas que tal vez no estén en la navegación.

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 || {},
  };
}

Paso 2: extrae contenido y metadatos

Extrae contenido limpio y todos los metadatos de cada página, preservando la estructura de encabezados y el formato.

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,
  };
}

Paso 3: preserva la estructura del documento

Para sitios grandes, usa el procesamiento por lotes y crea un mapa de enlaces completo para reescribir las URL.

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 };
}

Paso 4: transforma para la plataforma de destino

Reescribe los enlaces internos y transforma el contenido para que coincida con el formato de tu CMS de destino.

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 };
  }
}

Paso 5: valida la migración

Después de transformar, verifica que cada página se haya migrado correctamente.

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,
  };
}

Análisis de costo en credits

Para la migración de un sitio web de 500 páginas:

OperaciónHerramientaCreditsCantidadSubtotal
Inventario del sitiomap_site313
Extracción de contenidoextract_content2500 páginas1,000
Extracción de metadatosextract_metadata1500 páginas500
Extracción de enlacesextract_links1500 páginas500
Total2,003 credits

Una migración completa de 500 páginas cuesta unos 2,000 credits. El plan Hobby ($19/mes, 5,000 credits) lo maneja con holgura. Para sitios más grandes (más de 1,000 páginas), el plan Professional ($99/mes, 100,000 credits) ofrece margen de sobra.

Resultados y beneficios

La migración de contenido automatizada ofrece:

  • Velocidad: migra 500 páginas en 2-4 horas en lugar de 3-6 semanas
  • Precisión: sin errores de copiar y pegar, sin formato roto ni páginas omitidas
  • Integridad: cada página, cada meta etiqueta y cada enlace interno capturados
  • Ahorro de costos: más de 5,000 USD de mano de obra manual reemplazados por 19-99 USD en credits de la herramienta

CrawlForge es ideal para migraciones de contenido en las que necesitas preservar el valor SEO: meta etiquetas, enlaces internos, URL canónicas y la estructura del contenido se transfieren limpiamente.

Preguntas frecuentes

¿Puede CrawlForge manejar los shortcodes de WordPress?

La herramienta extract_content de CrawlForge procesa el HTML renderizado, no el código fuente bruto de WordPress. Los shortcodes ya están expandidos a su HTML de salida cuando CrawlForge los extrae. Obtienes el contenido renderizado, que es lo que quieres para la migración.

¿Y las imágenes y los archivos multimedia?

CrawlForge extrae las URL de las imágenes del contenido. Necesitarás un paso aparte para descargar y volver a alojar las imágenes en tu plataforma de destino. La herramienta fetch_url (1 credit) puede descargar archivos multimedia individuales.

¿Cómo manejo las redirecciones después de la migración?

El urlMap generado en el Paso 3 te da un mapeo completo de URL antigua a slug nuevo. Expórtalo como un mapa de redirecciones para tu plataforma de hosting (vercel.json de Vercel, _redirects de Netlify o la configuración de nginx).


Migra tu sitio este fin de semana. Empieza gratis con 1,000 credits: suficiente para migrar más de 250 páginas. Sin tarjeta de crédito.

Recursos relacionados:

Pruébalo tú mismo — sin necesidad de registrarte

Explora las 31 herramientas de scraping y extracción de CrawlForge en el playground y luego empieza gratis con 1,000 credits.

1,000 credits gratis • Por única vez • No se requiere tarjeta de crédito

Etiquetas

  • content-migration
  • cms
  • web-scraping
  • automation
  • wordpress
  • headless-cms
  • mcp

Sobre el autor

CrawlForge Team

Equipo de Ingeniería

Construimos el MCP server de web scraping más completo. Creamos herramientas que ayudan a los desarrolladores a extraer, analizar y transformar datos web para aplicaciones de IA.

Newsletter

Mantente al día con los últimos artículos

Recibe tutoriales, novedades del producto y consejos de web scraping en tu bandeja de entrada.

Sin spam. Cancela tu suscripción cuando quieras.

FAQ

Preguntas frecuentes

01¿Cuánto cuesta la automatización de la migración de contenido con CrawlForge?

Una migración completa de 500 páginas cuesta unos 2,000 credits (map_site + extract_content + extract_metadata + extract_links). El plan Hobby a $19/mes con 5,000 credits lo maneja con holgura. Para sitios más grandes de más de 1,000 páginas, el plan Professional a $99/mes ofrece margen de sobra.

02¿Puede CrawlForge manejar los shortcodes de WordPress durante la migración?

La herramienta extract_content de CrawlForge procesa el HTML renderizado, no el código fuente bruto de WordPress. Los shortcodes ya están expandidos a su HTML de salida cuando CrawlForge los extrae. Obtienes el contenido renderizado, que es lo que quieres para la migración.

03¿Y las imágenes y los archivos multimedia durante la migración de contenido?

CrawlForge extrae las URL de las imágenes del contenido. Necesitarás un paso aparte para descargar y volver a alojar las imágenes en tu plataforma de destino. La herramienta fetch_url (1 credit) puede descargar archivos multimedia individuales.

04¿Cómo manejo las redirecciones después de una migración de contenido?

El urlMap generado durante la migración te da un mapeo completo de URL antigua a slug nuevo. Expórtalo como un mapa de redirecciones para tu plataforma de hosting -- vercel.json de Vercel, _redirects de Netlify o la configuración de nginx -- para que el valor SEO se transfiera limpiamente.

Sigue leyendo

Artículos relacionados

Casos de uso

10m

Construye un motor de lead enrichment con CrawlForge

Enriquece leads de ventas con datos de empresa, stacks tecnológicos y datos de contacto automáticamente. Haz scraping de datos públicos de negocio para cualificar leads y priorizar el contacto.