Caso Real: Cómo resolvimos los Ghost 404 en Cloudflare Pages
TL;DR
- Ghost 404: CF Pages reporta deploy “success” pero las páginas nuevas devuelven 404. La causa:
_routes.jsonausente para static exports y una branch de producción con nombre legacy. - trailingSlash: En Astro + CF Pages, las URLs sin trailing slash devuelven 404. Fix:
trailingSlash: 'always'en la config. - SSL flexible: Llevaba 3 años en modo “flexible”, un riesgo silencioso para SEO y seguridad. Fix: cambiar a “full” via API.
- Schema.org: Incompleto en comerenasturias.es — queda como roadmap de alta prioridad.
- Deploy duplicado: Ambos sitios estaban conectados a Vercel y CF Pages simultáneamente. Cada push = 2 builds innecesarios. Fix: eliminar Vercel.
Contexto
Cloudflare Pages se ha convertido en una alternativa seria a Vercel para hosting de sitios estáticos. Es más barato, tiene edge networking de primera mano y su integración con GitHub es decente. Pero migrar un sitio de Vercel a CF Pages no es tan simple como cambiar el deploy target — especialmente si tu framework genera static exports y no un server.
En este artículo cuento lo que nos pasó al migrar dos sitios reales a CF Pages: comerenasturias.es (Next.js con static export) y gpt-diffusion.com (Astro SSG). Ninguno de los problemas que encontramos está bien documentado en un solo sitio. Aquí están los cinco que tuvimos que resolver, con las configs exactas.
Problema 1: Ghost 404 en Next.js (comerenasturias.es)
Los síntomas
Hacemos push, CF Pages ejecuta el build, la API devuelve "status": "success". Abre la URL: 404. Tanto en el custom domain (comerenasturias.es/fiestas/nueva-fiesta) como en el .pages.dev preview. Las páginas antiguas de /fiestas/ funcionan. Las nuevas, no.
El tipo de 404 que te vuelve loco porque todo dice que está bien.
Lo que encontré
Dos causas apiladas:
Causa 1: _routes.json ausente. Cuando usas Next.js con output: 'export', el build genera HTML estático en out/. CF Pages necesita un archivo _routes.json en public/ para saber qué rutas sirven como archivos estáticos y cuáles pasan a Functions. Sin él, el router de CF Pages no maneja correctamente las rutas dinámicas del tipo /fiestas/[slug].
// public/_routes.json
{
"version": 1,
"include": ["/*"],
"exclude": []
}
Sí, algo tan simple como eso. Sin ese archivo, CF Pages no sabía que /fiestas/algo debía resolverse a /fiestas/algo/index.html.
Causa 2: Branch de producción con nombre legacy. La branch de producción estaba llamada vercel-deploy. No main, no production: vercel-deploy. Esto generaba confusión al revisar la configuración del proyecto en CF Pages — la UI muestra el branch de producción, y si no coincide con tu flujo de trabajo, acabas deployando desde la branch equivocada sin darte cuenta.
Las soluciones
Migración completa (0812b89):
- Branch
vercel-deploy→maincomo source de producción - Eliminadas las dependencias de Vercel:
@vercel/analytics,@vercel/speed-insights,vercel.json - Analytics sustituido por Cloudflare Web Analytics beacon inline
Fix de routing (baa98e8):
- Añadido
public/_routes.jsoncon el include wildcard
Config equivalente a vercel.json:
public/_headerspara cache headerspublic/_redirectspara redirects
CI simplificado (4e30df2):
- Build command limpio:
npm run build - Output directory:
out
Problema 2: trailingSlash en Astro (gpt-diffusion.com)
Los síntomas
/blog/mi-articulo → 404. /blog/mi-articulo.md/ → funciona.
El sufijo .md en la URL es un claro indicador de que Astro está generando los archivos sin el tratamiento de trailing slash que CF Pages espera.
La causa
Astro por defecto no añade trailing slash a las URLs generadas. CF Pages, por su parte, sirve archivos desde el sistema de archivos del edge node — si el archivo es /blog/mi-articulo/index.html, necesitas acceder como /blog/mi-articulo/. Sin el trailing slash, CF Pages no resuelve.
La solución
Una línea en astro.config.mjs:
// astro.config.mjs
export default defineConfig({
output: 'static',
trailingSlash: 'always', // ← esta
// ... resto de config
});
Commit: 6745347. Problema resuelto.
Si vienes de Vercel, esto no te afectaba porque Vercel tiene un fallback que intenta resolver URLs sin trailing slash automáticamente. CF Pages no hace eso. No es un bug, es una diferencia de comportamiento que necesitas configurar.
Problema 3: SSL flexible durante 3 años (gpt-diffusion.com)
Lo que pasó
Al revisar la configuración de SSL de la zona de Cloudflare, descubrí que el modo estaba en “flexible” — configurado así desde abril de 2023.
El modo “flexible” significa que Cloudflare conecta al origin sobre HTTP, no HTTPS. Esto implica:
- Loop de redirects: Si el origin fuerza HTTPS, Cloudflare pide HTTP, el origin redirige a HTTPS, Cloudflare pide HTTP… loop infinito.
- Mixed content warnings: El navegador ve HTTPS en la barra pero el contenido se sirvió parcialmente en HTTP.
- SEO: Google penaliza contenido no servido completamente sobre HTTPS.
La solución
Cambiado a “full” vía la API de Cloudflare:
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/ssl" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "full"}'
Si tu origin tiene un certificado SSL válido (como debería), usa “full (strict)”. Si usas un certificado self-signed o de Cloudflare Origin CA, “full” es suficiente.
Fecha del fix: 1 de mayo de 2026. Tres años con SSL flexible en un sitio de contenido técnico. A veces los problemas más simples son los que más tiempo pasan desapercibidos.
Problema 4: Schema.org incompleto (comerenasturias.es)
Estado actual
Este no es un bug, es una deuda técnica. El sitio tiene tres layouts principales que necesitan Schema markup:
| Layout | Schema necesario | Estado actual |
|---|---|---|
| FiestaLayout | Event | ✅ JSON-LD con address, dates, location |
| RecetaLayout | Recipe | ❌ Sin Schema markup |
| RestauranteLayout | Restaurant | ❌ Sin Schema markup |
Además, faltan tipos FAQ para las páginas de preguntas frecuentes.
La prioridad es alta porque Schema.org directamente afecta los rich snippets en Google Search. Un restaurante con Schema Restaurant completo puede aparecer con estrellas, rango de precios y horarios directamente en los resultados de búsqueda. Sin eso, es un resultado azul genérico.
Estado: Roadmap pendiente. No incluyo configs aquí porque aún no están implementadas — no voy a inventar soluciones que no he probado.
Problema 5: Deploy duplicado Vercel + CF Pages
El problema
Ambos sitios (comerenasturias.es y gpt-diffusion.com) estaban conectados simultáneamente a Vercel y a CF Pages. Cada push a la branch de producción trigger dos builds: uno en Vercel y otro en CF Pages. Dos builds por cada cambio, dos facturas de compute, dos sitios sirviendo el mismo contenido.
La solución
Eliminar Vercel como deploy target y mantener solo CF Pages. Los pasos:
- Desconectar el repositorio de Vercel (Settings → Git → Disconnect)
- Verificar que CF Pages es el único deploy activo
- Eliminar dependencias de Vercel del proyecto (ver Problema 1)
- Actualizar DNS si Vercel manejaba algún record
Esto ya se hizo como parte de la migración descrita en el Problema 1.
Metodología
Todo lo descrito aquí lo debugueamos en producción real, no en staging:
- Herramientas de debugging: CF Pages deploy logs, curl con verbose headers, Chrome DevTools Network tab,
nslookuppara verificar DNS propagation - Commits de referencia:
0812b89,baa98e8,4e30df2(comerenasturias.es),6745347(gpt-diffusion.com) - Tiempo total de resolución: ~2 semanas de trabajo intermitente entre ambos sitios
Lecciones aprendidas
-
CF Pages no es Vercel. Parece obvio, pero las diferencias en routing (trailing slash,
_routes.json) y SSL te muerden si asumes compatibilidad directa. Lee la doc de CF Pages específica para tu framework antes de migrar. -
Nombra bien tus branches. Tener una branch de producción llamada
vercel-deploycuando ya usas CF Pages es una deuda técnica invisible hasta que te cuesta debuguear por qué los deploys no cuadran. -
Revisa tu configuración de SSL. Si migraste a Cloudflare hace tiempo y no tocaste el SSL mode, probablemente esté en “flexible”. Es el default y no es el correcto para la mayoría de casos.
-
Un deploy target, no dos. No necesitas Vercel y CF Pages al mismo tiempo. Elige uno y elimina el otro. Menos builds, menos confusión, menos superficie de error.
-
Los ghost 404 son casi siempre un problema de routing, no de hosting. Si el deploy es “success” y la página es 404, el archivo existe pero el servidor no sabe cómo resolver la URL al archivo.
Conclusión
Migrar a Cloudflare Pages tiene sentido económico y técnico, pero no es plug-and-play. Los tres problemas principales que encontramos — _routes.json, trailingSlash y SSL — son configuraciones de un minuto que pueden costarte horas de debugging si no sabes que existen.
Si estás pensando en migrar, revisa esta checklist antes de hacer el switch: (1) config específica de CF Pages para tu framework, (2) SSL mode en “full” o “full (strict)”, (3) un solo deploy target, (4) trailingSlash configurado si usas Astro o cualquier SSG.
Fuentes: Cloudflare Pages — Framework guides, Cloudflare Pages — _routes.json, Astro — trailingSlash config, Cloudflare SSL modes