Marcos Ramírez BETA
Infraestructura de Cloudflare Workers sobre fondo oscuro con red de nodos distribuidos y código de configuración

De Cloudflare Pages a Workers con Astro: la guerra real

· ⏱ 10+ min lectura

Por qué una web estática dejó de serlo

marcosramirez.dev arrancó como un sitio completamente estático: Astro compilando HTML/CSS/JS en build time, desplegado en Cloudflare Pages en segundos. Sin servidor. Sin SSR. Sin complicaciones. Duró una semana. Antes de entrar en materia, un dato de contexto: Cloudflare adquirió Astro a principios de 2026. En teoría, eso debería significar una integración perfecta entre el framework y la plataforma de despliegue. En la práctica, cuando hice esta migración, la documentación de Astro con Cloudflare Workers en modo SSR era penosa: desactualizada, con ejemplos para versiones antiguas y con lagunas importantes sobre comportamientos específicos del adaptador. Lo que sí funcionó fue el asistente de Inteligencia Artificial de Cloudflare. No el de documentación general, sino el integrado en el panel de desarrolladores, que conoce el estado actual del runtime, las versiones del adaptador y los comportamientos específicos de Workers. Le describí el error exacto, el wrangler.jsonc que tenía y la versión del adaptador. Me devolvió la config correcta en un mensaje. Lo que dos horas de documentación y GitHub issues no resolvieron, el asistente lo resolvió en dos minutos. Apunte mental para el futuro: cuando la documentación falla, el asistente integrado de la plataforma vale más que veinte posts de Stack Overflow. En este caso, literalmente. En cuanto añadí un agente de voz a la web (del que hablo en detalle en el post De VAPI a Retell: la migración que se llevó media arquitectura) necesité una ruta de API en el servidor para generar tokens de acceso de forma segura. Y ahí empezó la guerra.

El problema: rutas de API en Astro estático

Astro en modo estático (output: 'static', el valor por defecto) no tiene servidor. Todo se compila durante el build y el resultado son archivos en disco. Perfecto para sitios de contenido. Inútil para endpoints que necesitan ejecutar código en runtime, como leer variables de entorno secretas y hacer peticiones a APIs externas. La solución teórica es sencilla: cambias a output: 'server' en la configuración de Astro y añades un adaptador para el proveedor de despliegue. En mi caso, @astrojs/cloudflare:

// astro.config.mjs
import cloudflare from '@astrojs/cloudflare'
export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    platformProxy: {
      enabled: true
    }
  }),
  // ...
})

La teoría funciona. La práctica fue otra historia.

Intento 1: Pages + wrangler.json

Añadí el adaptador, cambié a output: 'server' y desplegué en Pages. La ruta /api/retell/create-call/ devolvía 404. El problema: Cloudflare Pages con SSR requiere que el worker generado por Astro se despliegue correctamente como función de Pages. Eso se controla con un fichero wrangler.json (o wrangler.toml) que le indica a Pages dónde están los assets del cliente y dónde está el worker SSR:

{
  "name": "marcosramirez-dev",
  "compatibility_date": "2026-05-07",
  "pages_build_output_dir": "./dist/client"
}

Deploy. Mismo 404. Seguimos.

Intento 2: wrangler.json con ASSETS binding

La documentación de Cloudflare sugería añadir un binding ASSETS para que el worker SSR pueda servir los archivos estáticos:

{
  "name": "marcosramirez-dev",
  "compatibility_date": "2026-05-07",
  "assets": {
    "binding": "ASSETS",
    "directory": "./dist/client"
  }
}

Error en el deploy: ASSETS is a reserved binding name”. Cloudflare Pages usa ese nombre internamente y no se puede redefinir. Punto.

Intento 3: wrangler.toml + .assetsignore

Cambio de formato, de wrangler.json a wrangler.toml. Añado un fichero public/.assetsignore para evitar que los worker files se suban como assets estáticos de Pages:

_worker.js
_routes.json
name = "marcosramirez-dev"
compatibility_date = "2026-05-07"
pages_build_output_dir = "./dist/client"

Deploy. Esta vez no hay error de binding. Pero la ruta de API sigue sin responder correctamente. Los logs de Pages muestran que el worker se ejecuta, pero la variable de entorno RETELL_API_KEY no llega al worker.

Intento 4: downgrade del adaptador

La versión de @astrojs/cloudflare que había instalado era la 13.5.0. Varios issues de GitHub apuntaban a problemas de compatibilidad con Pages en versiones recientes. Probé con la 11.2.0:

