Desplegar en Cloudflare

En esta página

Esta guía despliega un sitio EmDash en Cloudflare Workers con D1 como base de datos y R2 para medios. Comienza con una plantilla EmDash Cloudflare o aplica la misma configuración a un sitio Astro existente.

Requisitos previos

  • Una cuenta de Cloudflare
  • Las dependencias del proyecto instaladas
  • Wrangler autenticado con Cloudflare (pnpm wrangler login)

Configurar bindings

Las plantillas de Cloudflare incluyen el punto de entrada completo del Worker y bindings D1 y R2 con nombre. En el primer despliegue, Wrangler crea cada recurso si su nombre configurado aún no existe. Mantén los nombres en wrangler.jsonc; Wrangler reconecta despliegues posteriores a los mismos recursos.

La plantilla usa los siguientes bindings:

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "my-emdash-site",
	"main": "./src/worker.ts",
	"compatibility_date": "2026-02-24",
	"compatibility_flags": ["nodejs_compat"],

	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "my-emdash-site",
		},
	],

	"r2_buckets": [
		{
			"binding": "MEDIA",
			"bucket_name": "my-emdash-media",
		},
	],
	"worker_loaders": [{ "binding": "LOADER" }],
	"triggers": { "crons": ["* * * * *"] },
}

Los nombres DB, MEDIA y LOADER deben coincidir con los adaptadores de EmDash. El Cron Trigger ejecuta publicaciones programadas, tareas de plugins, copias de seguridad y mantenimiento. Consulta Sandbox de plugins si el sitio usa plugins en sandbox.

Configurar EmDash

La siguiente configuración de Astro usa los bindings D1 y R2.

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import emdash from "emdash/astro";
import { d1, r2, sandbox } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // Requerido — la UI de administración es una app React
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
			sandboxRunner: sandbox(),
		}),
	],
});

Si el sitio no usa plugins de marketplace, registry o sandboxed, omite sandboxRunner y el binding LOADER.

Agregar el punto de entrada del Worker

El punto de entrada del Worker conecta Astro con el Cron Trigger y exporta el puente de plugins:

import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

export default {
	...handler,
	scheduled: createScheduledHandler(),
} satisfies ExportedHandler;

El export PluginBridge es inofensivo cuando no hay ningún plugin en sandbox instalado. Consérvalo si el mismo proyecto podría habilitar plugins más adelante.

Para ejecutar mantenimiento general en un horario diferente a cada minuto, pasa la misma expresión Cron a createScheduledHandler({ generalCron: "..." }) y a triggers.crons. Si difieren, el handler registra e ignora el trigger inesperado.

Compilar y desplegar

Compila y despliega el sitio una vez para que Wrangler aprovisione la base de datos D1 y el bucket R2 con nombre. Wrangler usa el login local creado por pnpm wrangler login.

pnpm build
pnpm wrangler deploy

Con el modo de migración auto predeterminado, EmDash aplica las migraciones core pendientes cuando el Worker desplegado recibe su primera solicitud. Usa Gestionar migraciones de base de datos core cuando un pipeline de despliegue deba aplicar migraciones antes de que el nuevo código reciba tráfico o cuando necesites inspeccionar, verificar o recuperar una migración.

Si la base de datos está vacía (sin colecciones) y el asistente de configuración no se ha completado, EmDash también aplica un archivo seed en el primer arranque. El seed se lee en tiempo de compilación desde .emdash/seed.json, la ruta en package.json#emdash.seed, o seed/seed.json — lo que se encuentre primero — y se integra en el bundle. Si no hay ninguno presente, se usa un seed predeterminado incorporado. Los despliegues posteriores contra una base de datos existente dejan su contenido intacto.

Para cambiar el esquema o modelo de contenido de un sitio ya desplegado, consulta Evolucionar un sitio desplegado.

Colocar el Worker cerca de D1

Cloudflare ejecuta un Worker cerca del visitante por defecto. Las solicitudes renderizadas del servidor EmDash hacen varios viajes de ida y vuelta a D1, así que usa Targeted Placement para ejecutar el Worker cerca del D1 primario y hacer esas solicitudes más rápidas.

Wrangler acepta placement.mode: "targeted" con exactamente un selector: region, host o hostname. Selecciona el valor que apunta a la ubicación del D1 primario y agrega el objeto placement resultante a wrangler.jsonc. No habilites réplicas de lectura de D1 con Targeted Placement. Mantén la configuración session de EmDash en su valor predeterminado, "disabled", para que las lecturas y escrituras usen el primario cercano.

