WordPress alimenta una porción significativa de la web, pero para blogs y sitios de documentación orientados al contenido, la sobrecarga de una pila completa de PHP/MySQL es cada vez más difícil de justificar. Los generadores de sitios estáticos como Astro ofrecen un rendimiento ultrarrápido, eliminan virtualmente las vulnerabilidades de seguridad y cuestan casi nada de alojar.

Esta guía te lleva a través del proceso completo de migración de un blog de WordPress a Astro — desde exportar tu contenido y convertirlo a Markdown, hasta preservar tu posicionamiento SEO y desplegar en Cloudflare Pages. Esto no es teoría: migramos KnowledgeXchange.xyz (507 publicaciones, 1,427 páginas construidas en 11 segundos) usando exactamente este proceso.

¿Por qué migrar de WordPress a Astro?

Rendimiento

Una página típica de WordPress requiere múltiples ciclos de ejecución PHP, consultas a la base de datos y procesamiento de plugins antes de entregar HTML al navegador. Incluso con caché agresivo, el Time to First Byte (TTFB) raramente baja de 200-400ms.

Astro genera HTML estático en tiempo de compilación. Las páginas cargan en menos de 50ms desde un nodo de borde CDN. No hay procesamiento de servidor, ni base de datos, ni ejecución PHP en tiempo de solicitud.

Seguridad

WordPress es el CMS más atacado en internet. Cada plugin, tema y actualización del núcleo es un vector de ataque potencial. Inyección SQL, cross-site scripting, ataques de fuerza bruta al login y vulnerabilidades de carga de archivos requieren vigilancia constante.

Los archivos HTML estáticos tienen virtualmente cero superficie de ataque. No hay base de datos que inyectar, ni panel de administración que atacar por fuerza bruta, ni PHP que explotar.

Costo

Un sitio WordPress típicamente requiere un VPS ($5-50/mes), hosting administrado ($25-200/mes), o hosting compartido ($3-15/mes). Agrega plugins premium, herramientas de seguridad y servicios CDN.

Un sitio Astro en Cloudflare Pages cuesta $0/mes para la mayoría de los blogs. Incluso los sitios con alto tráfico se mantienen dentro de los límites del plan gratuito porque servir archivos estáticos es increíblemente barato.

Experiencia de desarrollo

Astro utiliza herramientas modernas: componentes escritos en archivos .astro (similares a HTML con JavaScript), Markdown/MDX para contenido, soporte de TypeScript y un sistema de compilación ultrarrápido basado en Vite. Si alguna vez has luchado con la jerarquía de plantillas de WordPress, los hooks de functions.php o los conflictos de plugins, apreciarás la simplicidad.

Entendiendo Astro

Astro es un generador de sitios estáticos moderno diseñado para sitios web orientados al contenido. Las características clave incluyen:

  • Content Collections — Contenido Markdown/MDX con tipado seguro y validación de esquema
  • Island Architecture — Envía cero JavaScript por defecto, hidrata componentes interactivos solo cuando se necesitan
  • Agnóstico de framework — Usa React, Vue, Svelte o componentes HTML simples
  • Optimizaciones integradas — Optimización automática de imágenes, alcance CSS y minificación HTML
  • Compilaciones rápidas — Sistema de compilación basado en Vite que maneja miles de páginas eficientemente

Planificando la migración

Antes de tocar cualquier código, planifica tu estrategia de migración:

Inventario de contenido

  1. Cuenta tus publicaciones, páginas y tipos de publicación personalizados — Conoce el alcance
  2. Cataloga tus medios — Imágenes, PDFs, videos almacenados en wp-content/uploads
  3. Documenta tu estructura de URLs/2024/01/mi-publicacion/ o /categoria/mi-publicacion/ o /mi-publicacion/
  4. Lista las páginas SEO críticas — Páginas de mejor rendimiento que deben mantener su posicionamiento
  5. Identifica las funcionalidades dinámicas — Formularios de contacto, comentarios, búsqueda, comercio electrónico

Mapeo de funcionalidades

Funcionalidad WordPressEquivalente en Astro
Publicaciones/PáginasArchivos Markdown en content collections
Categorías/EtiquetasMetadatos frontmatter + páginas generadas
Imágenes destacadasComponente Image de Astro
ComentariosServicio externo (Giscus, Disqus) o eliminar
Formularios de contactoCloudflare Workers, Formspree o Netlify Forms
BúsquedaPagefind, Fuse.js o Algolia
Feed RSSIntegración @astrojs/rss
SitemapIntegración @astrojs/sitemap
Metadatos SEOComponentes personalizados <head>

