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 ainsi que des bindings D1 et R2 nommés. Lors du premier déploiement, Wrangler crée l’une ou l’autre de ces ressources si le nom configuré n’existe pas encore. Conservez les noms dans wrangler.jsonc ; Wrangler reconnecte les déploiements suivants 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. Consultez Plugin sandbox si le site utilise des plugins en sandbox.

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 ni plugins marketplace, ni registry, ni plugins 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 de plugins :

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

export { PluginBridge };

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

L’export PluginBridge est sans conséquence lorsqu’aucun plugin en sandbox 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 autre calendrier que chaque minute, passez la même expression Cron à createScheduledHandler({ generalCron: "..." }) et à triggers.crons. Si elles diffèrent, le handler journalise le déclenchement inattendu et l’ignore.

Construire et déployer

Construisez et déployez le site une fois pour que Wrangler provisionne la base de données 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 auto par défaut, EmDash applique les migrations du cœur 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.

Tant que l’assistant de configuration n’est pas terminé, EmDash applique aussi une seule fois, à la première requête, le schéma et la structure d’un fichier seed. Le contenu d’exemple du seed n’est appliqué que lorsqu’un administrateur le choisit dans l’assistant de configuration. Le seed est lu au moment du build depuis .emdash/seed.json, le chemin indiqué dans package.json#emdash.seed ou seed/seed.json — le premier trouvé — puis intégré au bundle. Si aucun n’est présent, un seed par défaut intégré est utilisé. Les déploiements suivants sur une base de données existante ne touchent pas à son contenu.

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

Publication planifiée

Sur Cloudflare, EmDash ne publie les entrées planifiées que depuis le handler scheduled du Worker, que Cloudflare appelle selon le calendrier d’un Cron Trigger. Le Worker déployé a besoin des deux éléments suivants :

  • Un Cron Trigger dans la liste triggers.crons de wrangler.jsonc, comme indiqué dans Configurer les bindings.
  • Le handler scheduled de @emdash-cms/cloudflare/worker, exporté depuis le point d’entrée Worker vers lequel pointe main, comme indiqué dans Ajouter le point d’entrée Worker.

Si l’un des deux manque, les entrées planifiées ne sont pas mises en ligne. En développement local avec astro dev, EmDash publie les entrées planifiées à l’aide d’un minuteur ; un calendrier qui fonctionne en local ne confirme donc pas la configuration déployée.

Exécutez emdash doctor depuis la racine du projet pour vérifier le Cron Trigger et le handler scheduled dans les fichiers du projet local :

pnpm emdash doctor

Lorsque emdash doctor trouve les deux, la sortie inclut une vérification scheduler wiring réussie. Une vérification du planificateur en échec décrit le problème et affiche la correction. La vérification reconnaît le point d’entrée Worker présenté sur cette page et le ré-export d’une ligne qu’elle affiche comme correction ; un point d’entrée écrit autrement peut donc échouer à la vérification même s’il fonctionne.

Si aucune vérification du planificateur n’apparaît, emdash doctor n’a trouvé aucune configuration Wrangler dans le répertoire courant, ou n’a trouvé ni le Cron Trigger ni le handler scheduled. Sur un projet D1, la vérification database échoue car elle recherche un fichier SQLite local (./data.db par défaut). Cet échec n’affecte pas les vérifications du planificateur.

Redéployez après avoir ajouté ou modifié le Cron Trigger ou le handler scheduled. Cloudflare peut mettre jusqu’à 15 minutes pour commencer à exécuter un Cron Trigger nouveau ou modifié. Une fois que le déclencheur s’exécute, EmDash publie les entrées dont l’heure planifiée est déjà passée.

Pour observer le déclencheur, exécutez pnpm wrangler tail. Chaque exécution affiche une ligne telle que "* * * * *" @ 9/29/2026, 10:01:00 AM - Ok. Ok signifie que l’exécution s’est terminée sans erreur non interceptée. EmDash intercepte et journalise les erreurs de publication ; Ok ne confirme donc pas que des entrées ont été publiées. Une exécution qui publie des entrées journalise aussi un message tel que [scheduled] Published 1 scheduled item(s), et les erreurs de l’exécution apparaissent dans la même sortie.

Après le déploiement, le tableau de bord d’administration affiche Scheduled publishing needs attention lorsqu’une entrée planifiée a dépassé son heure planifiée et qu’aucune exécution du planificateur ne s’est terminée au cours des cinq dernières minutes. Si le Cron Trigger s’exécute moins souvent que toutes les cinq minutes, l’avis peut aussi apparaître entre deux exécutions, jusqu’à ce que la suivante publie l’entrée.

Placer le Worker près de D1

Cloudflare exécute par défaut un Worker près du visiteur. Les requêtes rendues côté serveur d’EmDash font plusieurs allers-retours vers 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. Sélectionnez la valeur qui cible l’emplacement du primaire D1, puis ajoutez l’objet placement obtenu à wrangler.jsonc. N’activez pas les réplicas en lecture D1 avec Targeted Placement. Laissez le paramètre session d’EmDash à sa valeur par défaut, "disabled", afin que les lectures et les écritures utilisent le primaire proche.

