Déployer sur Cloudflare

Sur cette page

Ce guide déploie un site EmDash sur Cloudflare Workers avec D1 pour sa base de données et R2 pour les médias. Partez d’un template EmDash Cloudflare ou appliquez la même configuration à un site Astro existant.

Prérequis

  • Un compte Cloudflare
  • Les dépendances du projet installées
  • Wrangler authentifié auprès de Cloudflare (pnpm wrangler login)

Configurer les bindings

Les templates Cloudflare incluent le point d’entrée Worker complet et des bindings D1 et R2 nommés. Au premier déploiement, Wrangler crée la ressource si son nom configuré n’existe pas encore. Conservez les noms dans wrangler.jsonc ; Wrangler reconnecte les déploiements ultérieurs aux mêmes ressources.

Le template utilise les bindings suivants :

{
	"$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": ["* * * * *"] },
}

Les noms DB, MEDIA et LOADER doivent correspondre aux adaptateurs EmDash. Le Cron Trigger exécute la publication planifiée, les tâches de plugins, les sauvegardes et la maintenance. Voir Plugin sandbox si le site utilise des plugins sandboxed.

Configurer EmDash

La configuration Astro suivante utilise les bindings D1 et 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(), // Required — the admin UI is a React app
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
			sandboxRunner: sandbox(),
		}),
	],
});

Si le site n’utilise pas de plugins marketplace, registry ou sandboxed, omettez sandboxRunner et le binding LOADER.

Ajouter le point d’entrée Worker

Le point d’entrée Worker connecte Astro au Cron Trigger et exporte le pont plugin :

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

export { PluginBridge };

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

L’export PluginBridge est inoffensif lorsqu’aucun plugin sandboxed n’est installé. Conservez-le si le même projet peut activer des plugins plus tard.

Pour exécuter la maintenance générale selon un planning autre que chaque minute, passez la même expression Cron à createScheduledHandler({ generalCron: "..." }) et à triggers.crons. S’ils diffèrent, le handler journalise et ignore le déclencheur inattendu.

Construire et déployer

Construisez et déployez le site une fois pour laisser Wrangler provisionner la base D1 et le bucket R2 nommés. Wrangler utilise la connexion locale créée par pnpm wrangler login.

pnpm build
pnpm wrangler deploy

Avec le mode de migration par défaut auto, EmDash applique les migrations core en attente lorsque le Worker déployé reçoit sa première requête. Utilisez Manage core database migrations lorsqu’un pipeline de déploiement doit appliquer les migrations avant que le nouveau code ne reçoive du trafic, ou lorsque vous devez inspecter, vérifier ou récupérer une migration.

Si la base est vide (aucune collection) et que l’assistant de configuration n’a pas été terminé, EmDash applique aussi un fichier seed au premier démarrage. Le seed est lu au moment du build depuis .emdash/seed.json, le chemin dans package.json#emdash.seed, ou seed/seed.json — le premier trouvé — et intégré au bundle. S’il n’y en a aucun, un seed par défaut intégré est utilisé. Les déploiements suivants contre une base existante laissent son contenu intact.

Pour modifier le schéma ou le modèle de contenu d’un site déjà déployé, voir Evolving a Deployed Site.

Placer le Worker près de D1

Cloudflare exécute un Worker près du visiteur par défaut. Les requêtes EmDash rendues côté serveur font plusieurs allers-retours D1 ; utilisez donc Targeted Placement pour exécuter le Worker près du primaire D1 et accélérer ces requêtes.

Wrangler accepte placement.mode: "targeted" avec exactement un sélecteur : region, host ou hostname. Choisissez la valeur qui cible l’emplacement du primaire D1, et ajoutez l’objet placement résultant à wrangler.jsonc. N’activez pas les réplicas en lecture D1 avec Targeted Placement. Conservez le réglage session d’EmDash à sa valeur par défaut, "disabled", pour que lectures et écritures utilisent le primaire proche.

Object cache

