Este blog es un sitio estático servido desde Workers, y me cuesta lo que cuesta el dominio. En este capítulo explico cómo se sirve cualquier frontend con Static Assets: desde una web estática como esta hasta una SPA con su API o una aplicación full-stack con renderizado en servidor.
Para quién es y qué necesitas
Para quien tiene un frontend (Astro, React, Vue o HTML a mano) y quiere servirlo desde Cloudflare, con o sin API.
Antes de empezar:
- El proyecto del capítulo anterior, o cualquier frontend que genere una carpeta de archivos (
dist/). - Wrangler 4.
Al terminar tendrás tu frontend en Workers con Static Assets, la API en el mismo Worker si la necesitas y respuestas cacheadas donde compense.
Tres formas de servir un frontend
flowchart TB
subgraph A["1 · Estático"]
A1[Petición] --> A2[Static Assets]
end
subgraph B["2 · SPA + API"]
B1[Petición] --> B2{¿/api/*?}
B2 -->|No| B3[Static Assets<br/>index.html]
B2 -->|Sí| B4[Worker]
end
subgraph C["3 · Full-stack"]
C1[Petición] --> C2{¿Archivo?}
C2 -->|Sí| C3[Static Assets]
C2 -->|No| C4[Worker<br/>renderiza la página]
end
| Modelo | Ejemplos | Ejecuta tu código | Coste de las peticiones a páginas |
|---|---|---|---|
| Estático | Blog, documentación, landing | No | Gratis |
| SPA + API | React, Vue o Svelte con una API | Solo en /api/* |
Gratis las páginas; la API se cobra como Worker |
| Full-stack | Astro con SSR, React Router, Next.js con vinext | En las páginas dinámicas | Las páginas dinámicas se cobran como Worker |
Opción 1: sitio estático
Basta con decirle a Wrangler dónde están los archivos. No hace falta main: sin código, ninguna petición ejecuta un Worker.
{ "name": "my-website", "compatibility_date": "2026-11-03", "assets": { "directory": "./dist", "not_found_handling": "404-page", "html_handling": "drop-trailing-slash" }}| Opción | Valores | Qué hace |
|---|---|---|
not_found_handling |
404-page · single-page-application · none |
Qué devolver si el archivo no existe: tu 404.html, el index.html (para SPAs) o un 404 vacío |
html_handling |
auto-trailing-slash · drop-trailing-slash · force-trailing-slash · none |
Cómo tratar /pagina, /pagina/ y /pagina.html |
Para cabeceras y redirecciones, dos archivos en la carpeta de salida:
/_astro/* Cache-Control: public, max-age=31536000, immutable/* X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin/feed /rss.xml 301/old /new 301Opción 2: SPA con su API en el mismo Worker
Una SPA de React o Vue necesita que cualquier ruta devuelva index.html, y la API, que /api/* llegue a tu código. Se consigue con run_worker_first:
{ "name": "notes", "main": "src/index.ts", "compatibility_date": "2026-11-03", "assets": { "directory": "./dist", "binding": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] }, "d1_databases": [ { "binding": "DB", "database_name": "notes", "database_id": "…" } ]}import { Hono } from "hono";
const app = new Hono<{ Bindings: Env }>();
app.get("/api/notes", async (c) => { const { results } = await c.env.DB .prepare("SELECT id, title FROM notes ORDER BY id DESC") .all(); return c.json(results);});
// The Worker only gets /api/*; Static Assets serves the rest without running it.// An API route that does not exist responds 404 as JSON.app.notFound((c) => c.json({ error: "Not found" }, 404));
export default app;Así decide Cloudflare quién atiende cada petición con esta configuración:
flowchart TD
P[Petición] --> R{¿Empieza por /api/?}
R -->|Sí| W[Tu Worker<br/>se cobra]
R -->|No| A{¿Existe el archivo?}
A -->|Sí| F[Static Assets lo sirve<br/>gratis]
A -->|No| I[index.html<br/>modo SPA]
W --> N{¿Existe la ruta?}
N -->|No| E[404 en JSON]
Opción 3: full-stack con un framework
Los frameworks con renderizado en servidor se integran con el plugin de Vite de Cloudflare (@cloudflare/vite-plugin): el servidor de desarrollo de Vite ejecuta tu código en workerd, con los mismos bindings que producción.
| Framework | Cómo se integra | Nota |
|---|---|---|
| Astro | Estático por defecto; para SSR, el adaptador de Cloudflare | Mi opción para webs de contenido |
| React Router v7 | Plantilla de Cloudflare con el plugin de Vite | Buena para aplicaciones con muchas pantallas |
| Next.js | vinext (recomendado por Cloudflare para proyectos nuevos) u OpenNext | Comprueba qué funciones de Next.js necesitas antes de elegir |
| SvelteKit, Nuxt, Hono + JSX | Adaptadores o plantillas de C3 | npm create cloudflare@latest te ofrece la plantilla |
npm create cloudflare@latest my-app -- --framework=react-routerCachear respuestas dinámicas con Workers Cache
Si una ruta dinámica devuelve lo mismo durante un rato (un listado, una página de producto), Workers Cache la sirve sin ejecutar tu código:
{ "cache": { "enabled": true }}app.get("/api/recent", async (c) => { const { results } = await c.env.DB .prepare("SELECT id, title FROM notes ORDER BY created_at DESC LIMIT 10") .all(); return c.json(results, 200, { "Cache-Control": "public, max-age=300, stale-while-revalidate=60", "Cache-Tag": "recent", });});Para borrar esa caché al publicar algo nuevo, importa cache de cloudflare:workers (el c.executionCtx de Hono no lo tipa) y llama a await cache.purge({ tags: ["recent"] }). En local no se simula: la purga solo funciona desplegado.
Desplegar
npm run buildnpx wrangler deployWrangler sube solo los archivos que han cambiado desde el último despliegue.
Cuánto cuesta
Cuánto cuesta al mes
Supuestos: Una SPA con 100.000 visitas al mes (600.000 peticiones a archivos) y 300.000 llamadas a la API
| Concepto | Importe |
|---|---|
| Peticiones a Static AssetsGratis e ilimitadas | 0,00 $ |
| Peticiones a la APIUnas 10.000 al día: caben en el plan gratuito | 0,00 $ |
| Almacenamiento de los archivosSin coste adicional | 0,00 $ |
| Total al mes | 0,00 $ |
Precios consultados el 3 de noviembre de 2026. Pueden cambiar.
Límites de Static Assets
| Límite | Plan gratuito | Plan de pago |
|---|---|---|
| Archivos por versión | 20.000 | Más (consultar el límite vigente) |
| Tamaño máximo por archivo | 25 MiB | 25 MiB |
Para archivos más grandes (vídeos, descargas), usa R2.
Resumen
- Static Assets sirve tu frontend gratis; el Worker solo se ejecuta donde lo necesitas.
- SPA + API en un Worker con
not_found_handling: "single-page-application"yrun_worker_first: ["/api/*"]. - Workers Cache para respuestas dinámicas, sabiendo que cambia la facturación de los estáticos.
- Siguiente capítulo: bases de datos.
Recursos
- Static Assets
- Workers Cache
- Guías de frameworks
- Migrar de Pages a Workers
- Cómo está montado este blog (próximamente)