Caché de objetos

Para reducir la carga de lectura en D1, cachea los resultados de consultas de contenido y configuración en Cloudflare KV. Las lecturas se sirven desde KV en lugar de consultar la base de datos en cada solicitud:

import { d1, r2, kvCache } from "@emdash-cms/cloudflare";

emdash({
	database: d1({ binding: "DB" }),
	storage: r2({ binding: "MEDIA" }),
	objectCache: kvCache({ binding: "CACHE" }),
}),

Consulta Caché de objetos para la configuración de KV, opciones y comportamiento de invalidación.

Workers Cache

El Workers Cache de Cloudflare coloca una caché edge delante de tu Worker: las solicitudes coincidentes se sirven sin ejecutar tu Worker en absoluto.

Habilitarlo

  1. Usa el proveedor de caché de Cloudflare de Astro para que las reglas de ruta y Astro.cache establezcan headers de caché y la invalidación use cache.purge().

    import { cacheCloudflare } from "@astrojs/cloudflare/cache";
    
    export default defineConfig({
     adapter: cloudflare(),
     cache: {
       provider: cacheCloudflare(),
     },
     routeRules: {
       "/": { maxAge: 300, swr: 86400 },
       // Otras rutas públicas pueden usar diferentes tiempos de vida de caché.
     },
    });

    El adaptador @astrojs/cloudflare detecta cacheCloudflare() y habilita Workers Cache en la configuración de despliegue generada.

  2. Purga respuestas cacheadas desde el código del Worker con la API de la plataforma. Esta llamada no necesita credenciales REST de Cloudflare.

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // O purgar tags seleccionados:
    await cache.purge({ tags: ["posts"] });

Las respuestas de admin y API de EmDash ya envían Cache-Control: private, no-store y nunca se almacenan. Las páginas públicas controlan su propia caché a través de Cache-Control / routeRules / Astro.cache.

Dos cosas a saber antes de habilitarlo:

  1. Las respuestas sin header Cache-Control se cachean de todos modos. Workers Cache aplica frescura heurística RFC 9111 — un 200 sin ningún header se cachea por 2 horas. Dale a cada ruta personalizada un Cache-Control explícito (usa private, no-store para cualquier cosa dependiente de sesión).
  2. Las páginas cacheadas se comparten con editores conectados. La caché se ejecuta antes de tu Worker, así que no puede evitarse basándose en cookies de solicitud. Un editor conectado puede recibir la variante anónima cacheada de una página pública — sin la barra de herramientas de edición visual — hasta que la entrada expire. Las respuestas renderizadas por el editor nunca se almacenan (llevan private, no-store), así que nada se filtra en la otra dirección.

No es lo mismo que cloudflareCache() de @emdash-cms/cloudflare

Preferido: Workers CachingLegacy: cloudflareCache()
Configuración"cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } de @emdash-cms/cloudflare
AlmacenamientoPlatform Workers CachingCache API (caches.open / put / match)
Purgacache.purge() de cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretsNinguno para purgaCF_ZONE_ID + CF_CACHE_PURGE_TOKEN

Usa la ruta preferida para sitios nuevos. Mantén cloudflareCache() solo si ya dependes de su comportamiento de Cache API.

Tampoco confundas ninguno de esos con la caché de objetos (objectCache: kvCache({ binding: "CACHE" })), que cachea resultados de consultas de base de datos en KV — una capa separada bajo el Worker.

Dominios personalizados

El primer despliegue recibe una URL workers.dev. El dominio personalizado ya debe ser un dominio activo gestionado por Cloudflare en la misma cuenta que el Worker. Después de que el Worker responda exitosamente en su URL workers.dev, agrega el dominio de producción como ruta de Wrangler:

{
	"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}

Despliega de nuevo y verifica ambas direcciones. Mantener la dirección workers.dev disponible mientras pruebas DNS ayuda a distinguir un problema de enrutamiento de un problema de aplicación.

Acceso público a R2

Por defecto, los medios se sirven a través de la ruta autenticada de medios de EmDash. Si el bucket tiene un dominio público personalizado, establece ese origen como publicUrl para que las URLs de medios generadas lo usen:

storage: r2({
	binding: "MEDIA",
	publicUrl: "https://media.example.com",
}),

El acceso público al bucket se aplica a cada objeto alcanzable, no solo medios. Las copias de seguridad JSON automáticas usan el prefijo backups/ en el mismo backend de almacenamiento, así que no expongas ese prefijo a través del dominio público. Elegir almacenamiento de medios explica el límite seguro.

Transformación de imágenes

EmDash redimensiona y recodifica medios R2 dentro del Worker, a través del binding IMAGES de Cloudflare. El componente Image de emdash/ui y las imágenes en texto enriquecido renderizan a través del endpoint de imágenes que EmDash instala bajo el adaptador de Cloudflare. Para medios en la ruta interna /_emdash/api/media/file/…, ese endpoint lee los bytes fuente directamente del binding R2, sin un fetch HTTP. Esas transformaciones siguen funcionando detrás de Cloudflare Access y con global_fetch_strictly_public. Los medios servidos desde una URL de bucket — ver Acceso público a R2 — usan en su lugar el propio endpoint de transformación del adaptador, que obtiene el archivo vía HTTP antes de transformarlo.

No necesitas declarar el binding. @astrojs/cloudflare lo agrega a la configuración del Worker que genera durante astro build, de la misma manera que agrega cache para Workers Caching. Lo hace siempre que el servicio de imágenes en tiempo de ejecución sea cloudflare-binding: imageService no establecido, el string mismo, o { runtime: "cloudflare-binding" }. Cualquier otro valor — "passthrough", "compile", "cloudflare", "custom" — deja el binding fuera. Listarlo en tu propio wrangler.jsonc hace la intención obvia:

{
	"images": {
		"binding": "IMAGES",
	},
}

Para ver qué obtiene realmente un despliegue, lee la configuración generada en lugar de wrangler.jsonc. Un build escribe .wrangler/deploy/config.json, que apunta wrangler deploy al archivo fusionado (dist/server/wrangler.json por defecto). Busca una entrada images allí.

Cloudflare factura estas transformaciones como Images transformations. Cada combinación única de imagen fuente y parámetros se factura una vez por mes calendario, y las solicitudes repetidas dentro de ese mes son gratuitas. Si un sitio tiene 500 imágenes fuente y solicita un tamaño de miniatura y un tamaño hero para cada imagen, esos dos conjuntos de parámetros cuentan como 1.000 imágenes transformadas para ese mes. El plan Images Free cubre 5.000 transformaciones únicas por mes. Más allá de ese límite, las transformaciones cacheadas siguen siendo servidas, pero las nuevas devuelven un error 9422 y la solicitud de imagen falla.

Autenticación con Cloudflare Access

Cloudflare Access puede reemplazar la autenticación con passkey con el proveedor de identidad adjunto a una aplicación Access. El valor de audience es una configuración secreta en tiempo de ejecución; mantenlo fuera de astro.config.mjs nombrando su variable de entorno:

import { access } from "@emdash-cms/cloudflare";

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50,
			Editors: 40,
		},
	}),
}),

Establece CF_ACCESS_AUDIENCE con pnpm wrangler secret put CF_ACCESS_AUDIENCE. La guía de autenticación explica el aprovisionamiento de usuarios, roles predeterminados y sincronización de roles.

Correo electrónico

Los Workers de producción no tienen servicio de entrega de correo predeterminado. El inicio de sesión con enlace mágico, invitaciones de equipo y notificaciones de comentarios devuelven El correo electrónico no está configurado hasta que un plugin de correo esté activo.

El plugin de correo de Cloudflare usa un binding send_email. Primero incorpora y verifica el dominio del remitente con Cloudflare Email Sending. Cloudflare rechaza mensajes cuya dirección de remitente no es un remitente aceptado.

Agrega el binding y registra el proveedor:

{
	"send_email": [{ "name": "EMAIL" }],
}
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [
		cloudflareEmail({
			from: { email: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]",
		}),
	],
}),

Después del despliegue, activa el plugin bajo Extensiones y selecciónalo bajo Configuración → Correo electrónico. El envío falla hasta que el remitente sea aceptado y el binding exista.

El plugin usa el binding llamado EMAIL a menos que su opción binding nombre otro. Si es el único proveedor de correo activo, EmDash lo selecciona automáticamente. Si más de un proveedor está activo, elige el proveedor de Cloudflare bajo Configuración → Correo electrónico. La dirección replyTo opcional recibe respuestas sin cambiar la dirección de remitente aceptada.