Pour réduire la charge de lecture sur D1, mettez en cache les résultats de requêtes de contenu et de configuration dans Cloudflare KV. Les lectures sont servies depuis KV au lieu d’interroger la base à chaque requête :

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

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

Voir Object Cache pour la configuration KV, les options et le comportement d’invalidation.

Workers Cache

Le Workers Cache de Cloudflare place un cache edge devant votre Worker : les requêtes correspondantes sont servies sans exécuter du tout votre Worker.

L’activer

  1. Utilisez le fournisseur de cache Cloudflare d’Astro pour que les règles de route et Astro.cache définissent les en-têtes de cache et que l’invalidation utilise cache.purge().

    import { cacheCloudflare } from "@astrojs/cloudflare/cache";
    
    export default defineConfig({
     adapter: cloudflare(),
     cache: {
       provider: cacheCloudflare(),
     },
     routeRules: {
       "/": { maxAge: 300, swr: 86400 },
       // Other public routes can use different cache lifetimes.
     },
    });

    L’adaptateur @astrojs/cloudflare détecte cacheCloudflare() et active Workers Cache dans la configuration de déploiement générée.

  2. Purgez les réponses mises en cache depuis le code Worker avec l’API de la plateforme. Cet appel n’a pas besoin d’identifiants REST Cloudflare.

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

Les réponses d’administration et d’API EmDash envoient déjà Cache-Control: private, no-store et ne sont jamais stockées. Les pages publiques contrôlent leur propre mise en cache via Cache-Control / routeRules / Astro.cache.

Deux points à connaître avant de l’activer :

  1. Les réponses sans en-tête Cache-Control sont quand même mises en cache. Workers Cache applique la fraîcheur heuristique RFC 9111 — un 200 sans en-tête est mis en cache 2 heures. Donnez à chaque route personnalisée un Cache-Control explicite (utilisez private, no-store pour tout ce qui dépend de la session).
  2. Les pages mises en cache sont partagées avec les éditeurs connectés. Le cache s’exécute avant votre Worker, il ne peut donc pas être contourné selon les cookies de requête. Un éditeur connecté peut recevoir la variante anonyme mise en cache d’une page publique — sans la barre d’édition visuelle — jusqu’à expiration de l’entrée. Les réponses rendues par l’éditeur elles-mêmes ne sont jamais stockées (elles portent private, no-store), donc rien ne fuit dans l’autre sens.

Pas la même chose que cloudflareCache() de @emdash-cms/cloudflare

Préféré : 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
SecretsAucun pour la purgeCF_ZONE_ID + CF_CACHE_PURGE_TOKEN

Utilisez le chemin préféré pour les nouveaux sites. Conservez cloudflareCache() seulement si vous dépendez déjà de son comportement Cache API.

Ne confondez pas non plus l’un ou l’autre avec l’object cache (objectCache: kvCache({ binding: "CACHE" })), qui met en cache les résultats de requêtes base de données dans KV — une couche séparée sous le Worker.

Domaines personnalisés

Le premier déploiement reçoit une URL workers.dev. Le domaine personnalisé doit déjà être un domaine actif géré par Cloudflare dans le même compte que le Worker. Une fois que le Worker répond correctement à son URL workers.dev, ajoutez le domaine de production comme route Wrangler :

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

Déployez à nouveau et vérifiez les deux adresses. Garder l’adresse workers.dev disponible pendant les tests DNS aide à distinguer un problème de routage d’un problème d’application.

Accès R2 public

Par défaut, les médias sont servis via la route média authentifiée d’EmDash. Si le bucket a un domaine personnalisé public, définissez cette origine comme publicUrl pour que les URL média générées l’utilisent :

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

L’accès public au bucket s’applique à tout objet joignable, pas seulement aux médias. Les sauvegardes JSON automatiques utilisent le préfixe backups/ dans le même backend de stockage ; n’exposez donc pas ce préfixe via le domaine public. Choose media storage explique la limite sûre.

