01. Prepara WordPress
WordPress lleva la REST API integrada desde la versión 4.7: no necesitas instalar ningún plugin extra para empezar. Antes de tocar Astro, comprueba que tu WordPress está listo para servir contenido como CMS headless.
Verifica que la REST API está activa
Abre en el navegador https://tu-wordpress.com/wp-json/wp/v2/posts. Si ves un JSON con tus posts, la API funciona. Si ves un error 404, ve a Ajustes → Enlaces permanentes y guarda los cambios: eso regenera las reglas de reescritura.
Configura CORS si es necesario
Si WordPress y Astro van a estar en dominios diferentes durante desarrollo (localhost:4321 → tu-wp.com), necesitas permitir CORS en WordPress. Añade esto en functions.php de tu tema hijo o en un plugin custom:
add_action('rest_api_init', function() { remove_filter('rest_pre_serve_request', 'rest_send_cors_headers'); add_filter('rest_pre_serve_request', function($value) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); return $value; }); });
Access-Control-Allow-Origin: * está bien para desarrollo. En producción, reemplaza el asterisco por el dominio de tu Astro: 'https://tu-dominio.com'.02. Crea el proyecto Astro
Si ya tienes un proyecto Astro, salta al paso 3. Si empiezas desde cero, o si prefieres partir de una base ya configurada, puedes usar el Starter Kit de Culebra Labque ya tiene la integración lista.
# Proyecto nuevo desde cero npm create astro@latest mi-proyecto cd mi-proyecto npm install # O clonar el starter de Culebra Lab (ya tiene todo configurado) git clone https://github.com/DhiviPanda/astro-wp-starter cd astro-wp-starter && npm install
Configura las variables de entorno
Crea un archivo .env en la raíz del proyecto con la URL de tu WordPress. Nunca subas este archivo a GitHub: añádelo a .gitignore.
WP_API_URL=https://cms.tu-dominio.com/wp-json/wp/v2 WP_URL=https://cms.tu-dominio.com
03. Conecta la REST API
El patrón que usamos en todos los starters de Culebra Lab es centralizar todas las llamadas a la REST API de WordPress en un solo archivo de utilidades. Así si cambia la URL o la versión de la API, solo tocas un sitio.
const WP_API = import.meta.env.WP_API_URL; // Tipos básicos export interface WPPost { id: number; slug: string; title: { rendered: string }; excerpt: { rendered: string }; content: { rendered: string }; date: string; _embedded?: { 'wp:featuredmedia'?: Array<{ source_url: string; alt_text: string }>; }; } // Obtener todos los posts export async function getPosts(perPage = 10): Promise<WPPost[]> { const res = await fetch( `${WP_API}/posts?per_page=${perPage}&_embed` ); if (!res.ok) return []; return res.json(); } // Obtener un post por slug export async function getPost(slug: string): Promise<WPPost | null> { const res = await fetch( `${WP_API}/posts?slug=${slug}&_embed` ); if (!res.ok) return null; const posts = await res.json(); return posts[0] ?? null; } // Obtener slugs para rutas estáticas export async function getPostSlugs(): Promise<string[]> { const posts = await getPosts(100); return posts.map(p => p.slug); }
04. Listado de posts
Con las utilidades listas, crear la página de listado es sencillo. El fetch ocurre en el frontmatter de Astro: en build time, no en el navegador. El HTML resultante es completamente estático.
--- import { getPosts } from '../../lib/wordpress'; const posts = await getPosts(12); --- <ul> {posts.map(post => ( <li> <a href={`/blog/${post.slug}/`}> <h2 set:html={post.title.rendered} /> <p set:html={post.excerpt.rendered} /> </a> </li> ))} </ul>
set:html en lugar de {post.title.rendered} para que Astro renderice correctamente las entidades HTML que WordPress incluye en los títulos (comillas tipográficas, ampersands, etc.).05. Página de detalle
Para las páginas individuales necesitas rutas dinámicas. En Astro,getStaticPaths() se ejecuta en build time y genera una ruta estática por cada slug que devuelva WordPress.
--- import { getPost, getPostSlugs } from '../../lib/wordpress'; export async function getStaticPaths() { const slugs = await getPostSlugs(); return slugs.map(slug => ({ params: { slug } })); } const { slug } = Astro.params; const post = await getPost(slug); if (!post) return Astro.redirect('/404'); const featuredImage = post._embedded?.['wp:featuredmedia']?.[0]; --- <article> <h1 set:html={post.title.rendered} /> {featuredImage && ( <img src={featuredImage.source_url} alt={featuredImage.alt_text} width="1200" height="630" /> )} <div set:html={post.content.rendered} /> </article>
06. Imágenes y rendimiento
Las imágenes de WordPress vienen como URLs externas en el JSON de la API. Astro no puede optimizarlas automáticamente como haría con imágenes locales, pero puedes mejorar el rendimiento con estas estrategias:
- Pide el tamaño correcto desde la API: WordPress genera varios tamaños automáticamente. En lugar de pedir
source_url, pidemedia_details.sizes.medium_large.source_urlpara el tamaño que necesitas. - Añade
loading="lazy"en todas las imágenes que no sean above the fold. - Añade
decoding="async"para evitar que el decode de imagen bloquee el hilo principal. - Añade siempre
widthyheight: aunque no sean las dimensiones exactas, ayudan al navegador a reservar el espacio y evitan CLS. - Usa WebP si tu WordPress lo soporta: desde WP 5.8, WordPress genera automáticamente versiones WebP de las imágenes subidas.
const img = post._embedded?.['wp:featuredmedia']?.[0]; const src = img?.media_details?.sizes?.large?.source_url ?? img?.source_url; // En el template: <img src={src} alt={img?.alt_text ?? post.title.rendered} width="1200" height="630" loading="lazy" decoding="async" />
07. Sitemap y SEO
Con @astrojs/sitemap el sitemap se genera automáticamente en el build incluyendo todas las rutas estáticas, incluidas las generadas por getStaticPaths() desde WordPress.
npm install @astrojs/sitemapimport { defineConfig } from 'astro/config'; import sitemap from '@astrojs/sitemap'; export default defineConfig({ site: 'https://tu-dominio.com', integrations: [sitemap()], });
Meta tags SEO en cada página
Para las páginas de detalle, usa los datos de WordPress para generar los meta tags de forma dinámica:
const title = post.title.rendered.replace(/<[^>]+>/g, ''); const desc = post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 155); const canonical = `https://tu-dominio.com/blog/${post.slug}/`; // En el head: <title>{title}</title> <meta name="description" content={desc} /> <link rel="canonical" href={canonical} /> <meta property="og:title" content={title} /> <meta property="og:description" content={desc} /> <meta property="og:url" content={canonical} />
08. Deploy en producción
Astro genera un output estático en /dist: archivos HTML, CSS y JS listos para subir a cualquier hosting. No necesitas Node.js en producción.
# Build de producción npm run build # Preview local antes de subir npm run preview # Subir /dist a tu hosting por FTP # (IONOS, SiteGround, cualquier hosting compartido)
Rebuildear cuando WordPress cambia
Como el output es estático, los cambios en WordPress no se reflejan automáticamente: necesitas rebuildar. Para automatizarlo, añade un webhook en WordPress que dispare el build cuando publiques contenido:
- En Netlify o Vercel: copia la URL del build hook y añádela en WordPress → Ajustes → un plugin como WP Webhooks.
- En hosting compartido (IONOS): programa un cron diario que ejecute el build y suba
/distpor FTP. - Para sitios con poco volumen de publicación, el rebuild manual es perfectamente viable.
Preguntas frecuentes
¿Necesito modificar el panel de WordPress para hacerlo headless?+
No. El panel de administración sigue funcionando exactamente igual. Los editores crean y publican contenido como siempre. El cambio ocurre solo en cómo se sirve ese contenido al visitante: en lugar de que WordPress genere el HTML, lo hace Astro.
¿Qué pasa con los plugins de Elementor o page builders?+
Dejan de ser necesarios para el frontend, porque el frontend ya no lo genera WordPress. Los plugins de backend (SEO, ACF, formularios) siguen funcionando con normalidad desde la REST API.
¿WordPress headless con Astro es mejor que Next.js?+
Depende del caso. Astro genera HTML estático por defecto: cero JS en el cliente, mejor rendimiento para sitios de contenido. Next.js tiene más ecosistema y SSR nativo. Para blogs, webs corporativas y landing pages, Astro gana en velocidad y simplicidad. Para aplicaciones con mucha interactividad, Next.js puede ser más adecuado.
¿Cuánto tarda en notarse la mejora de rendimiento tras la migración?+
El cambio en Lighthouse es inmediato tras el primer build. El impacto en posicionamiento suele notarse en las semanas siguientes, cuando Google rastrea de nuevo el sitio y registra las mejoras en Core Web Vitals.
¿Puedo desplegarlo en hosting compartido como IONOS?+
Sí. Astro genera un output estático en /dist. Sube ese directorio a tu public_html por FTP. El .htaccess incluido en los starters de Culebra Lab activa compresión y caché automáticamente.
¿Cuánto cuesta migrar WordPress a headless con Astro?+
Depende del proyecto. Cris Culebras (Daruma Producciones) hace la migración completa a WordPress headless + Astro desde 2.800€, con el contenido y las URLs migrados sin pérdidas. El starter kit de Culebra Lab es gratuito (licencia MIT) para quien quiera hacerlo por su cuenta.
Recursos relacionados
¿Lo migramos juntos?
Si prefieres que la migración la haga Cris Culebras, con sitemap, SEO técnico y optimización de imágenes incluidos, hablamos.