Paso 1: Exportar contenido de WordPress

Usando la exportación XML de WordPress

Desde tu panel de administración de WordPress:

  1. Navega a Herramientas > Exportar
  2. Selecciona Todo el contenido
  3. Haz clic en Descargar archivo de exportación

Esto genera un archivo XML WXR (WordPress eXtended RSS) que contiene todas las publicaciones, páginas, comentarios, categorías, etiquetas y referencias de medios.

Usando la API REST (Alternativa)

Para mayor control, puedes obtener contenido a través de la API REST de WordPress:

# Obtener todas las publicaciones (paginadas, 100 por página)
curl -s "https://knowledgexchange.xyz/wp-json/wp/v2/posts?per_page=100&page=1" > posts-page1.json
curl -s "https://knowledgexchange.xyz/wp-json/wp/v2/posts?per_page=100&page=2" > posts-page2.json

# Obtener todas las categorías
curl -s "https://knowledgexchange.xyz/wp-json/wp/v2/categories?per_page=100" > categories.json

# Obtener todas las etiquetas
curl -s "https://knowledgexchange.xyz/wp-json/wp/v2/tags?per_page=100" > tags.json

# Obtener la biblioteca de medios
curl -s "https://knowledgexchange.xyz/wp-json/wp/v2/media?per_page=100&page=1" > media-page1.json

Paso 2: Convertir contenido a Markdown

Script de migración en Node.js

Aquí tienes un script práctico en Node.js que convierte XML de WordPress a archivos Markdown con frontmatter compatible con Astro:

// migrate.mjs
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { XMLParser } from 'fast-xml-parser';
import TurndownService from 'turndown';
import slugify from 'slugify';

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

// Manejar patrones HTML específicos de WordPress
turndown.addRule('preCode', {
  filter: (node) => node.nodeName === 'PRE',
  replacement: (content, node) => {
    const code = node.querySelector('code');
    const lang = code?.className?.replace('language-', '') || '';
    const text = code ? code.textContent : node.textContent;
    return `\n\`\`\`${lang}\n${text.trim()}\n\`\`\`\n`;
  },
});

// Analizar la exportación XML de WordPress
const xml = readFileSync('wordpress-export.xml', 'utf-8');
const parser = new XMLParser({ ignoreAttributes: false });
const data = parser.parse(xml);

const items = data.rss.channel.item;
const posts = Array.isArray(items) ? items : [items];

const outputDir = './src/content/posts';
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });

let count = 0;

for (const post of posts) {
  // Omitir tipos no publicados o que no sean publicaciones si se desea
  const status = post['wp:status'];
  const postType = post['wp:post_type'];
  if (status !== 'publish' || postType !== 'post') continue;

  const title = post.title?.toString() || 'Untitled';
  const date = post['wp:post_date']?.split(' ')[0] || '2024-01-01';
  const content = post['content:encoded'] || '';
  const slug = post['wp:post_name'] || slugify(title, { lower: true, strict: true });

  // Extraer categorías y etiquetas
  const cats = [];
  const tags = [];
  const taxonomy = Array.isArray(post.category) ? post.category : [post.category].filter(Boolean);
  for (const term of taxonomy) {
    if (term?.['@_domain'] === 'category') cats.push(term['#text'] || term);
    if (term?.['@_domain'] === 'post_tag') tags.push(term['#text'] || term);
  }

  // Convertir HTML a Markdown
  const markdown = turndown.turndown(content);

  // Construir frontmatter de Astro
  const frontmatter = [
    '---',
    `title: "${title.replace(/"/g, '\\"')}"`,
    `date: "${date}"`,
    `lastModified: "${date}"`,
    `categories: [${cats.map(c => `"${c}"`).join(', ')}]`,
    `tags: [${tags.map(t => `"${t}"`).join(', ')}]`,
    `author: "JC"`,
    `slug: "${slug}"`,
    `description: "${title.replace(/"/g, '\\"')}"`,
    `lang: "en"`,
    '---',
  ].join('\n');

  const fileContent = `${frontmatter}\n\n${markdown}\n`;
  writeFileSync(`${outputDir}/${slug}.md`, fileContent, 'utf-8');
  count++;
}

console.log(`Migrated ${count} posts to ${outputDir}`);

Instala las dependencias y ejecuta:

npm install fast-xml-parser turndown slugify
node migrate.mjs

Alternativa en Python

