Cloudflare Workers proporciona un entorno de ejecución rápido y distribuido globalmente para EmDash. Esta guía cubre el despliegue con D1 para la base de datos y R2 para el almacenamiento de medios.
Requisitos previos
- Una cuenta de Cloudflare
- Wrangler CLI instalado (
npm install -g wrangler) - Autenticado con Cloudflare (
wrangler login)
Configurar Bindings
Aprovisiona la base de datos D1 de producción y el bucket R2, luego crea wrangler.jsonc en la raíz de tu proyecto con bindings para sus IDs y nombres inmutables. El aprovisionamiento de la base de datos es independiente de la aplicación de las migraciones de schema de EmDash.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-emdash-site",
"compatibility_date": "2025-01-15",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db",
"database_id": "00000000-0000-0000-0000-000000000000",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media",
},
],
}
Estos son los bindings que configuras tú mismo. El adaptador @astrojs/cloudflare añade más propios cuando genera la configuración del Worker desplegado. Uno de ellos es el binding IMAGES que usan las transformaciones de medios — consulta Transformación de imágenes.
Los plugins sandboxados — instalaciones del marketplace y los plugins bajo sandboxed: [] — necesitan un binding worker_loaders y un punto de entrada del Worker que exporte PluginBridge. Consulta Plugin 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 } from "@emdash-cms/cloudflare";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
react(), // Requerido — la UI de admin es una app React
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
Migrar y desplegar
Las migraciones en tiempo de ejecución permanecen automáticas por defecto. Para migraciones gestionadas por despliegue, construye el Worker e inspecciona el objetivo D1 aprovisionado usando su UUID de cuenta y base de datos.
pnpm build
pnpm exec emdash migrate --status --json \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--d1 "$D1_DATABASE_ID"
Después de revisar y registrar la huella digital del objetivo reportada, aplica las migraciones y despliega el mismo build.
pnpm exec emdash migrate \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--d1 "$D1_DATABASE_ID" \
--expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy
El trabajo de migración requiere CLOUDFLARE_API_TOKEN con permiso de edición D1. Serializa los trabajos por UUID de cuenta y base de datos. Consulta Gestionar migraciones de base de datos del núcleo para aprovisionamiento, concurrencia de CI, modos de ejecución y guía de recuperació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 build desde .emdash/seed.json, la ruta en package.json#emdash.seed, o seed/seed.json — lo que se encuentre primero — y se incorpora 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 schema o modelo de contenido de un sitio que ya está desplegado, consulta Evolucionar un sitio desplegado.
Tareas programadas
Cloudflare ejecuta publicaciones programadas, tareas de plugins y mantenimiento general desde un Cron Trigger.
Usa el punto de entrada estándar del Worker:
import handler, {
createScheduledHandler,
PluginBridge,
} from "@emdash-cms/cloudflare/worker";
export { PluginBridge };
export default {
...handler,
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;
Configura un Cron Trigger para mantenimiento general en wrangler.jsonc:
{
"triggers": {
"crons": ["* * * * *"],
},
}
Para usar un horario diferente de mantenimiento general, establece generalCron en createScheduledHandler() y usa la misma expresión en wrangler.jsonc.
Desplegar
Despliega en Cloudflare Workers:
wrangler deploy
Tu sitio ahora está en vivo en https://my-emdash-site.<your-subdomain>.workers.dev.
Réplicas de lectura
Para sitios distribuidos globalmente, habilita la replicación de lectura D1 para enrutar consultas de lectura a réplicas cercanas en lugar de siempre consultar la base de datos primaria. Esto reduce significativamente la latencia para visitantes lejos de la región primaria.
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
storage: r2({ binding: "MEDIA" }),
}),
También necesitas habilitar la replicación de lectura en la base de datos D1 misma en el panel de Cloudflare o mediante la API REST.
Consulta Opciones de base de datos — Réplicas de lectura para modos de sesión y cómo funciona la consistencia basada en marcadores.
Cache de objetos
Para reducir la carga de lectura en D1, almacena en caché 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 Cache de objetos para configuración de KV, opciones y comportamiento de invalidación.
Workers Cache
El Workers Cache de Cloudflare pone un cache de borde delante de tu Worker: las solicitudes coincidentes se sirven sin ejecutar tu Worker en absoluto.
Habilitarlo
- Activa el cache de plataforma en
wrangler.jsonc:
{
"cache": {
"enabled": true,
},
}
- Usa el proveedor de cache de Cloudflare de Astro para que las reglas de ruta /
Astro.cacheestablezcan los headers correctos y la invalidación usecache.purge()nativo:
import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({
adapter: cloudflare(),
cache: {
provider: cacheCloudflare(),
},
routeRules: {
"/": { maxAge: 300, swr: 86400 },
// …
},
});
Con cacheCloudflare(), el adaptador @astrojs/cloudflare también inyecta "cache": { "enabled": true } en la configuración Wrangler generada cuando falta — listarlo explícitamente en tu propio wrangler.jsonc mantiene la intención obvia.
- Purga desde el Worker con la API de plataforma (sin credenciales REST de Cloudflare):
import { cache } from "cloudflare:workers";
await cache.purge({ purgeEverything: true });
// o: await cache.purge({ tags: ["posts"] });
Las respuestas del admin y API de EmDash ya envían Cache-Control: private, no-store y nunca se almacenan. Las páginas públicas controlan su propio cache a través de Cache-Control / routeRules / Astro.cache.
Dos cosas a saber antes de habilitarlo:
- Las respuestas sin header
Cache-Controlaún se cachean. Workers Cache aplica frescura heurística RFC 9111 — una200sin header se cachea durante 2 horas. Dale a cada ruta personalizada unCache-Controlexplícito (usaprivate, no-storepara todo lo dependiente de sesión). - Las páginas cacheadas se comparten con editores conectados. El cache se ejecuta antes de tu Worker, por lo que no puede omitirse 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 editores nunca se almacenan (llevan
private, no-store), por lo que nada se filtra en la otra dirección.
No es lo mismo que cloudflareCache() de @emdash-cms/cloudflare
| Preferido: Workers Caching | Legacy: cloudflareCache() | |
|---|---|---|
| Configuración | "cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cache | cache: { provider: cloudflareCache() } de @emdash-cms/cloudflare |
| Almacenamiento | Plataforma Workers Caching | Cache API (caches.open / put / match) |
| Purga | cache.purge() de cloudflare:workers | Zone REST POST /zones/{id}/purge_cache |
| Secretos | Ninguno para purga | CF_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 cache de objetos (objectCache: kvCache({ binding: "CACHE" })), que cachea resultados de consultas de base de datos en KV — una capa separada bajo el Worker.
Dominio personalizado
Añade un dominio personalizado en el panel de Cloudflare:
- Ve a Workers & Pages > tu worker
- Haz clic en Custom Domains > Add Custom Domain
- Ingresa tu dominio y sigue las instrucciones de configuración DNS
Acceso público R2
Para servir medios directamente desde R2 (recomendado para rendimiento):
- En el panel de Cloudflare, ve a R2 > tu bucket
- Haz clic en Settings > Public access
- Habilita el acceso público y anota la URL pública
- Actualiza tu configuración de almacenamiento:
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxx.r2.dev"
}),
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 ambos renderizan a través del endpoint de imagen 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 — consulta Acceso público R2 — toman el endpoint de transformación propio del adaptador, que hace fetch del archivo por HTTP antes de transformarlo.
No tienes que declarar el binding. @astrojs/cloudflare lo añade a la configuración del Worker que genera durante astro build, de la misma manera que añade cache para Workers Caching. Lo hace siempre que el servicio de imagen en tiempo de ejecución es cloudflare-binding: imageService no establecido, el string mismo, o { runtime: "cloudflare-binding" }. Cualquier otro valor — "passthrough", "compile", "cloudflare", "custom" — omite el binding. Listarlo en tu propio wrangler.jsonc mantiene la intención obvia:
{
"images": {
"binding": "IMAGES",
},
}
Para ver qué recibe 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 cobra estas transformaciones como transformaciones de Images. Cada combinación única de imagen fuente y parámetros se cobra una vez por mes calendario, y las solicitudes repetidas dentro de ese mes son gratuitas. El plan gratuito de Images cubre 5.000 transformaciones únicas por mes. Pasado ese límite, las transformaciones en caché aún se sirven, pero las nuevas devuelven un error 9422 y la solicitud de imagen falla.
Autenticación con Cloudflare Access
Si tu organización usa Cloudflare Access, puedes usarlo como proveedor de autenticación en lugar de passkeys, dando inicio de sesión único a través de tu proveedor de identidad existente. La siguiente configuración lo habilita:
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
"Admins": 50,
"Editors": 40,
},
}),
}),
Consulta la guía de autenticación para las opciones de configuración completas.
Cloudflare AI Search
El plugin AI Search indexa el contenido publicado de EmDash y añade una interfaz de búsqueda inteligente a tu sitio.
-
Registra el plugin en el array
pluginspasado a EmDash:import { aiSearch } from "@emdash-cms/cloudflare/plugins"; // ... plugins: [ formsPlugin(), aiSearch(), ], -
Añade el binding de namespace de AI Search a tu configuración del Worker:
{ "ai_search_namespaces": [ { "binding": "AI_SEARCH", "namespace": "default", }, ], } -
Crea el endpoint de búsqueda usado por la interfaz de búsqueda:
export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search"; -
Añade la interfaz de búsqueda al layout de tu sitio. El slot trigger puede contener cualquier botón que encaje con el diseño de tu sitio:
--- import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro"; --- <AISearchSnippet apiUrl="/api/ai-search" placeholder="Buscar..."> <button slot="trigger" type="button">Buscar</button> </AISearchSnippet> -
Despliega el sitio:
pnpm exec wrangler deploy -
Abre Cloudflare AI Search en el panel de admin de EmDash, selecciona las colecciones a indexar y haz clic en Sync All Content.
Esta sincronización inicial es requerida: los hooks de contenido del plugin solo se activan para contenido creado o actualizado después de que fue habilitado, así que cualquier cosa publicada antes permanece fuera del índice hasta que ejecutes una sincronización completa.
El contenido publicado o actualizado después de la configuración se mantiene sincronizado automáticamente. La misma página muestra el progreso de indexación.
En Workers, el único handler email:deliver incorporado es un stub de consola de desarrollo, así que los flujos dependientes de email — login por magic-link, invitaciones de equipo y notificaciones de comentarios — fallan con “Email is not configured” en producción. El plugin cloudflareEmail() entrega email real a través de Cloudflare Email Sending usando un binding nativo send_email del Worker, sin claves de API externas.
1. Incorporar un dominio de envío
En el panel de Cloudflare, ve a Email y verifica el dominio (o dirección) desde la que envías. Email Sending rechaza mensajes de remitentes no verificados.
2. Añadir el binding
Declara un binding send_email en wrangler.jsonc:
{
"send_email": [{ "name": "EMAIL" }],
}
3. Registrar el proveedor
Añade el plugin a tu integración emdash():
import { d1, r2 } from "@emdash-cms/cloudflare";
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
plugins: [
cloudflareEmail({
from: { email: "[email protected]", name: "My Site CMS" },
replyTo: "[email protected]", // opcional
binding: "EMAIL", // opcional, por defecto "EMAIL"
}),
],
}),
4. Activar y seleccionar
Despliega, luego activa el plugin en Admin → Extensions y elige como proveedor en Settings → Email.
Opciones
| Opción | Tipo | Por defecto | Descripción |
|---|---|---|---|
from | string | { email, name? } | — (requerido) | Dirección del remitente en un dominio incorporado para Email Sending. |
replyTo | string | — | Reply-To opcional, útil cuando from es una dirección de subdominio no-reply. |
binding | string | "EMAIL" | Nombre del binding send_email en wrangler.jsonc. |
Variables de entorno
Recomendado: clave de cifrado
EMDASH_ENCRYPTION_KEY es la clave para cifrar secretos de plugins en reposo (tokens de webhook, claves Turnstile, etc.). La clave se valida al inicio; el cifrado de secretos de plugins la usa una vez habilitada. Establécela en cada despliegue para que los secretos estén protegidos sin un cambio de configuración posterior.
La clave la proporcionas tú y nunca se almacena en la base de datos; solo se almacena texto cifrado. Perderla significa perder cada secreto cifrado con ella.
Genera una clave y almacénala como secreto del Worker con los siguientes comandos:
npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY
Opcional: sobreescrituras de valor estable
EmDash auto-genera el secreto HMAC de vista previa y la sal de hash de IP de comentarista y los persiste en la base de datos en el primer uso. Las variables de entorno a continuación son sobreescrituras para casos donde necesitas fijar el valor tú mismo — por ejemplo, cuando un Worker de vista previa en un proceso separado necesita compartir el secreto con tu sitio principal.
| Variable | Propósito |
|---|---|
EMDASH_PREVIEW_SECRET | Sobreescritura para el secreto HMAC de vista previa auto-generado. |
EMDASH_IP_SALT | Sobreescritura para la sal de hash de IP de comentarista auto-generada. |
EMDASH_AUTH_SECRET | Opcional. Si se establece, se usa como fuente de sal de IP (a menos que también se establezca EMDASH_IP_SALT, que tiene precedencia), manteniendo los hashes de IP de comentarista estables para instalaciones que ya dependen de ello. Déjalo sin establecer para un nuevo despliegue. |
Accede a las variables de entorno en tu configuración usando import.meta.env o el binding env de Cloudflare.
Para el inventario completo de cada secreto que EmDash usa — incluyendo ubicaciones de almacenamiento, pasos de rotación y qué se rompe cuando se pierde una clave — consulta Secretos y gestión de claves.
Despliegues de vista previa
Despliega una rama de vista previa:
wrangler deploy --env preview
Añade una sección de entorno a wrangler.jsonc:
{
"env": {
"preview": {
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db-preview",
},
],
},
},
}
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 schema, sigue los logs del Worker (wrangler tail) y reproduce el error para capturar el mensaje subyacente — luego abre un issue con esa salida.