Déployer sur Cloudflare

Sur cette page

Cloudflare Workers fournit un environnement d’exécution rapide et distribué globalement pour EmDash. Ce guide couvre le déploiement avec D1 pour la base de données et R2 pour le stockage des médias.

Prérequis

  • Un compte Cloudflare
  • Wrangler CLI installé (npm install -g wrangler)
  • Authentifié avec Cloudflare (wrangler login)

Configurer les bindings

Provisionnez la base de données D1 de production et le bucket R2, puis créez wrangler.jsonc à la racine de votre projet avec des bindings pour leurs IDs et noms immuables. Le provisionnement de la base de données est séparé de l’application des migrations de schéma d’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",
		},
	],
}

Ce sont les bindings que vous configurez vous-même. L’adaptateur @astrojs/cloudflare en ajoute d’autres lors de la génération de la configuration du Worker déployé. L’un d’eux est le binding IMAGES utilisé par les transformations de médias — voir Transformation d’images.

Les plugins sandboxés — installations du marketplace et plugins sous sandboxed: [] — nécessitent un binding worker_loaders et un point d’entrée Worker qui exporte PluginBridge. Voir Plugin 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 } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // Requis — l'UI admin est une app React
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

Migrer et déployer

Les migrations à l’exécution restent automatiques par défaut. Pour des migrations gérées par le déploiement, construisez le Worker et inspectez la cible D1 provisionnée en utilisant son UUID de compte et de base de données.

pnpm build
pnpm exec emdash migrate --status --json \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID"

Après avoir examiné et enregistré l’empreinte cible signalée, appliquez les migrations et déployez le même build.

pnpm exec emdash migrate \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID" \
  --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy

Le job de migration nécessite CLOUDFLARE_API_TOKEN avec la permission D1 Edit. Sérialisez les jobs par UUID de compte et de base de données. Voir Gérer les migrations de base de données du noyau pour le provisionnement, la concurrence CI, les modes d’exécution et les conseils de récupération.

Si la base de données est vide (pas de collections) et que l’assistant de configuration n’a pas été complété, EmDash applique également 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 — selon ce qui est trouvé en premier — et intégré dans le bundle. Si aucun n’est présent, un seed par défaut intégré est utilisé. Les déploiements suivants contre une base de données existante laissent son contenu intact.

Pour modifier le schéma ou le modèle de contenu d’un site déjà déployé, voir Faire évoluer un site déployé.

Tâches planifiées

Cloudflare exécute les publications planifiées, les tâches de plugins et la maintenance générale depuis un Cron Trigger.

Utilisez le point d’entrée Worker standard :

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

export { PluginBridge };

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

Configurez un Cron Trigger pour la maintenance générale dans wrangler.jsonc :

{
	"triggers": {
		"crons": ["* * * * *"],
	},
}

Pour utiliser un planning de maintenance générale différent, définissez generalCron dans createScheduledHandler() et utilisez la même expression dans wrangler.jsonc.

Déployer

Déployez sur Cloudflare Workers :

wrangler deploy

Votre site est maintenant en ligne à https://my-emdash-site.<your-subdomain>.workers.dev.

Répliques de lecture

Pour les sites distribués globalement, activez la réplication de lecture D1 pour router les requêtes de lecture vers des répliques proches au lieu de toujours interroger la base de données primaire. Cela réduit considérablement la latence pour les visiteurs éloignés de la région primaire.

emdash({
	database: d1({
		binding: "DB",
		session: "auto",
	}),
	storage: r2({ binding: "MEDIA" }),
}),

Vous devez également activer la réplication de lecture sur la base de données D1 elle-même dans le tableau de bord Cloudflare ou via l’API REST.

Voir Options de base de données — Répliques de lecture pour les modes de session et comment fonctionne la cohérence basée sur les signets.

Cache d’objets

Pour réduire la charge de lecture sur D1, mettez en cache les résultats des requêtes de contenu et de configuration dans Cloudflare KV. 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" }),
}),

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

Workers Cache

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

L’activer

  1. Activez le cache de plateforme dans wrangler.jsonc :
{
	"cache": {
		"enabled": true,
	},
}
  1. Utilisez le fournisseur de cache Cloudflare d’Astro pour que les règles de route / Astro.cache définissent les bons en-têtes et que l’invalidation utilise le cache.purge() natif :
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
	adapter: cloudflare(),
	cache: {
		provider: cacheCloudflare(),
	},
	routeRules: {
		"/": { maxAge: 300, swr: 86400 },
		// …
	},
});