Transformation d’images

EmDash redimensionne et réencode les médias R2 dans le Worker, via le binding IMAGES de Cloudflare. Le composant Image de emdash/ui et les images du texte enrichi se rendent tous deux via le endpoint d’image qu’EmDash installe sous l’adaptateur Cloudflare. Pour les médias sur la route interne /_emdash/api/media/file/…, ce endpoint lit les octets source directement depuis le binding R2, sans fetch HTTP. Ces transformations continuent de fonctionner derrière Cloudflare Access et avec global_fetch_strictly_public. Les médias servis depuis une URL de bucket — voir Public R2 Access — prennent plutôt le endpoint de transformation propre à l’adaptateur, qui récupère le fichier en HTTP avant de le transformer.

Vous n’avez pas à déclarer le binding. @astrojs/cloudflare l’ajoute à la configuration Worker qu’il génère pendant astro build, de la même façon qu’il ajoute cache pour Workers Caching. Il le fait lorsque le service d’image runtime est cloudflare-binding : imageService non défini, la chaîne elle-même, ou { runtime: "cloudflare-binding" }. Toute autre valeur — "passthrough", "compile", "cloudflare", "custom" — omet le binding. Le lister dans votre propre wrangler.jsonc rend l’intention claire :

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

Pour voir ce qu’un déploiement obtient réellement, lisez la configuration générée plutôt que wrangler.jsonc. Un build écrit .wrangler/deploy/config.json, qui pointe wrangler deploy vers le fichier fusionné (dist/server/wrangler.json par défaut). Cherchez-y une entrée images.

Cloudflare facture ces transformations comme Images transformations. Chaque combinaison unique d’image source et de paramètres est facturée une fois par mois calendaire, et les requêtes répétées dans ce mois sont gratuites. Si un site a 500 images sources et demande une taille vignette et une taille héros pour chaque image, ces deux jeux de paramètres comptent comme 1 000 images transformées pour ce mois. Le plan Images Free couvre 5 000 transformations uniques par mois. Au-delà, les transformations mises en cache sont toujours servies, mais les nouvelles renvoient une erreur 9422 et la requête d’image échoue.

Authentification Cloudflare Access

Cloudflare Access peut remplacer l’authentification par passkey par le fournisseur d’identité attaché à une application Access. La valeur d’audience est un réglage secret de runtime ; gardez-la hors de astro.config.mjs en nommant sa variable d’environnement :

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

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

Définissez CF_ACCESS_AUDIENCE avec pnpm wrangler secret put CF_ACCESS_AUDIENCE. Le authentication guide explique le provisionnement des utilisateurs, les rôles par défaut et la synchronisation des rôles.

E-mail

Les Workers de production n’ont pas de service d’envoi d’e-mail par défaut. La connexion par magic link, les invitations d’équipe et les notifications de commentaires renvoient Email is not configured jusqu’à ce qu’un plugin e-mail soit actif.

Le plugin e-mail Cloudflare utilise un binding send_email. D’abord intégrez et vérifiez le domaine d’expéditeur avec Cloudflare Email Sending. Cloudflare rejette les messages dont l’adresse From n’est pas un expéditeur accepté.

Ajoutez le binding et enregistrez le fournisseur :

{
	"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",
		}),
	],
}),

Après le déploiement, activez le plugin sous Extensions et sélectionnez-le sous Settings → Email. L’envoi échoue tant que l’expéditeur n’est pas accepté et que le binding n’existe pas.

Le plugin utilise le binding nommé EMAIL sauf si son option binding en nomme un autre. S’il est le seul fournisseur e-mail actif, EmDash le sélectionne automatiquement. Si plus d’un fournisseur est actif, choisissez le fournisseur Cloudflare sous Settings → Email. L’adresse optionnelle replyTo reçoit les réponses sans changer l’adresse From acceptée. Un plugin peut définir replyTo sur un message individuel, ce qui remplace cette option pour ce message.