Cache d’objets

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

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

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

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

Workers Cache

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

L’activer

  1. Utilisez le fournisseur de cache Cloudflare d’Astro afin 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 en cache depuis le code du Worker avec l’API de la plateforme. Cet appel n’a pas besoin d’identifiants de l’API REST de 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 d’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 tout de même mises en cache. Workers Cache applique la fraîcheur heuristique de la RFC 9111 : une 200 sans aucun en-tête est mise en cache pendant 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 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é en fonction des cookies de la requête. Un éditeur connecté peut recevoir la variante anonyme en cache d’une page publique — sans la barre d’outils d’édition visuelle — jusqu’à l’expiration de l’entrée. Les réponses rendues pour l’éditeur ne sont elles-mêmes jamais stockées (elles portent private, no-store), donc rien ne fuit dans l’autre sens.

Workers Caching est distinct du cache d’objets (objectCache: kvCache({ binding: "CACHE" })), qui met en cache les résultats des requêtes de base de données dans KV 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 }],
}

Redéployez 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.

Sans siteUrl, EMDASH_SITE_URL ni SITE_URL, la configuration enregistre l’adresse sur laquelle elle s’exécute comme origine du site, et les passkeys créées pendant la configuration ne fonctionnent que sur cette adresse. Exécutez la configuration sur le domaine de production, ou définissez EMDASH_SITE_URL sur ce domaine avant la configuration. Si vous définissez EMDASH_SITE_URL sur un autre domaine après la configuration, la connexion passe à ce domaine, et les passkeys créées sur l’ancienne adresse cessent d’y fonctionner.

Accès R2 public

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

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

L’accès public au bucket s’applique à chaque objet accessible, 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 à respecter.

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 dans le texte enrichi sont tous deux rendus via l’endpoint d’images qu’EmDash installe sous l’adaptateur Cloudflare. Pour les médias sur la route interne /_emdash/api/media/file/…, cet endpoint lit les octets sources directement depuis le binding R2, sans récupération 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 Accès R2 public — utilisent à la place l’endpoint de transformation propre à l’adaptateur, qui récupère le fichier via HTTP avant de le transformer.

Vous n’avez pas besoin de déclarer le binding. @astrojs/cloudflare l’ajoute à la configuration du 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 chaque fois que le service d’images à l’exécution 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 évidente :

{
	"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 oriente wrangler deploy vers le fichier fusionné (dist/server/wrangler.json par défaut). Cherchez-y une entrée images.

Cloudflare facture ces transformations comme des 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 au cours de ce mois sont gratuites. Si un site a 500 images sources et demande une taille de miniature et une taille de héros pour chaque image, ces deux jeux de paramètres comptent pour 1 000 images transformées ce mois-là. Le plan Images Free couvre 5 000 transformations uniques par mois. Au-delà de cette limite, les transformations 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é rattaché à une application Access. La valeur d’audience est un paramètre d’exécution secret ; 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-mails par défaut. La connexion par lien magique, les invitations d’équipe et les notifications de commentaires renvoient Email is not configured tant qu’un plugin d’e-mail n’est pas actif.

Le plugin d’e-mail Cloudflare utilise un binding send_email. Enregistrez et vérifiez d’abord le domaine 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: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]",
		}),
	],
}),

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 désigne un autre. S’il est le seul fournisseur d’e-mail actif, EmDash le sélectionne automatiquement. Si plusieurs fournisseurs sont actifs, choisissez le fournisseur Cloudflare sous Settings → Email. L’adresse facultative replyTo reçoit les réponses sans modifier 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 les avoir déployés, ouvrez Cloudflare AI Search dans l’administration, choisissez les collections et exécutez Sync All Content. La synchronisation initiale indexe le contenu publié avant l’activation du plugin ; les hooks maintiennent ensuite les modifications synchronisées.

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 à un layout. Le slot de déclenchement 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 placez pas dans wrangler.jsonc et ne les lisez pas à partir des valeurs import.meta.env au moment du build.

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

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

EmDash lit ses secrets depuis process.env à l’exécution. Le code du Worker lit les bindings depuis env, importé de cloudflare:workers. Ne lisez jamais les secrets via import.meta.env : Vite remplace ces valeurs au moment du 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 dans la base de données, sauf si vous fournissez des remplacements à l’exécution. Secrets and key management liste les variables exactes, les emplacements de stockage et les effets de la rotation.

Déploiements de prévisualisation

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

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 D1, R2 et sandbox du cœur ont cette forme après que Wrangler a écrit les identifiants de ressources :

{
	"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 facultatifs KV, AI Search, e-mail et autres lorsque la prévisualisation utilise ces fonctionnalités. Ajoutez les secrets propres à 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 du cœur 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 à l’administration, l’envoi de médias et tout binding facultatif avant de la partager. Ne pointez jamais un binding de prévisualisation vers une base de données ou un bucket de production.

Vérifier le déploiement

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

Dépannage

« D1 binding not found »

Vérifiez que le nom du binding dans wrangler.jsonc correspond à la configuration de votre 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 journaux du Worker (wrangler tail) et reproduisez l’erreur pour capturer le message sous-jacent — puis ouvrez une issue avec cette sortie.