Desplegar en Cloudflare

En esta página

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

Prerrequisitos

  • 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 nombrados. En el primer despliegue, Wrangler crea cualquier recurso si su nombre configurado aún no existe. Mantenga 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. Consulte Plugin sandbox 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 aplicación React
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
			sandboxRunner: sandbox(),
		}),
	],
});

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

Agregar el punto de entrada del Worker

El punto de entrada del Worker conecta Astro al Cron Trigger y exporta el puente de plugin:

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

export { PluginBridge };

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

La exportación PluginBridge es inofensiva cuando no hay ningún plugin en sandbox instalado. Consérvela si el mismo proyecto puede habilitar plugins más tarde.

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

Construir y desplegar

Construya y despliegue el sitio una vez para permitir que Wrangler aprovisione la base de datos D1 y el bucket R2 nombrados. Wrangler usa el inicio de sesión local creado por pnpm wrangler login.

pnpm build
pnpm wrangler deploy

Con el modo de migración auto predeterminado, EmDash aplica las migraciones principales pendientes cuando el Worker desplegado recibe su primera solicitud. Use Gestionar migraciones principales de base de datos cuando una canalización de despliegue deba aplicar migraciones antes de que el código nuevo reciba tráfico o cuando necesite 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 de .emdash/seed.json, la ruta en package.json#emdash.seed, o seed/seed.json — lo que se encuentre primero — y se incluye en el bundle. Si ninguno está 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, consulte Evolucionar un sitio desplegado.

Colocar el Worker cerca de D1

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

Wrangler acepta placement.mode: "targeted" con exactamente un selector: region, host o hostname. Seleccione el valor que se dirige a la ubicación primaria D1 y agregue el objeto placement resultante a wrangler.jsonc. No habilite réplicas de lectura D1 con Targeted Placement. Mantenga 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, almacene 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" }),
}),

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

Workers Cache

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

Habilitarlo

  1. Use el proveedor de caché de Cloudflare de Astro para que las reglas de ruta y Astro.cache establezcan encabezados 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. Purgue respuestas almacenadas en caché del código 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 etiquetas seleccionadas:
    await cache.purge({ tags: ["posts"] });

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

Dos cosas que debe saber antes de habilitarlo:

  1. Las respuestas sin encabezado Cache-Control aún se almacenan en caché. Workers Cache aplica la frescura heurística RFC 9111 — un 200 sin ningún encabezado se almacena en caché durante 2 horas. Dé a cada ruta personalizada un Cache-Control explícito (use private, no-store para cualquier cosa dependiente de la sesión).
  2. Las páginas almacenadas en caché se comparten con editores que han iniciado sesión. La caché se ejecuta antes que su Worker, por lo que no puede omitirse según las cookies de solicitud. Un editor que ha iniciado sesión puede recibir la variante anónima en caché de una página pública — sin la barra de herramientas de edición visual — hasta que expire la entrada. Las respuestas renderizadas por el editor 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 CachingLegacy: cloudflareCache()
Config"cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } de @emdash-cms/cloudflare
StoragePlatform Workers CachingCache API (caches.open / put / match)
Purgecache.purge() de cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretsNinguno para purgeCF_ZONE_ID + CF_CACHE_PURGE_TOKEN

Use el camino preferido para sitios nuevos. Mantenga cloudflareCache() solo si ya depende de su comportamiento de Cache API.

Tampoco confunda ninguno de esos con caché de objetos (objectCache: kvCache({ binding: "CACHE" })), que almacena en caché los 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 administrado por Cloudflare en la misma cuenta que el Worker. Después de que el Worker responda exitosamente en su URL workers.dev, agregue el dominio de producción como una ruta Wrangler:

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

Despliegue nuevamente y verifique ambas direcciones. Mantener la dirección workers.dev disponible mientras prueba DNS ayuda a distinguir un problema de enrutamiento de un problema de aplicación.

Acceso público R2

Por defecto, los medios se sirven a través de la ruta de medios autenticada de EmDash. Si el bucket tiene un dominio personalizado público, establezca 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 a medios. Las copias de seguridad JSON automáticas usan el prefijo backups/ en el mismo backend de almacenamiento, así que no exponga 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 e imágenes en texto enriquecido se 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 de origen directamente del binding R2, sin una obtención HTTP. Esas transformaciones siguen funcionando detrás de Cloudflare Access y con global_fetch_strictly_public. Los medios servidos desde una URL de bucket — consulte Acceso público R2 — toman el propio endpoint de transformación del adaptador en su lugar, que obtiene el archivo sobre HTTP antes de transformarlo.

No tiene que 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 imagen en tiempo de ejecución sea cloudflare-binding: imageService sin establecer, la cadena en sí, o { runtime: "cloudflare-binding" }. Cualquier otro valor — "passthrough", "compile", "cloudflare", "custom" — deja el binding fuera. Listarlo en su propio wrangler.jsonc mantiene la intención obvia:

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

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