Le plugin AI Search nécessite à la fois un enregistrement de plugin natif et un binding ai_search_namespaces. Après leur déploiement, ouvrez Cloudflare AI Search dans l’administration, choisissez les collections et lancez Sync All Content. La synchronisation initiale indexe le contenu publié avant l’activation du plugin ; les hooks maintiennent synchronisés les changements ultérieurs.

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

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

Exposez la route de recherche depuis le site :

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

Ajoutez l’interface de recherche à une mise en page. Le slot déclencheur accepte un bouton qui correspond au design du site :

---
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>

Secrets Worker

Stockez les valeurs secrètes avec pnpm wrangler secret put <NAME>. Ne les mettez pas dans wrangler.jsonc et ne les lisez pas depuis des valeurs import.meta.env de build.

EMDASH_ENCRYPTION_KEY chiffre les réglages de plugin déclarés comme secrets. Définissez-le avant d’enregistrer un secret de plugin, et conservez-le séparément des sauvegardes D1. Pendant la rotation, fournissez d’abord la nouvelle clé et conservez les anciennes après des virgules jusqu’à ce que chaque secret de plugin ait été réenregistré. Secrets and key management décrit la rotation et la récupération.

Le pont plugin lit ce binding de secret Worker directement. La route générée des réglages d’administration le lit via process.env. Avec nodejs_compat, Cloudflare remplit process.env par défaut pour les dates de compatibilité à partir de 2025-04-01. Les projets épinglés à une date antérieure doivent aussi ajouter nodejs_compat_populate_process_env avant d’enregistrer des réglages chiffrés.

EmDash lit ses secrets depuis process.env à l’exécution. Le code Worker lit les bindings depuis env, importé de cloudflare:workers. Ne lisez jamais les secrets via import.meta.env : Vite remplace ces valeurs au build et peut les écrire dans le bundle serveur.

Le secret HMAC de prévisualisation et le sel d’IP des commentateurs sont générés et stockés en base sauf si vous fournissez des remplacements runtime. Secrets and key management liste les variables exactes, emplacements de stockage et effets de rotation.

Déploiements de prévisualisation

Les environnements Wrangler nommés n’héritent pas des bindings. Créez des ressources de prévisualisation séparées et écrivez-les dans l’environnement preview avant de construire :

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

L’environnement de prévisualisation doit répéter chaque binding utilisé par le Worker de prévisualisation. Les bindings core D1, R2 et sandbox ont cette forme après que Wrangler a écrit les identifiants de ressource :

{
	"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" }],
		},
	},
}

Utilisez l’UUID de prévisualisation écrit par Wrangler. Répétez les bindings optionnels KV, AI Search, e-mail et autres lorsque la prévisualisation utilise ces fonctions. Ajoutez des secrets réservés à la prévisualisation avec pnpm wrangler secret put <NAME> --env preview.

Construisez et déployez l’environnement de prévisualisation. Sa première requête applique les migrations core en attente via le mode auto par défaut.

pnpm build
pnpm wrangler deploy --env preview

Vérifiez l’URL de prévisualisation, la connexion admin, le téléversement de médias et tout binding optionnel avant de la partager. Ne pointez jamais un binding de prévisualisation vers une base ou un bucket de production.

Vérifier le déploiement

Après le déploiement, demandez une page publique, connectez-vous à /_emdash/admin, téléversez et récupérez un fichier média de test, et confirmez que le handler planifié apparaît dans pnpm wrangler tail.

Dépannage

« D1 binding not found »

Vérifiez que le nom du binding dans wrangler.jsonc correspond à votre configuration de base de données :

// Must match: d1({ binding: "DB" })
"binding": "DB"

« R2 binding not found »

Vérifiez que le bucket R2 est correctement lié :

// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"

Erreurs de migration

Si vous voyez des erreurs de schéma, suivez les logs du Worker (wrangler tail) et reproduisez l’erreur pour capturer le message sous-jacent — puis ouvrez un issue avec cette sortie.