Descubre WordPress Headless: cómo impulsa tu flexibilidad, rendimiento y seguridad. Compara la API REST vs GraphQL y aprende a optimizar costes de hosting.
Durante años, WordPress ha sido el CMS dominante con más del 43% de la web construida sobre él. Sin embargo, la arquitectura tradicional monolítica está siendo desafiada por un nuevo paradigma: WordPress Headless. Esta aproximación separa el backend de gestión de contenido del frontend de presentación, desbloqueando posibilidades que antes eran impensables.
¿Qué es WordPress Headless?
WordPress Headless (o "decapitado") es una arquitectura donde WordPress funciona exclusivamente como backend de gestión de contenido, exponiendo sus datos a través de APIs, mientras que el frontend se construye con tecnologías modernas como React, Vue, Next.js o Svelte.
Arquitectura Tradicional vs Headless
WordPress Tradicional:
Usuario → WordPress (PHP/Temas) → Base de Datos
↓
HTML renderizado
WordPress Headless:
Administrador → WordPress Backend → Base de Datos
↓ (API REST/GraphQL)
Usuario → Frontend (React/Next.js/Vue)
Las Ventajas que Cambian el Juego
1. Rendimiento Excepcional
Con headless, tu frontend puede ser una aplicación estática o generada del lado del servidor (SSR), lo que significa tiempos de carga ultrarrápidos.
Comparativa real:
- WordPress tradicional: 2-4 segundos de carga inicial
- WordPress Headless con Next.js: 0.3-0.8 segundos de carga inicial
- Static Site Generation: prácticamente instantáneo
Ejemplo con Next.js:
// pages/posts/[slug].js
import { getPostBySlug, getAllPosts } from '../../lib/api';
export default function Post({ post }) {
return (
<article>
<h1>{post.title.rendered}</h1>
<div
dangerouslySetInnerHTML={{ __html: post.content.rendered }}
/>
</article>
);
}
// Static Site Generation - se genera en build time
export async function getStaticPaths() {
const posts = await getAllPosts();
return {
paths: posts.map((post) => ({
params: { slug: post.slug }
})),
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug);
return {
props: { post },
revalidate: 60 // ISR: regenera cada 60 segundos
};
}
Biblioteca de API para WordPress:
// lib/api.js
const WP_API_URL = process.env.WORDPRESS_API_URL;
async function fetchAPI(endpoint) {
const response = await fetch(`${WP_API_URL}/wp-json/wp/v2/${endpoint}`);
if (!response.ok) {
throw new Error('Error fetching data from WordPress');
}
return response.json();
}
export async function getAllPosts() {
const data = await fetchAPI('posts?_embed&per_page=100');
return data;
}
export async function getPostBySlug(slug) {
const posts = await fetchAPI(`posts?slug=${slug}&_embed`);
return posts[0];
}
export async function getCategories() {
const data = await fetchAPI('categories');
return data;
}
export async function getPostsByCategory(categoryId) {
const data = await fetchAPI(`posts?categories=${categoryId}&_embed`);
return data;
}
2. Seguridad Reforzada
Al separar el backend del frontend, reduces significativamente la superficie de ataque.
Beneficios de seguridad:
- Backend oculto: Tu instalación de WordPress no está expuesta públicamente
- Sin plugins en frontend: Eliminas vulnerabilidades de temas y plugins en la capa de presentación
- Autenticación JWT: Control granular de acceso a la API
- Rate limiting: Proteges tu API de abusos
Implementación de autenticación JWT:
// En tu tema/plugin de WordPress
add_action('rest_api_init', function() {
register_rest_route('custom/v1', '/secure-data', [
'methods' => 'GET',
'callback' => 'get_secure_data',
'permission_callback' => 'check_jwt_auth'
]);
});
function check_jwt_auth() {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($auth_header)) {
return new WP_Error('no_auth', 'No authorization token provided', ['status' => 401]);
}
// Validar JWT token
$token = str_replace('Bearer ', '', $auth_header);
$secret = JWT_SECRET_KEY;
try {
$decoded = JWT::decode($token, $secret, ['HS256']);
return true;
} catch (Exception $e) {
return new WP_Error('invalid_token', 'Invalid token', ['status' => 401]);
}
}
function get_secure_data() {
return [
'message' => 'This is secure data',
'data' => ['sensitive' => 'information']
];
}
Configuración de seguridad en frontend:
// lib/auth.js
export async function getSecureData() {
const token = localStorage.getItem('jwt_token');
const response = await fetch(
`${process.env.WORDPRESS_API_URL}/wp-json/custom/v1/secure-data`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error('Unauthorized access');
}
return response.json();
}
3. Flexibilidad Tecnológica Total
Construye tu frontend con la tecnología que prefieras o que mejor se adapte a tu proyecto.
Stack moderno típico:
# Next.js + WordPress
npx create-next-app@latest my-headless-wp
cd my-headless-wp
npm install axios swr
Componente React con SWR para datos en tiempo real:
// components/LatestPosts.js
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function LatestPosts() {
const { data, error, isLoading } = useSWR(
`${process.env.NEXT_PUBLIC_WP_API}/wp-json/wp/v2/posts?per_page=5&_embed`,
fetcher,
{
refreshInterval: 30000, // Refresca cada 30 segundos
revalidateOnFocus: false
}
);
if (error) return <div>Error al cargar posts</div>;
if (isLoading) return <PostsSkeleton />;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{data.map((post) => (
<article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden">
{post._embedded?.['wp:featuredmedia']?.[0] && (
<img
src={post._embedded['wp:featuredmedia'][0].source_url}
alt={post.title.rendered}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h2
className="text-xl font-bold mb-2"
dangerouslySetInnerHTML={{ __html: post.title.rendered }}
/>
<div
className="text-gray-600 line-clamp-3"
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
/>
<a
href={`/posts/${post.slug}`}
className="mt-4 inline-block text-blue-600 hover:underline"
>
Leer más →
</a>
</div>
</article>
))}
</div>
);
}
function PostsSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-gray-200 animate-pulse rounded-lg h-80" />
))}
</div>
);
}
4. Escalabilidad y Omnicanalidad
El mismo contenido puede alimentar múltiples frontends simultáneamente.
Ejemplo de arquitectura multicanal:
WordPress Backend (API)
↓
┌────┴────┬─────────┬──────────┐
↓ ↓ ↓ ↓
Web App Mobile App Kiosco Smart TV
(Next.js) (React N.) (Vue) (React)
Shared API client para múltiples plataformas:
// shared/wpClient.js
class WordPressClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.cache = new Map();
}
async get(endpoint, options = {}) {
const cacheKey = `${endpoint}-${JSON.stringify(options)}`;
// Retornar de cache si existe y no ha expirado
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < 60000) { // 1 minuto
return cached.data;
}
}
const params = new URLSearchParams(options).toString();
const url = `${this.baseURL}/wp-json/wp/v2/${endpoint}${params ? `?${params}` : ''}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// Cachear resultado
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
console.error('Error fetching from WordPress:', error);
throw error;
}
}
async post(endpoint, data) {
const url = `${this.baseURL}/wp-json/wp/v2/${endpoint}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
clearCache() {
this.cache.clear();
}
}
export default new WordPressClient(process.env.WORDPRESS_API_URL);
REST API vs GraphQL: La Gran Decisión
WordPress REST API (Nativa)
Ventajas:
- Incluida por defecto en WordPress
- No requiere plugins adicionales
- Documentación extensa
- Amplia compatibilidad
Desventajas:
- Over-fetching: recibes datos que no necesitas
- Múltiples requests para datos relacionados
- Menos flexible para consultas complejas
Ejemplo de uso:
// Obtener un post con su autor y categorías requiere múltiples llamadas
async function getPostWithRelations(slug) {
// 1. Obtener el post
const post = await fetch(
`${WP_API}/posts?slug=${slug}`
).then(r => r.json());
// 2. Obtener el autor
const author = await fetch(
`${WP_API}/users/${post[0].author}`
).then(r => r.json());
// 3. Obtener categorías
const categories = await Promise.all(
post[0].categories.map(id =>
fetch(`${WP_API}/categories/${id}`).then(r => r.json())
)
);
return {
...post[0],
author,
categories
};
}
WPGraphQL: La Alternativa Moderna
Ventajas:
- Obtén exactamente los datos que necesitas
- Una sola query para datos relacionados
- Type-safe con TypeScript
- Mejor performance con menos requests
Desventajas:
- Requiere plugin WPGraphQL
- Curva de aprendizaje mayor
- Configuración inicial más compleja
Instalación:
# Instalar plugin WPGraphQL en WordPress
# Luego en tu proyecto Next.js:
npm install @apollo/client graphql
Configuración de Apollo Client:
// lib/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: `${process.env.WORDPRESS_API_URL}/graphql`,
}),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
export default client;
Query compleja con GraphQL:
// lib/queries.js
import { gql } from '@apollo/client';
export const GET_POST_WITH_RELATIONS = gql`
query GetPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
excerpt
slug
featuredImage {
node {
sourceUrl
altText
mediaDetails {
width
height
}
}
}
author {
node {
name
avatar {
url
}
description
}
}
categories {
nodes {
name
slug
}
}
tags {
nodes {
name
slug
}
}
seo {
title
metaDesc
opengraphImage {
sourceUrl
}
}
}
}
`;
Uso en componente:
// pages/posts/[slug].js
import { useQuery } from '@apollo/client';
import { GET_POST_WITH_RELATIONS } from '../../lib/queries';
import client from '../../lib/apollo-client';
export default function Post({ slug }) {
const { data, loading, error } = useQuery(GET_POST_WITH_RELATIONS, {
variables: { slug }
});
if (loading) return <PostSkeleton />;
if (error) return <ErrorMessage error={error} />;
const { post } = data;
return (
<article className="max-w-4xl mx-auto px-4 py-8">
{post.featuredImage && (
<img
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText}
className="w-full h-96 object-cover rounded-lg mb-8"
/>
)}
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-gray-600">
<img
src={post.author.node.avatar.url}
alt={post.author.node.name}
className="w-12 h-12 rounded-full"
/>
<div>
<p className="font-medium text-gray-900">{post.author.node.name}</p>
<time className="text-sm">{new Date(post.date).toLocaleDateString()}</time>
</div>
</div>
<div className="flex gap-2 mt-4">
{post.categories.nodes.map((cat) => (
<span
key={cat.slug}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{cat.name}
</span>
))}
</div>
</header>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
export async function getStaticProps({ params }) {
const { data } = await client.query({
query: GET_POST_WITH_RELATIONS,
variables: { slug: params.slug }
});
return {
props: {
slug: params.slug
},
revalidate: 60
};
}
export async function getStaticPaths() {
const { data } = await client.query({
query: gql`
query GetAllPosts {
posts(first: 1000) {
nodes {
slug
}
}
}
`
});
return {
paths: data.posts.nodes.map((post) => ({
params: { slug: post.slug }
})),
fallback: 'blocking'
};
}
Comparativa de Performance
Escenario: Obtener 10 posts con autor, categorías e imagen destacada
| Métrica | REST API | GraphQL |
|---|---|---|
| Requests | 31 (1 posts + 10 autores + 10 imágenes + 10 categorías) | 1 |
| Datos transferidos | ~450KB | ~180KB |
| Tiempo de carga | 2.8s | 0.9s |
| Over-fetching | Alto | Ninguno |
Optimización de Costes de Hosting
Una de las mayores ventajas de WordPress Headless es la optimización radical de costes.
Arquitectura de Hosting Eficiente
Backend (WordPress):
- Servidor pequeño (1GB RAM es suficiente)
- Solo accesible para administradores
- Sin necesidad de CDN para WordPress
Frontend:
- Hosting estático gratuito o muy económico
- CDN global incluido
- Escalado automático
Opciones de Hosting por Presupuesto
Opción 1: Ultra-Económica ($5-15/mes)
Backend: DigitalOcean Droplet ($6/mes)
Frontend: Vercel/Netlify (Gratis hasta 100GB bandwidth)
Total: ~$6/mes para 100K visitantes/mes
Configuración de DigitalOcean:
# SSH a tu droplet
ssh root@tu-ip
# Instalar WordPress con Docker
docker run -d \
--name wordpress \
-p 8080:80 \
-e WORDPRESS_DB_HOST=db \
-e WORDPRESS_DB_USER=wordpress \
-e WORDPRESS_DB_PASSWORD=tu_password \
-e WORDPRESS_DB_NAME=wordpress \
--link mysql:db \
wordpress:latest
# Nginx reverse proxy con SSL
apt install nginx certbot python3-certbot-nginx
certbot --nginx -d wp-backend.tudominio.com
nginx.conf:
server {
listen 443 ssl http2;
server_name wp-backend.tudominio.com;
ssl_certificate /etc/letsencrypt/live/wp-backend.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wp-backend.tudominio.com/privkey.pem;
# Restringir acceso a IPs específicas (opcional)
# allow 203.0.113.0/24;
# deny all;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Rate limiting para API
location ~ ^/wp-json/ {
limit_req zone=api burst=10 nodelay;
proxy_pass http://localhost:8080;
}
}
# Rate limit zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
Opción 2: Profesional ($30-50/mes)
Backend: Kinsta/WP Engine (~$30/mes)
Frontend: Vercel Pro ($20/mes)
Total: ~$50/mes para 1M visitantes/mes
Opción 3: Enterprise ($200+/mes)
Backend: AWS/GCP con auto-scaling
Frontend: Vercel Enterprise
CDN: Cloudflare Enterprise
Total: $200-500/mes para 10M+ visitantes/mes
Cálculo de Ahorro Real
Sitio con 500K pageviews/mes:
WordPress Tradicional:
- WP Engine/Kinsta: $100-300/mes
- Optimización constante necesaria
- Costes de plugins premium
WordPress Headless:
- Backend (DigitalOcean): $12/mes
- Frontend (Vercel): $0-20/mes
- Ahorro: $70-270/mes (70-90% menos)
Estrategias de Caché Avanzadas
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/posts/:slug',
permanent: true,
},
];
},
images: {
domains: ['tu-wordpress.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
},
};
Implementar ISR (Incremental Static Regeneration):
export async function getStaticProps() {
const posts = await getAllPosts();
return {
props: { posts },
revalidate: 60, // Regenera la página cada 60 segundos si hay requests
};
}
Desafíos y Soluciones
1. SEO y Meta Tags Dinámicos
Problema: Los motores de búsqueda necesitan meta tags correctos.
Solución con Next.js:
import Head from 'next/head';
export default function Post({ post }) {
return (
<>
<Head>
<title>{post.seo?.title || post.title}</title>
<meta name="description" content={post.seo?.metaDesc} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.featuredImage?.sourceUrl} />
<meta property="og:url" content={`https://tudominio.com/posts/${post.slug}`} />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href={`https://tudominio.com/posts/${post.slug}`} />
</Head>
{/* Contenido del post */}
</>
);
}
2. Formularios y Interactividad
Problema: WordPress maneja formularios tradicionalmente con PHP.
Solución: Usa la API REST para enviar datos.
// components/ContactForm.js
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [status, setStatus] = useState('idle');
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('loading');
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
setStatus('success');
setFormData({ name: '', email: '', message: '' });
} else {
setStatus('error');
}
} catch (error) {
setStatus('error');
}
};
return (
<form onSubmit={handleSubmit} className="max-w-lg mx-auto space-y-4">
<input
type="text"
placeholder="Nombre"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
className="w-full px-4 py-2 border rounded-lg"
/>
<textarea
placeholder="Mensaje"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
required
rows="5"
className="w-full px-4 py-2 border rounded-lg"
/>
<button
type="submit"
disabled={status === 'loading'}
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{status === 'loading' ? 'Enviando...' : 'Enviar'}
</button>
{status === 'success' && (
<p className="text-green-600 text-center">¡Mensaje enviado exitosamente!</p>
)}
{status === 'error' && (
<p className="text-red-600 text-center">Error al enviar. Intenta nuevamente.</p>
)}
</form>
);
}
API Route en Next.js:
// pages/api/contact.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { name, email, message } = req.body;
// Enviar a WordPress usando Contact Form 7 API o custom endpoint
try {
const wpResponse = await fetch(
`${process.env.WORDPRESS_API_URL}/wp-json/contact-form-7/v1/contact-forms/123/feedback`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'your-name': name,
'your-email': email,
'your-message': message,
}),
}
);
if (wpResponse.ok) {
res.status(200).json({ message: 'Sent successfully' });
} else {
res.status(500).json({ message: 'Error sending' });
}
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
}
3. Preview de Contenido
Problema: Los editores necesitan ver cambios antes de publicar.
Solución: Implementa un sistema de preview.
// pages/api/preview.js
export default async function preview(req, res) {
const { secret, id } = req.query;
// Verificar el secret token
if (secret !== process.env.WORDPRESS_PREVIEW_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
// Obtener el post draft
const post = await getPostPreview(id);
if (!post) {
return res.status(401).json({ message: 'Post not found' });
}
// Habilitar preview mode
res.setPreviewData({
post: {
id: post.id,
slug: post.slug,
},
});
// Redirigir al post
res.redirect(`/posts/${post.slug}`);
}
Casos de Uso Ideales para Headless
✅ Cuándo SÍ usar WordPress Headless:
- Sitios de alto tráfico donde el rendimiento es crítico
- Aplicaciones multi-plataforma (web + móvil + apps)
- Equipos con experiencia en React/Vue y desarrollo moderno
- Proyectos que requieren UI/UX personalizada sin limitaciones de temas
- Sitios con múltiples idiomas y audiencias globales
- Startups tech que priorizan escalabilidad desde el inicio
❌ Cuándo NO usar WordPress Headless:
- Sitios simples tipo blog personal o pequeña empresa
- Equipos sin conocimientos de JavaScript moderno
- Presupuestos muy limitados con necesidad de lanzamiento rápido
- Dependencia de plugins de WordPress con funcionalidad específica
- Clientes que necesitan editar todo sin conocimientos técnicos
El Futuro: Hybrid Approaches
La tendencia actual apunta a enfoques híbridos que combinan lo mejor de ambos mundos.
Ejemplo con Next.js:
- Landing pages y marketing: Full SSG (estático)
- Blog: ISR (regeneración incremental)
- Dashboard de usuario: CSR (client-side rendering)
- Admin panel: WordPress tradicional
Top comments (0)