Cloudflare factura estas transformaciones como transformaciones de Images. Cada combinación única de imagen de origen 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 de origen y solicita un tamaño de miniatura y un tamaño de héroe para cada imagen, esos dos conjuntos de parámetros cuentan como 1,000 imágenes transformadas para ese mes. El plan gratuito de Images cubre 5,000 transformaciones únicas por mes. Más allá de ese límite, las transformaciones almacenadas en caché aún se sirven, pero las nuevas devuelven un error 9422 y la solicitud de imagen falla.

Autenticación Cloudflare Access

Cloudflare Access puede reemplazar la autenticación de 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; manténgalo 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,
		},
	}),
}),

Establezca 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 electrónico predeterminado. El inicio de sesión de magic-link, invitaciones de equipo y notificaciones de comentarios devuelven Email is not configured hasta que un plugin de correo electrónico esté activo.

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

Agregue el binding y registre el proveedor:

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

emdash({
	plugins: [
		cloudflareEmail({
			from: { email: "cms@mails.example.com", name: "My Site CMS" },
			replyTo: "hello@example.com",
		}),
	],
}),

Después del despliegue, active el plugin en Extensions y selecciónelo en Settings → Email. El envío falla hasta que el remitente sea aceptado y el binding exista.

El plugin usa el binding nombrado EMAIL a menos que su opción binding nombre otro. Si es el único proveedor de correo electrónico activo, EmDash lo selecciona automáticamente. Si más de un proveedor está activo, elija el proveedor de Cloudflare en Settings → Email. La dirección replyTo opcional recibe respuestas sin cambiar la dirección From aceptada. Un plugin puede establecer replyTo en un mensaje individual, lo que anula esta opción para ese mensaje.

El plugin AI Search necesita tanto un registro de plugin nativo como un binding ai_search_namespaces. Después de desplegarlos, abra Cloudflare AI Search en el administrador, elija las colecciones y ejecute Sync All Content. La sincronización inicial indexa contenido publicado antes de que se habilitara el plugin; los hooks mantienen sincronizados cambios posteriores.

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

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

Exponga la ruta de búsqueda desde el sitio:

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

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

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

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

Secretos del Worker

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

EMDASH_ENCRYPTION_KEY encripta configuraciones de plugin declaradas como secretos. Establézcalo antes de guardar un secreto de plugin y consérvelo por separado de las copias de seguridad de D1. Durante la rotación, proporcione la nueva clave primero y retenga claves antiguas después de comas hasta que cada secreto de plugin se haya guardado nuevamente. Secretos y gestión de claves describe la rotación y recuperación.

El puente de plugin lee este binding secreto del Worker directamente. La ruta de configuración de administración generada lo lee a través de process.env. Con nodejs_compat, Cloudflare rellena process.env por defecto para fechas de compatibilidad en o después del 2025-04-01. Los proyectos fijados a una fecha anterior también deben agregar nodejs_compat_populate_process_env antes de guardar configuraciones encriptadas.

EmDash lee sus secretos de process.env en tiempo de ejecución. El código Worker lee bindings de env, importado de cloudflare:workers. Nunca lea secretos 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 secreto HMAC de vista previa y el salt de IP del comentarista se generan y almacenan en la base de datos a menos que proporcione anulaciones en tiempo de ejecución. Secretos y gestión de claves enumera las variables exactas, ubicaciones de almacenamiento y efectos de rotación.

Despliegues de vista previa

Los entornos Wrangler nombrados no heredan bindings. Cree recursos de vista previa separados y escríbalos 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 de vista previa debe repetir cada binding que use el Worker de vista previa. Los bindings principales D1, R2 y sandbox tienen esta forma después de que Wrangler escriba 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" }],
		},
	},
}

Use el UUID de vista previa escrito por Wrangler. Repita bindings opcionales de KV, AI Search, correo electrónico y otros cuando la vista previa use esas características. Agregue secretos solo de vista previa con pnpm wrangler secret put <NAME> --env preview.

Compile y despliegue el entorno de vista previa. Su primera solicitud aplica migraciones principales pendientes a través del modo auto predeterminado.

pnpm build
pnpm wrangler deploy --env preview

Verifique la URL de vista previa, inicio de sesión de administrador, carga de medios y cualquier binding opcional antes de compartirlo. Nunca apunte un binding de vista previa a una base de datos o bucket de producción.

Verificar el despliegue

Después del despliegue, solicite una página pública, inicie sesión en /_emdash/admin, cargue y recupere un archivo de medios de prueba, y confirme que el controlador programado aparezca en pnpm wrangler tail.

Resolución de problemas

”D1 binding not found”

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

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

“R2 binding not found”

Verifique que el bucket R2 esté correctamente vinculado:

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

Errores de migración

Si ve errores de esquema, siga los logs del Worker (wrangler tail) y reproduzca el error para capturar el mensaje subyacente — luego presente un issue con esa salida.