Avec cacheCloudflare(), l’adaptateur @astrojs/cloudflare injecte aussi "cache": { "enabled": true } dans la configuration Wrangler générée quand elle est manquante — la lister explicitement dans votre propre wrangler.jsonc rend l’intention évidente.

  1. Purgez depuis le Worker avec l’API de plateforme (pas de credentials REST Cloudflare) :
import { cache } from "cloudflare:workers";

await cache.purge({ purgeEverything: true });
// ou : await cache.purge({ tags: ["posts"] });

Les réponses admin et 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 choses à savoir 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 — une 200 sans 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 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é sur la base des cookies de requête. Un éditeur connecté peut recevoir la variante anonyme mise en cache d’une page publique — sans la barre d’outils d’édition visuelle — jusqu’à ce que l’entrée expire. Les réponses rendues par les éditeurs elles-mêmes ne sont jamais stockées (elles portent private, no-store), donc rien ne fuit dans l’autre direction.

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

Préféré : Workers CachingLegacy : cloudflareCache()
Configuration"cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } de @emdash-cms/cloudflare
StockagePlateforme 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. Gardez cloudflareCache() uniquement si vous dépendez déjà de son comportement Cache API.

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

Domaine personnalisé

Ajoutez un domaine personnalisé dans le tableau de bord Cloudflare :

  1. Allez à Workers & Pages > votre worker
  2. Cliquez sur Custom Domains > Add Custom Domain
  3. Entrez votre domaine et suivez les instructions de configuration DNS

Accès public R2

Pour servir les médias directement depuis R2 (recommandé pour la performance) :

  1. Dans le tableau de bord Cloudflare, allez à R2 > votre bucket
  2. Cliquez sur Settings > Public access
  3. Activez l’accès public et notez l’URL publique
  4. Mettez à jour votre configuration de stockage :
storage: r2({
  binding: "MEDIA",
  publicUrl: "https://pub-xxx.r2.dev"
}),

Transformation d’images

EmDash redimensionne et réencode les médias R2 à l’intérieur du Worker, via le binding IMAGES de Cloudflare. Le composant Image de emdash/ui et les images dans le texte riche rendent tous deux via le point de terminaison d’image qu’EmDash installe sous l’adaptateur Cloudflare. Pour les médias sur la route interne /_emdash/api/media/file/…, ce point de terminaison 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 Accès public R2 — empruntent plutôt le propre point de terminaison de transformation de l’adaptateur, qui récupère le fichier via 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 chaque fois que le service d’image à 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 reçoit 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 transformations Images. 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. Le plan Images gratuit couvre 5 000 transformations uniques par mois. Au-delà de cette limite, les transformations en cache sont toujours servies, mais les nouvelles retournent une erreur 9422 et la requête d’image échoue.

Authentification Cloudflare Access

Si votre organisation utilise Cloudflare Access, vous pouvez l’utiliser comme fournisseur d’authentification au lieu des passkeys, donnant l’authentification unique via votre fournisseur d’identité existant. La configuration suivante l’active :

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,
    },
  }),
}),

Voir le guide d’authentification pour les options de configuration complètes.

Le plugin AI Search indexe le contenu EmDash publié et ajoute une interface de recherche intelligente à votre site.

  1. Enregistrez le plugin dans le tableau plugins passé à EmDash :

    import { aiSearch } from "@emdash-cms/cloudflare/plugins";
    
    // ...
    plugins: [
    	formsPlugin(),
    	aiSearch(),
    ],
  2. Ajoutez le binding de namespace AI Search à votre configuration Worker :

    {
    	"ai_search_namespaces": [
    		{
    			"binding": "AI_SEARCH",
    			"namespace": "default",
    		},
    	],
    }
  3. Créez le point de terminaison de recherche utilisé par l’interface de recherche :

    export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
  4. Ajoutez l’interface de recherche à la mise en page de votre site. Le slot trigger peut contenir n’importe quel bouton qui correspond au design de votre site :

    ---
    import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
    ---
    
    <AISearchSnippet apiUrl="/api/ai-search" placeholder="Rechercher...">
    	<button slot="trigger" type="button">Rechercher</button>
    </AISearchSnippet>
  5. Déployez le site :

    pnpm exec wrangler deploy
  6. Ouvrez Cloudflare AI Search dans le panneau admin EmDash, sélectionnez les collections à indexer et cliquez sur Sync All Content.

    Cette synchronisation initiale est requise : les hooks de contenu du plugin ne se déclenchent que pour le contenu créé ou mis à jour après son activation, donc tout ce qui a été publié avant reste absent de l’index jusqu’à ce que vous lanciez une synchronisation complète.

