Cloudflare Workers bietet eine schnelle, global verteilte Laufzeitumgebung für EmDash. Dieser Leitfaden behandelt die Bereitstellung mit D1 für die Datenbank und R2 für die Medienspeicherung.
Voraussetzungen
- Ein Cloudflare-Konto
- Wrangler CLI installiert (
npm install -g wrangler) - Bei Cloudflare authentifiziert (
wrangler login)
Bindings konfigurieren
Erstellen Sie die Produktions-D1-Datenbank und den R2-Bucket, dann erstellen Sie wrangler.jsonc im Projektstammverzeichnis mit Bindings für deren unveränderliche IDs und Namen. Die Datenbankbereitstellung ist getrennt von der Anwendung der EmDash-Schema-Migrationen.
{
"$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",
},
],
}
Dies sind die Bindings, die Sie selbst konfigurieren. Der @astrojs/cloudflare-Adapter fügt bei der Generierung der bereitgestellten Worker-Konfiguration weitere hinzu. Eines davon ist das IMAGES-Binding, das Medientransformationen verwenden — siehe Bildtransformation.
Sandboxed Plugins — Marketplace-Installationen und die Plugins unter sandboxed: [] — benötigen ein worker_loaders-Binding und einen Worker-Einstiegspunkt, der PluginBridge exportiert. Siehe Plugin-Sandbox.
EmDash konfigurieren
Die folgende Astro-Konfiguration verwendet die D1- und R2-Bindings.
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(), // Erforderlich — die Admin-UI ist eine React-App
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
Migrieren und Bereitstellen
Laufzeit-Migrationen bleiben standardmäßig automatisch. Für Deployment-verwaltete Migrationen erstellen Sie den Worker und inspizieren das bereitgestellte D1-Ziel anhand seiner Account- und Datenbank-UUID.
pnpm build
pnpm exec emdash migrate --status --json \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--d1 "$D1_DATABASE_ID"
Nach Überprüfung und Aufzeichnung des gemeldeten Ziel-Fingerprints wenden Sie die Migrationen an und deployen denselben Build.
pnpm exec emdash migrate \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--d1 "$D1_DATABASE_ID" \
--expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy
Der Migrationsjob erfordert CLOUDFLARE_API_TOKEN mit D1-Edit-Berechtigung. Serialisieren Sie Jobs nach Account- und Datenbank-UUID. Siehe Kern-Datenbank-Migrationen verwalten für Bereitstellung, CI-Nebenläufigkeit, Laufzeitmodi und Wiederherstellungsanleitung.
Wenn die Datenbank leer ist (keine Sammlungen) und der Setup-Assistent nicht abgeschlossen wurde, wendet EmDash beim ersten Start auch eine Seed-Datei an. Der Seed wird zur Build-Zeit aus .emdash/seed.json, dem Pfad in package.json#emdash.seed oder seed/seed.json gelesen — je nachdem, was zuerst gefunden wird — und in das Bundle eingefügt. Wenn keiner vorhanden ist, wird ein eingebauter Standard-Seed verwendet. Nachfolgende Deployments gegen eine bestehende Datenbank lassen deren Inhalt unverändert.
Um das Schema oder Inhaltsmodell einer bereits bereitgestellten Website zu ändern, siehe Eine bereitgestellte Website weiterentwickeln.
Geplante Aufgaben
Cloudflare führt geplante Veröffentlichungen, Plugin-Aufgaben und allgemeine Wartung über einen Cron-Trigger aus.
Verwenden Sie den Standard-Worker-Einstiegspunkt:
import handler, {
createScheduledHandler,
PluginBridge,
} from "@emdash-cms/cloudflare/worker";
export { PluginBridge };
export default {
...handler,
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;
Konfigurieren Sie einen Cron-Trigger für allgemeine Wartung in wrangler.jsonc:
{
"triggers": {
"crons": ["* * * * *"],
},
}
Um einen anderen allgemeinen Wartungsplan zu verwenden, setzen Sie generalCron in createScheduledHandler() und verwenden denselben Ausdruck in wrangler.jsonc.
Bereitstellen
Auf Cloudflare Workers bereitstellen:
wrangler deploy
Ihre Website ist jetzt live unter https://my-emdash-site.<your-subdomain>.workers.dev.
Lesereplikate
Für global verteilte Websites aktivieren Sie die D1-Lesereplikation, um Leseabfragen an nahe gelegene Replikate zu leiten, anstatt immer die primäre Datenbank abzufragen. Dies reduziert die Latenz für Besucher, die weit von der primären Region entfernt sind, erheblich.
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
storage: r2({ binding: "MEDIA" }),
}),
Sie müssen auch die Lesereplikation für die D1-Datenbank selbst im Cloudflare-Dashboard oder über die REST-API aktivieren.
Siehe Datenbankoptionen — Lesereplikate für Session-Modi und wie Bookmark-basierte Konsistenz funktioniert.
Objekt-Cache
Um die Leselast auf D1 zu reduzieren, cachen Sie Inhalts- und Konfigurationsabfrageergebnisse in Cloudflare KV. Lesevorgänge werden aus KV bedient, anstatt bei jeder Anfrage die Datenbank abzufragen:
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
Siehe Objekt-Cache für KV-Setup, Optionen und Invalidierungsverhalten.
Workers Cache
Cloudflares Workers Cache setzt einen Edge-Cache vor Ihren Worker: passende Anfragen werden bedient, ohne Ihren Worker auszuführen.
Aktivieren
- Aktivieren Sie den Plattform-Cache in
wrangler.jsonc:
{
"cache": {
"enabled": true,
},
}
- Verwenden Sie Astros Cloudflare-Cache-Provider, damit Routenregeln /
Astro.cachedie richtigen Header setzen und die Invalidierung nativescache.purge()verwendet:
import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({
adapter: cloudflare(),
cache: {
provider: cacheCloudflare(),
},
routeRules: {
"/": { maxAge: 300, swr: 86400 },
// …
},
});
Mit cacheCloudflare() fügt der @astrojs/cloudflare-Adapter auch "cache": { "enabled": true } in die generierte Wrangler-Konfiguration ein, wenn es fehlt — die explizite Auflistung in Ihrer eigenen wrangler.jsonc macht die Absicht deutlich.
- Aus dem Worker mit der Plattform-API bereinigen (keine Cloudflare-REST-Anmeldedaten):
import { cache } from "cloudflare:workers";
await cache.purge({ purgeEverything: true });
// oder: await cache.purge({ tags: ["posts"] });
EmDash-Admin- und API-Antworten senden bereits Cache-Control: private, no-store und werden nie gespeichert. Öffentliche Seiten steuern ihr eigenes Caching über Cache-Control / routeRules / Astro.cache.
Zwei Dinge, die Sie vor der Aktivierung wissen sollten:
- Antworten ohne
Cache-Control-Header werden trotzdem gecacht. Workers Cache wendet die heuristische Frische nach RFC 9111 an — eine200ohne Header wird 2 Stunden gecacht. Geben Sie jeder benutzerdefinierten Route ein explizitesCache-Control(verwenden Sieprivate, no-storefür alles Session-abhängige). - Gecachte Seiten werden mit eingeloggten Redakteuren geteilt. Der Cache läuft vor Ihrem Worker, daher kann er nicht basierend auf Request-Cookies umgangen werden. Ein eingeloggter Redakteur kann die gecachte anonyme Variante einer öffentlichen Seite erhalten — ohne die visuelle Bearbeitungs-Toolbar — bis der Eintrag abläuft. Vom Redakteur gerenderte Antworten selbst werden nie gespeichert (sie tragen
private, no-store), sodass in der anderen Richtung nichts durchsickert.
Nicht dasselbe wie cloudflareCache() von @emdash-cms/cloudflare
| Bevorzugt: Workers Caching | Legacy: cloudflareCache() | |
|---|---|---|
| Konfiguration | "cache": { "enabled": true } + cacheCloudflare() von @astrojs/cloudflare/cache | cache: { provider: cloudflareCache() } von @emdash-cms/cloudflare |
| Speicher | Plattform Workers Caching | Cache API (caches.open / put / match) |
| Bereinigung | cache.purge() von cloudflare:workers | Zone REST POST /zones/{id}/purge_cache |
| Secrets | Keine für die Bereinigung | CF_ZONE_ID + CF_CACHE_PURGE_TOKEN |
Verwenden Sie den bevorzugten Pfad für neue Websites. Behalten Sie cloudflareCache() nur bei, wenn Sie bereits von seinem Cache-API-Verhalten abhängen.
Verwechseln Sie auch keines von beiden mit Objekt-Cache (objectCache: kvCache({ binding: "CACHE" })), der Datenbankabfrageergebnisse in KV cached — eine separate Schicht unter dem Worker.
Benutzerdefinierte Domain
Fügen Sie eine benutzerdefinierte Domain im Cloudflare-Dashboard hinzu:
- Gehen Sie zu Workers & Pages > Ihr Worker
- Klicken Sie auf Custom Domains > Add Custom Domain
- Geben Sie Ihre Domain ein und folgen Sie den DNS-Setup-Anweisungen
Öffentlicher R2-Zugriff
Um Medien direkt aus R2 bereitzustellen (empfohlen für Performance):
- Gehen Sie im Cloudflare-Dashboard zu R2 > Ihr Bucket
- Klicken Sie auf Settings > Public access
- Aktivieren Sie den öffentlichen Zugriff und notieren Sie die öffentliche URL
- Aktualisieren Sie Ihre Speicherkonfiguration:
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxx.r2.dev"
}),
Bildtransformation
EmDash skaliert und kodiert R2-Medien innerhalb des Workers über Cloudflares IMAGES-Binding neu. Die Image-Komponente von emdash/ui und Bilder in Rich Text rendern beide über den Bild-Endpoint, den EmDash unter dem Cloudflare-Adapter installiert. Für Medien auf der internen Route /_emdash/api/media/file/… liest dieser Endpoint die Quelldaten direkt aus dem R2-Binding, ohne einen HTTP-Fetch. Diese Transformationen funktionieren weiterhin hinter Cloudflare Access und mit global_fetch_strictly_public. Medien, die von einer Bucket-URL bereitgestellt werden — siehe Öffentlicher R2-Zugriff — verwenden stattdessen den eigenen Transform-Endpoint des Adapters, der die Datei über HTTP abruft, bevor sie transformiert wird.
Sie müssen das Binding nicht deklarieren. @astrojs/cloudflare fügt es der Worker-Konfiguration hinzu, die es während astro build generiert, genauso wie es cache für Workers Caching hinzufügt. Es tut dies, wann immer der Laufzeit-Image-Service cloudflare-binding ist: imageService nicht gesetzt, der String selbst oder { runtime: "cloudflare-binding" }. Jeder andere Wert — "passthrough", "compile", "cloudflare", "custom" — lässt das Binding weg. Die Auflistung in Ihrer eigenen wrangler.jsonc macht die Absicht deutlich:
{
"images": {
"binding": "IMAGES",
},
}
Um zu sehen, was ein Deployment tatsächlich erhält, lesen Sie die generierte Konfiguration statt wrangler.jsonc. Ein Build schreibt .wrangler/deploy/config.json, das wrangler deploy auf die zusammengeführte Datei (dist/server/wrangler.json standardmäßig) verweist. Suchen Sie dort nach einem images-Eintrag.
Cloudflare berechnet diese Transformationen als Images-Transformationen. Jede einzigartige Kombination aus Quellbild und Parametern wird einmal pro Kalendermonat berechnet, und wiederholte Anfragen innerhalb dieses Monats sind kostenlos. Der kostenlose Images-Plan deckt 5.000 einzigartige Transformationen pro Monat ab. Über dieses Limit hinaus werden gecachte Transformationen weiterhin bereitgestellt, aber neue geben einen 9422-Fehler zurück und die Bildanfrage schlägt fehl.
Cloudflare-Access-Authentifizierung
Wenn Ihre Organisation Cloudflare Access verwendet, können Sie es anstelle von Passkeys als Authentifizierungsanbieter verwenden und Single Sign-On über Ihren bestehenden Identitätsanbieter ermöglichen. Die folgende Konfiguration aktiviert es:
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,
},
}),
}),
Siehe den Authentifizierungsleitfaden für vollständige Konfigurationsoptionen.
Cloudflare AI Search
Das AI-Search-Plugin indiziert veröffentlichte EmDash-Inhalte und fügt Ihrer Website eine intelligente Suchoberfläche hinzu.
-
Registrieren Sie das Plugin im
plugins-Array, das an EmDash übergeben wird:import { aiSearch } from "@emdash-cms/cloudflare/plugins"; // ... plugins: [ formsPlugin(), aiSearch(), ], -
Fügen Sie das AI-Search-Namespace-Binding zu Ihrer Worker-Konfiguration hinzu:
{ "ai_search_namespaces": [ { "binding": "AI_SEARCH", "namespace": "default", }, ], } -
Erstellen Sie den Such-Endpoint, der von der Suchoberfläche verwendet wird:
export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search"; -
Fügen Sie die Suchoberfläche zu Ihrem Seitenlayout hinzu. Der Trigger-Slot kann jeden Button enthalten, der zum Design Ihrer Website passt:
--- import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro"; --- <AISearchSnippet apiUrl="/api/ai-search" placeholder="Suchen..."> <button slot="trigger" type="button">Suchen</button> </AISearchSnippet> -
Stellen Sie die Website bereit:
pnpm exec wrangler deploy -
Öffnen Sie Cloudflare AI Search im EmDash-Admin-Panel, wählen Sie die zu indizierenden Sammlungen und klicken Sie auf Sync All Content.
Diese erste Synchronisation ist erforderlich: Die Content-Hooks des Plugins werden nur für Inhalte ausgelöst, die nach der Aktivierung erstellt oder aktualisiert wurden, sodass alles, was vorher veröffentlicht wurde, im Index fehlt, bis Sie eine vollständige Synchronisation durchführen.
Nach dem Setup veröffentlichte oder aktualisierte Inhalte werden automatisch synchron gehalten. Dieselbe Seite zeigt den Indizierungsfortschritt.
Auf Workers ist der einzige eingebaute email:deliver-Handler ein Dev-Console-Stub, sodass E-Mail-abhängige Abläufe — Magic-Link-Login, Team-Einladungen und Kommentarbenachrichtigungen — in der Produktion mit “Email is not configured” fehlschlagen. Das cloudflareEmail()-Plugin liefert echte E-Mails über Cloudflare Email Sending mit einem nativen send_email-Worker-Binding, ohne externe API-Schlüssel.
1. Absenderdomain einrichten
Gehen Sie im Cloudflare-Dashboard zu Email und verifizieren Sie die Domain (oder Adresse), von der Sie senden. Email Sending lehnt Nachrichten von nicht verifizierten Absendern ab.
2. Binding hinzufügen
Deklarieren Sie ein send_email-Binding in wrangler.jsonc:
{
"send_email": [{ "name": "EMAIL" }],
}
3. Provider registrieren
Fügen Sie das Plugin zu Ihrer emdash()-Integration hinzu:
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]", // optional
binding: "EMAIL", // optional, Standard ist "EMAIL"
}),
],
}),
4. Aktivieren und auswählen
Deployen Sie, dann aktivieren Sie das Plugin unter Admin → Extensions und wählen Sie es als Provider unter Settings → Email aus.
Optionen
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
from | string | { email, name? } | — (erforderlich) | Absenderadresse auf einer für Email Sending eingerichteten Domain. |
replyTo | string | — | Optionales Reply-To, nützlich wenn from eine No-Reply-Subdomain-Adresse ist. |
binding | string | "EMAIL" | Name des send_email-Bindings in wrangler.jsonc. |
Umgebungsvariablen
Empfohlen: Verschlüsselungsschlüssel
EMDASH_ENCRYPTION_KEY ist der Schlüssel zur Verschlüsselung von Plugin-Geheimnissen im Ruhezustand (Webhook-Tokens, Turnstile-Schlüssel usw.). Der Schlüssel wird beim Start validiert; die Plugin-Geheimnis-Verschlüsselung verwendet ihn nach der Aktivierung. Setzen Sie ihn bei jedem Deployment, damit Geheimnisse ohne spätere Konfigurationsänderung geschützt sind.
Der Schlüssel wird von Ihnen bereitgestellt und nie in der Datenbank gespeichert; nur verschlüsselter Chiffretext wird gespeichert. Der Verlust bedeutet den Verlust jedes damit verschlüsselten Geheimnisses.
Generieren Sie einen Schlüssel und speichern Sie ihn als Worker-Secret mit folgenden Befehlen:
npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY
Optional: Stabile-Wert-Überschreibungen
EmDash generiert automatisch das Preview-HMAC-Geheimnis und den Kommentator-IP-Hash-Salt und persistiert sie bei der ersten Verwendung in der Datenbank. Die unten stehenden Umgebungsvariablen sind Überschreibungen für Fälle, in denen Sie den Wert selbst festlegen müssen — zum Beispiel, wenn ein Preview-Worker in einem separaten Prozess das Geheimnis mit Ihrer Hauptsite teilen muss.
| Variable | Zweck |
|---|---|
EMDASH_PREVIEW_SECRET | Überschreibung für das automatisch generierte Preview-HMAC-Geheimnis. |
EMDASH_IP_SALT | Überschreibung für den automatisch generierten Kommentator-IP-Hash-Salt. |
EMDASH_AUTH_SECRET | Optional. Wenn gesetzt, wird es als IP-Salt-Quelle verwendet (sofern nicht auch EMDASH_IP_SALT gesetzt ist, das Vorrang hat), um Kommentator-IP-Hashes für Installationen stabil zu halten, die bereits darauf angewiesen sind. Für ein neues Deployment nicht setzen. |
Greifen Sie auf Umgebungsvariablen in Ihrer Konfiguration mit import.meta.env oder dem Cloudflare-env-Binding zu.
Für das vollständige Inventar aller Geheimnisse, die EmDash verwendet — einschließlich Speicherorten, Rotationsschritten und was bei Schlüsselverlust kaputt geht — siehe Geheimnisse & Schlüsselverwaltung.
Preview-Deployments
Deployen Sie einen Preview-Branch:
wrangler deploy --env preview
Fügen Sie einen Umgebungsabschnitt zu wrangler.jsonc hinzu:
{
"env": {
"preview": {
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db-preview",
},
],
},
},
}
Fehlerbehebung
”D1 binding not found”
Überprüfen Sie, ob der Binding-Name in wrangler.jsonc mit Ihrer Datenbankkonfiguration übereinstimmt:
// Muss übereinstimmen: d1({ binding: "DB" })
"binding": "DB"
“R2 binding not found”
Überprüfen Sie, ob der R2-Bucket korrekt gebunden ist:
// Muss übereinstimmen: r2({ binding: "MEDIA" })
"binding": "MEDIA"
Migrationsfehler
Wenn Sie Schema-Fehler sehen, verfolgen Sie die Worker-Logs (wrangler tail) und reproduzieren Sie den Fehler, um die zugrunde liegende Meldung zu erfassen — erstellen Sie dann ein Issue mit dieser Ausgabe.