El plugin de AI Search necesita tanto un registro de plugin nativo como un binding ai_search_namespaces. Después de desplegarlos, abre Cloudflare AI Search en el admin, elige las colecciones y ejecuta Sincronizar todo el contenido. La sincronización inicial indexa contenido publicado antes de que el plugin fuera habilitado; los hooks mantienen los cambios posteriores sincronizados.

import { aiSearch } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [aiSearch()],
}),
{
	"ai_search_namespaces": [{ "binding": "AI_SEARCH", "namespace": "default" }],
}

Expón la ruta de búsqueda desde el sitio:

export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";

Agrega la interfaz de búsqueda a un layout. El slot de trigger acepta un botón que coincida con el diseño del sitio:

---
import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
---

<AISearchSnippet apiUrl="/api/ai-search" placeholder="Buscar contenido">
	<button slot="trigger" type="button">Buscar</button>
</AISearchSnippet>

Secrets del Worker

Almacena valores secretos con pnpm wrangler secret put <NAME>. No los pongas en wrangler.jsonc ni los leas de valores import.meta.env de tiempo de compilación.

EMDASH_ENCRYPTION_KEY actualmente no cifra secrets de plugins ni otros datos almacenados. Si está establecido, EmDash verifica su formato durante el arranque. Un valor malformado produce un mensaje de log visible para el operador, pero el sitio continúa manejando solicitudes. Los secrets de plugins permanecen en texto plano en la base de datos.

EmDash lee sus secrets de process.env en tiempo de ejecución. El código del Worker lee bindings de env, importado de cloudflare:workers. Nunca leas secrets a través de import.meta.env: Vite reemplaza esos valores en tiempo de compilación y puede escribirlos en el bundle del servidor.

El secret HMAC de preview y la sal de IP de comentaristas se generan y almacenan en la base de datos a menos que proporciones sobreescrituras en tiempo de ejecución. Secrets y gestión de claves lista las variables exactas, ubicaciones de almacenamiento y efectos de rotación.

Despliegues de preview

Los entornos con nombre de Wrangler no heredan bindings. Crea recursos de preview separados y escríbelos en el entorno preview antes de compilar:

pnpm wrangler d1 create my-emdash-site-preview \
  --binding DB --env preview --update-config
pnpm wrangler r2 bucket create my-emdash-media-preview \
  --binding MEDIA --env preview --update-config

El entorno preview debe repetir cada binding que el Worker de preview usa. Los bindings core de D1, R2 y sandbox tienen esta forma después de que Wrangler escribe los identificadores de recursos:

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "my-emdash-site-preview",
					"database_id": "00000000-0000-0000-0000-000000000000",
				},
			],
			"r2_buckets": [
				{
					"binding": "MEDIA",
					"bucket_name": "my-emdash-media-preview",
				},
			],
			"worker_loaders": [{ "binding": "LOADER" }],
		},
	},
}

Usa el UUID de preview escrito por Wrangler. Repite bindings opcionales de KV, AI Search, correo y otros cuando el preview use esas funcionalidades. Agrega secrets específicos de preview con pnpm wrangler secret put <NAME> --env preview.

Compila y despliega el entorno preview. Su primera solicitud aplica migraciones core pendientes a través del modo auto predeterminado.

pnpm build
pnpm wrangler deploy --env preview

Verifica la URL de preview, inicio de sesión del admin, carga de medios y cualquier binding opcional antes de compartirla. Nunca apuntes un binding de preview a una base de datos o bucket de producción.

Verificar el despliegue

Después del despliegue, solicita una página pública, inicia sesión en /_emdash/admin, sube y recupera un archivo de medios de prueba, y confirma que el handler programado aparece en pnpm wrangler tail.

Solución de problemas

”D1 binding not found”

Verifica que el nombre del binding en wrangler.jsonc coincida con tu configuración de base de datos:

// Debe coincidir: d1({ binding: "DB" })
"binding": "DB"

“R2 binding not found”

Verifica que el bucket R2 esté correctamente vinculado:

// Debe coincidir: r2({ binding: "MEDIA" })
"binding": "MEDIA"

Errores de migración

Si ves errores de esquema, sigue los logs del Worker (wrangler tail) y reproduce el error para capturar el mensaje subyacente — luego crea un issue con esa salida.