Si prefieres Python:

#!/usr/bin/env python3
"""migrate_wp.py -- Convertir exportación XML de WordPress a archivos Markdown de Astro."""

import xml.etree.ElementTree as ET
import os
import re
from html2text import HTML2Text

WP_NS = {
    'wp': 'http://wordpress.org/export/1.2/',
    'content': 'http://purl.org/rss/1.0/modules/content/',
    'excerpt': 'http://wordpress.org/export/1.2/excerpt/',
}

h2t = HTML2Text()
h2t.body_width = 0  # No ajustar líneas
h2t.protect_links = True

tree = ET.parse('wordpress-export.xml')
root = tree.getroot()
channel = root.find('channel')

output_dir = './src/content/posts'
os.makedirs(output_dir, exist_ok=True)

count = 0
for item in channel.findall('item'):
    status = item.find('wp:status', WP_NS).text
    post_type = item.find('wp:post_type', WP_NS).text

    if status != 'publish' or post_type != 'post':
        continue

    title = item.find('title').text or 'Untitled'
    date = item.find('wp:post_date', WP_NS).text.split(' ')[0]
    slug = item.find('wp:post_name', WP_NS).text
    content_html = item.find('content:encoded', WP_NS).text or ''

    # Convertir HTML a Markdown
    markdown = h2t.handle(content_html)

    frontmatter = f"""---
title: "{title.replace('"', '\\"')}"
date: "{date}"
slug: "{slug}"
author: "JC"
lang: "en"
---"""

    filepath = os.path.join(output_dir, f'{slug}.md')
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(f'{frontmatter}\n\n{markdown}\n')
    count += 1

print(f'Migrated {count} posts')

Paso 3: Manejar las imágenes

WordPress almacena las imágenes en directorios wp-content/uploads/YYYY/MM/. Tienes varias opciones:

Opción A: Descargar y almacenar localmente

# Descargar todas las imágenes referenciadas en tus publicaciones
wget --mirror --no-parent --reject "index.html*" \
  https://knowledgexchange.xyz/wp-content/uploads/ \
  -P ./public/wp-content/uploads/

Luego actualiza las referencias de imágenes en tus archivos Markdown:

# Reemplazar URLs absolutas de WordPress con rutas relativas
find ./src/content/posts -name "*.md" -exec sed -i \
  's|https://knowledgexchange.xyz/wp-content/uploads/|/wp-content/uploads/|g' {} +

Opción B: Usar un CDN (Recomendado)

Mantén las imágenes en un CDN externo como Cloudflare R2, AWS S3 o Cloudinary. Esto mantiene tu repositorio pequeño y los tiempos de compilación rápidos:

// En tu componente Astro, referencia imágenes externas
// src/components/PostImage.astro
---
const { src, alt, width, height } = Astro.props;
const cdnUrl = `https://cdn.knowledgexchange.xyz/images/${src}`;
---
<img src={cdnUrl} alt={alt} width={width} height={height} loading="lazy" />

Paso 4: Preservar la estructura de URLs

Mantener tu estructura de URLs existente es crítico para el SEO. Configura el enrutamiento de Astro para que coincida con tu formato de enlaces permanentes de WordPress.

Ruta dinámica para publicaciones

Crea el archivo src/pages/post/[slug].astro:

---
import { getCollection } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';

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

const { post } = Astro.props;
const { Content } = await post.render();
---
<PostLayout frontmatter={post.data}>
  <Content />
</PostLayout>

Manejo de redirecciones para URLs modificadas

Si tu estructura de URLs cambia, crea un archivo _redirects para Cloudflare Pages:

# Redirigir URLs antiguas de WordPress a nuevas URLs de Astro
/2024/01/my-old-post/  /post/my-old-post/  301
/category/linux/        /categories/linux/  301
/tag/ubuntu/            /tags/ubuntu/       301