Le contenu publié ou mis à jour après la configuration est synchronisé automatiquement. La même page montre la progression de l’indexation.

Email

Sur Workers, le seul handler email:deliver intégré est un stub de console de développement, donc les flux dépendant de l’email — connexion par magic-link, invitations d’équipe et notifications de commentaires — échouent avec “Email is not configured” en production. Le plugin cloudflareEmail() livre de vrais emails via Cloudflare Email Sending en utilisant un binding Worker natif send_email, sans clés d’API externes.

1. Intégrer un domaine d’envoi

Dans le tableau de bord Cloudflare, allez à Email et vérifiez le domaine (ou l’adresse) depuis lequel vous envoyez. Email Sending rejette les messages d’expéditeurs non vérifiés.

2. Ajouter le binding

Déclarez un binding send_email dans wrangler.jsonc :

{
	"send_email": [{ "name": "EMAIL" }],
}

3. Enregistrer le fournisseur

Ajoutez le plugin à votre intégration 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]", // optionnel
			binding: "EMAIL", // optionnel, par défaut "EMAIL"
		}),
	],
}),

4. Activer et sélectionner

Déployez, puis activez le plugin sous Admin → Extensions et choisissez-le comme fournisseur sous Settings → Email.

Options

OptionTypePar défautDescription
fromstring | { email, name? }— (requis)Adresse d’expéditeur sur un domaine intégré pour Email Sending.
replyTostringReply-To optionnel, utile quand from est une adresse de sous-domaine no-reply.
bindingstring"EMAIL"Nom du binding send_email dans wrangler.jsonc.

Variables d’environnement

Recommandé : clé de chiffrement

EMDASH_ENCRYPTION_KEY est la clé pour chiffrer les secrets des plugins au repos (tokens webhook, clés Turnstile, etc.). La clé est validée au démarrage ; le chiffrement des secrets de plugins l’utilise une fois activé. Définissez-la à chaque déploiement pour que les secrets soient protégés sans changement de configuration ultérieur.

La clé est fournie par vous et jamais stockée dans la base de données ; seul le texte chiffré est stocké. La perdre signifie perdre chaque secret chiffré avec elle.

Générez une clé et stockez-la comme secret Worker avec les commandes suivantes :

npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY

Optionnel : surcharges de valeurs stables

EmDash génère automatiquement le secret HMAC d’aperçu et le sel de hash d’IP de commentateur et les persiste dans la base de données à la première utilisation. Les variables d’environnement ci-dessous sont des surcharges pour les cas où vous devez fixer la valeur vous-même — par exemple, quand un Worker d’aperçu dans un processus séparé doit partager le secret avec votre site principal.

VariableObjectif
EMDASH_PREVIEW_SECRETSurcharge pour le secret HMAC d’aperçu auto-généré.
EMDASH_IP_SALTSurcharge pour le sel de hash d’IP de commentateur auto-généré.
EMDASH_AUTH_SECRETOptionnel. Si défini, il est utilisé comme source de sel d’IP (sauf si EMDASH_IP_SALT est aussi défini, qui a la priorité), gardant les hash d’IP de commentateur stables pour les installations qui en dépendent déjà. Laissez-le non défini pour un nouveau déploiement.

Accédez aux variables d’environnement dans votre configuration en utilisant import.meta.env ou le binding env de Cloudflare.

Pour l’inventaire complet de chaque secret qu’EmDash utilise — incluant les emplacements de stockage, les étapes de rotation et ce qui casse quand une clé est perdue — voir Secrets et gestion des clés.

Déploiements d’aperçu

Déployez une branche d’aperçu :

wrangler deploy --env preview

Ajoutez une section d’environnement à wrangler.jsonc :

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "emdash-db-preview",
				},
			],
		},
	},
}

Dépannage

”D1 binding not found”

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

// Doit correspondre : d1({ binding: "DB" })
"binding": "DB"

“R2 binding not found”

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

// Doit correspondre : 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 créez un issue avec cette sortie.