npm install @astrojs/cloudflare@11.2.0

Diferente error. El build petaba por incompatibilidad de versiones entre el adaptador y Astro 6.3. Deshaciendo el downgrade.

Intento 5: output hybrid

Mientras seguía buscando, encontré referencias a output: 'hybrid' en la documentación de Astro. Un modo que pre-renderiza todo por defecto y solo ejecuta SSR en las páginas que lo pidan explícitamente con prerender = false. Parecía la solución perfecta: la mayoría del sitio estático, solo la ruta de la API en SSR.

export default defineConfig({
  output: 'hybrid', // ← spoiler: no existe en Astro 6
  adapter: cloudflare({ ... })
})

Error inmediato en el build: output: 'hybrid' fue eliminado en Astro 6. Lo que antes era hybrid se consigue ahora con output: 'server' más export const prerender = true en las páginas que quieres estáticas. La documentación que encontré era de Astro 4 o 5. Deshaciendo.

La decisión: olvidar Pages, ir a Workers

Llevaba día y medio depurando configuraciones de Pages. El patrón era evidente: Pages funciona bien para sitios estáticos, pero en cuanto introduces SSR real con secrets de runtime, el comportamiento se vuelve impredecible dependiendo de versiones y configuraciones específicas. Hay otro motivo para no seguir peleando con Pages: Cloudflare ha anunciado oficialmente que Pages y Workers convergen en una sola plataforma. Ya escribí sobre eso cuando vi que Pages desaparecía del panel del dashboard. Migrar a Workers no era solo la solución al problema de SSR, era también el movimiento correcto a largo plazo. Cloudflare Workers es el runtime subyacente. Pages es una abstracción encima de Workers que simplifica el despliegue, pero esa abstracción tiene límites. Cuando los límites me impiden hacer lo que necesito, la respuesta es quitar la abstracción. La migración a Workers requería:

  1. Cambiar el script de deploy: de wrangler pages deploy a wrangler deploy
  2. Nueva configuración wrangler.jsonc para Workers (distinta de Pages)
  3. Los secrets de entorno van en Workers, no en Pages
  4. El dominio custom se configura diferente
// wrangler.jsonc — configuración final para Workers
{
  "name": "marcosramirez-dev",
  "compatibility_date": "2026-05-08",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "binding": "ASSETS",
    "directory": "./dist"
  },
  "observability": {
    "enabled": true
  }
}

Nota importante: en Workers, ASSETS sí es un binding válido. En Pages no. Esa es una de las diferencias que no están bien documentadas en ningún sitio. El directory apunta a ./dist (el output completo de Astro), no a ./dist/client. Workers sirve los estáticos desde ahí directamente.

// package.json — scripts actualizados
{
  "preview": "wrangler dev",
  "deploy": "wrangler deploy"
}

En astro.config.mjs, output: 'server' ya estaba. El adaptador de Cloudflare funciona igual para Pages y para Workers.

El resultado: funciona

La primera vez que deployé en Workers, la ruta de API respondió correctamente. La API key llegaba desde los secrets de Workers. El token se generaba. El agente de voz iniciaba la llamada. El endpoint es un fichero TypeScript estándar de Astro con prerender = false para forzar que esa ruta sea SSR aunque el resto del sitio pudiera ser estático:

// src/pages/api/retell/create-call.ts
export const prerender = false
import type { APIRoute } from 'astro'
export const POST: APIRoute = async ({ request, env }) => {
  const RETELL_API_KEY = import.meta.env.RETELL_API_KEY
    || (env && (env as any).RETELL_API_KEY)
  // genera el token de acceso efímero desde el servidor
  const response = await fetch('https://api.retellai.com/v2/create-web-call', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${RETELL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ agent_id: agentId })
  })
  const data = await response.json()
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  })
}

La API key nunca llega al navegador. El cliente recibe solo el token efímero.

La guerra de los trailing slashes

Con Workers pensé que ya estaba resuelto todo. Entonces empezaron los trailing slashes. Cloudflare Workers (y Astro con trailingSlash: 'always') es estricto con las URLs. /api/retell/create-call y /api/retell/create-call/ son rutas distintas. El fetch del componente del cliente apuntaba a la versión sin barra, la ruta estaba definida con barra. 404. La solución era añadir la barra final en el fetch del cliente. Simple. Pero encontrarlo costó tiempo porque el error no era obvio: Workers devolvía un redirect 308 silencioso en lugar de un error claro.