# Redirigir URLs de administración y feed de WordPress
/wp-admin/*             /                   301
/wp-login.php           /                   301
/feed/                  /rss.xml            301

Paso 5: Configurar Content Collections de Astro

Define tu esquema de contenido en src/content.config.ts:

import { defineCollection, z } from 'astro:content';

const posts = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    date: z.string(),
    lastModified: z.string().optional(),
    categories: z.array(z.string()).default([]),
    tags: z.array(z.string()).default([]),
    author: z.string().default('Juan Carlos'),
    slug: z.string(),
    description: z.string().optional(),
    lang: z.enum(['en', 'es']).default('en'),
    difficulty: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
    featured: z.boolean().default(false),
    readingTime: z.number().optional(),
  }),
});

export const collections = { posts };

Consejo: Astro valida cada archivo Markdown contra este esquema en tiempo de compilación. Si una publicación migrada tiene frontmatter faltante o malformado, la compilación fallará con un mensaje de error claro indicando el archivo y campo exactos.

Paso 6: Diseñar layouts y componentes

Layout de publicación

---
// src/layouts/PostLayout.astro
const { frontmatter } = Astro.props;
---
<html lang={frontmatter.lang || 'en'}>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{frontmatter.title} | KnowledgeXchange</title>
  <meta name="description" content={frontmatter.description || frontmatter.title} />
  <meta property="og:title" content={frontmatter.title} />
  <meta property="og:description" content={frontmatter.description || frontmatter.title} />
  <meta property="og:type" content="article" />
  <meta property="article:published_time" content={frontmatter.date} />
  <meta property="article:modified_time" content={frontmatter.lastModified || frontmatter.date} />
  <link rel="canonical" href={`https://www.knowledgexchange.xyz/post/${frontmatter.slug}/`} />
</head>
<body>
  <article>
    <header>
      <h1>{frontmatter.title}</h1>
      <time datetime={frontmatter.date}>{frontmatter.date}</time>
      <span>Por {frontmatter.author}</span>
      {frontmatter.readingTime && <span>{frontmatter.readingTime} min de lectura</span>}
    </header>
    <div class="content">
      <slot />
    </div>
    <footer>
      <div class="categories">
        {frontmatter.categories?.map((cat: string) => (
          <a href={`/categories/${cat.toLowerCase()}/`}>{cat}</a>
        ))}
      </div>
      <div class="tags">
        {frontmatter.tags?.map((tag: string) => (
          <a href={`/tags/${tag.toLowerCase()}/`}>#{tag}</a>
        ))}
      </div>
    </footer>
  </article>
</body>
</html>

Paso 7: Preservación SEO

Sitemap

Instala la integración de sitemap:

npx astro add sitemap

Configura en astro.config.mjs:

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

export default defineConfig({
  site: 'https://www.knowledgexchange.xyz',
  integrations: [sitemap()],
});

Feed RSS

npm install @astrojs/rss

Crea src/pages/rss.xml.js:

import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('posts');
  return rss({
    title: 'KnowledgeXchange',
    description: 'Technology articles and tutorials',
    site: context.site,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: new Date(post.data.date),
      description: post.data.description || '',
      link: `/post/${post.data.slug}/`,
    })),
  });
}

Datos estructurados (JSON-LD)

Agrega datos estructurados a tu layout de publicación para resultados de búsqueda enriquecidos:

<script type="application/ld+json" set:html={JSON.stringify({
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": frontmatter.title,
  "datePublished": frontmatter.date,
  "dateModified": frontmatter.lastModified || frontmatter.date,
  "author": {
    "@type": "Person",
    "name": frontmatter.author
  },
  "publisher": {
    "@type": "Organization",
    "name": "KnowledgeXchange"
  }
})} />

Paso 8: Desplegar en Cloudflare Pages

Conectar tu repositorio

  1. Sube tu proyecto Astro a un repositorio Git (GitHub, GitLab)
  2. Inicia sesión en el Panel de Cloudflare
  3. Navega a Workers & Pages > Crear aplicación > Pages
  4. Conecta tu repositorio Git
  5. Configura los ajustes de compilación:
    • Comando de compilación: npm run build
    • Directorio de salida de compilación: dist
    • Versión de Node.js: 20 (configurar mediante variable de entorno NODE_VERSION=20)

Dominio personalizado

  1. En tu proyecto de Cloudflare Pages, ve a Dominios personalizados
  2. Agrega tu dominio (por ejemplo, www.knowledgexchange.xyz)
  3. Cloudflare aprovisiona automáticamente un certificado SSL y configura el DNS

Rendimiento de compilación

Las compilaciones de Cloudflare Pages son rápidas. Como referencia, nuestra migración de KnowledgeXchange:

  • 507 publicaciones convertidas a entradas de content collection en Markdown
  • 1,427 páginas totales generadas (publicaciones + páginas de categorías + páginas de etiquetas + páginas de índice)
  • Tiempo de compilación: 11 segundos en Cloudflare Pages

Compara esto con WordPress, donde generar versiones en caché de 507 publicaciones podría tomar minutos, y cada carga de página sin caché requería consultas a la base de datos.

La migración de KnowledgeXchange: Un caso de estudio

Cuando migramos KnowledgeXchange.xyz de WordPress a Astro, enfrentamos varios desafíos del mundo real:

Desafío 1: Formato HTML heredado

Muchas publicaciones antiguas de WordPress contenían HTML sin procesar, shortcodes y marcado específico de plugins. Nuestro script de conversión manejó los casos comunes, pero aproximadamente el 15% de las publicaciones requirió limpieza manual — particularmente aquellas con diseños de tablas complejos, iframes incrustados y shortcodes de galería de WordPress.

Lección aprendida: Reserva tiempo para revisión manual. La conversión automatizada te lleva el 85% del camino; el 15% restante necesita atención humana.

Desafío 2: Referencias de imágenes

Nuestras 507 publicaciones referenciaban cientos de imágenes almacenadas en la estructura de directorios wp-content/uploads de WordPress. Elegimos mantener la misma estructura de directorios en la carpeta public/ para minimizar los cambios de URL.

Lección aprendida: Mantén las mismas rutas de URL de imágenes cuando sea posible. Cambiar las URLs de imágenes significa que los motores de búsqueda necesitan reindexarlas, y cualquier sitio externo que enlace a tus imágenes se romperá.

Desafío 3: Contenido multilingüe

KnowledgeXchange tiene contenido tanto en inglés como en español. Usamos el campo lang del frontmatter para categorizar las publicaciones y generar páginas de índice específicas por idioma.

Lección aprendida: Planifica tu estrategia de internacionalización antes de la migración. Las content collections de Astro facilitan el filtrado por idioma, pero la estructura de enrutamiento necesita decidirse por adelantado.

Desafío 4: Preservar el posicionamiento SEO

Creamos reglas de redirección completas para cualquier URL que cambió de formato. Enviamos el nuevo sitemap a Google Search Console inmediatamente después del lanzamiento y monitoreamos la indexación de cerca durante dos semanas.

Lección aprendida: Envía tu sitemap a los motores de búsqueda el mismo día que lanzas. Monitorea Google Search Console para errores 404 y agrega redirecciones a medida que surjan.

Resultados

Después de la migración:

  • Tiempo de carga de página bajó de 1.2s de promedio a menos de 100ms
  • Puntuación de rendimiento Lighthouse pasó de 67 a 99
  • Costo de hosting bajó de $15/mes a $0/mes (plan gratuito de Cloudflare Pages)
  • Incidentes de seguridad pasaron de intentos ocasionales de fuerza bruta a cero
  • Compilación y despliegue toma 30 segundos desde git push hasta el sitio en vivo

Lecciones aprendidas y mejores prácticas

  1. No migres todo de una vez. Comienza con un subconjunto de publicaciones, verifica la salida y luego procesa el archivo completo.

  2. Mantén tu sitio WordPress funcionando durante la migración. Apunta un subdominio como old.knowledgexchange.xyz hacia él como referencia mientras verificas la versión de Astro.

  3. Prueba las redirecciones exhaustivamente. Usa herramientas como Screaming Frog o un script simple para rastrear tu sitemap antiguo y verificar que cada URL se resuelve correctamente en el nuevo sitio.

  4. Preserva la URL de tu feed RSS o redirecciónala. Los suscriptores que dependen de tu feed no deberían perder el acceso.

  5. Configura analíticas desde el primer día en el nuevo sitio para que puedas comparar patrones de tráfico antes y después de la migración.

  6. Usa content collections para seguridad de tipos. La validación de esquema de Astro detecta errores de frontmatter en tiempo de compilación, previniendo páginas rotas en producción.

  7. Haz commit de tus scripts de migración en el repositorio. Podrías necesitar volver a ejecutarlos si descubres problemas semanas después.

Resumen

Migrar de WordPress a Astro es un proyecto significativo, pero los beneficios son sustanciales: tiempos de carga más rápidos, mejor seguridad, costos más bajos y una experiencia de desarrollo moderna. La clave es la planificación metódica, la conversión automatizada de contenido y la preservación cuidadosa del SEO.

Con content collections, Astro te brinda manejo de Markdown con seguridad de tipos que escala a cientos o miles de publicaciones. Combinado con Cloudflare Pages para el alojamiento, obtienes una infraestructura de blog de grado producción que no cuesta nada operar y maneja cualquier cantidad de tráfico.

Para temas relacionados con WordPress, consulta nuestras guías sobre desplegar WordPress en Ubuntu y permisos de archivos de WordPress en Linux.