Lo que no deberías hacer

Resumen de las configuraciones que probé y no funcionaron:

  • wrangler.toml con pages_build_output_dir para Pages SSR: el worker no recibía los secrets
  • assets.binding: "ASSETS" en Pages: nombre reservado, el deploy falla
  • @astrojs/cloudflare@11.2.0 con Astro 6.3: incompatible
  • ❌ Campo main en wrangler.jsonc: causa error de build con el adaptador de Astro
  • ❌ Campo routes en wrangler.jsonc: conflicto con el dominio custom configurado en el dashboard de Cloudflare
  • output: 'hybrid' en astro.config.mjs: eliminado en Astro 6, la documentación que lo menciona es de versiones anteriores

Lo que sí funciona

Astro 6.3 + @astrojs/cloudflare 13.5.0 + output: 'server' + wrangler deploy

Si partes de cero con Astro y necesitas SSR en Cloudflare, ve directamente a Workers. Evita Pages si vas a tener rutas de API con secrets de entorno. La documentación de Cloudflare no es mala, pero la interacción entre el adaptador de Astro y Pages en modo SSR tiene esquinas oscuras que solo encuentras a golpes.

Preguntas frecuentes

Preguntas frecuentes

¿Cuándo usar Cloudflare Pages y cuándo Workers?

Pages si tu sitio es estático o casi estático (Astro sin SSR, Next.js con export estático). Workers si necesitas SSR real, rutas de API con secrets de runtime, o lógica de servidor que varía por petición. El límite es borroso y la documentación no lo deja claro, pero en la práctica: si añades un adaptador SSR a Astro para Cloudflare, ve a Workers desde el principio.

¿Qué es el binding ASSETS en Workers?

Es como Workers accede a los archivos estáticos del sitio (HTML, CSS, JS) durante la ejecución. En modo Workers, los assets se suben junto al worker y se sirven desde ese binding. En Pages, el binding ASSETS es interno y no se puede redefinir — de ahí el error “reserved binding name” si intentas configurarlo en el wrangler.json de Pages.

¿Funciona wrangler dev para desarrollo local con SSR?

Sí. Con wrangler dev en lugar de astro dev puedes probar el comportamiento SSR localmente, incluyendo los secrets del fichero .dev.vars. El inconveniente es que el hot reload es más lento que el dev server de Astro. Para desarrollo del frontend uso astro dev, para probar las rutas de API uso wrangler dev.

¿Cómo paso los secrets a Workers?

Con el CLI de wrangler: wrangler secret put NOMBRE_VARIABLE. Te pide el valor por stdin, nunca lo escribe a disco. Para desarrollo local, crea un fichero .dev.vars en la raíz del proyecto (está en .gitignore por defecto) con el mismo formato que un .env.


Compártelo si te ha resultado útil. Si lo necesitas a nivel profesional en tu empresa, puedo ayudarte. ¿Has peleado con Astro y Cloudflare en modo SSR? Cuéntame cómo lo resolviste. Y… ¡hasta aquí por hoy!

Artículos relacionados

Terminal en Windows con comandos Git mostrando un renombrado de archivo con cambio de case

Git en Windows: renombrar un archivo cambiando solo el case

En Windows, cambiar solo el case de un nombre de archivo con Git no funciona con un simple mv ni con git mv directo. El sistema de archivos es case-insensitive y Git tiene core.ignorecase=true por defecto, así que simplemente ignora el cambio. La solución es un renombrado en dos pasos: primero a un nombre temporal y luego al nombre final. Te explico por qué ocurre, cómo solucionarlo y cuándo te vas a encontrar con este problema más de lo que crees.

06:30 9 min Marcos Ramírez Lucía
Panel de Cloudflare con la sección Workers & Pages mostrando Pages escondida bajo el flujo de Workers

Cloudflare Pages está desapareciendo (y Workers sale ganando)

Cloudflare ha anunciado oficialmente que Pages y Workers se fusionan en una sola plataforma, y el panel ya refleja esa estrategia: Pages aparece enterrada como un enlace pequeño al final de una pantalla orientada a Workers. No es un fallo de UX. Es una decisión de negocio. Te explico qué significa para los sitios estáticos, cuánto te puede costar a futuro y por qué Pages sigue siendo la mejor opción para hosting estático mientras exista.

06:30 8 min Marcos Ramírez Lucía