Auf Cloudflare bereitstellen

Auf dieser Seite

Diese Anleitung stellt eine EmDash-Website auf Cloudflare Workers bereit, mit D1 als Datenbank und R2 für Medien. Beginnen Sie mit einer EmDash Cloudflare-Vorlage oder wenden Sie die gleiche Konfiguration auf eine bestehende Astro-Website an.

Voraussetzungen

  • Ein Cloudflare-Konto
  • Die Projektabhängigkeiten sind installiert
  • Wrangler ist bei Cloudflare authentifiziert (pnpm wrangler login)

Bindings konfigurieren

Die Cloudflare-Vorlagen enthalten den vollständigen Worker-Einstiegspunkt und benannte D1- und R2-Bindings. Beim ersten Deploy erstellt Wrangler jede Ressource, wenn ihr konfigurierter Name noch nicht existiert. Behalten Sie die Namen in wrangler.jsonc; Wrangler verbindet spätere Deploys mit denselben Ressourcen.

Die Vorlage verwendet die folgenden 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": ["* * * * *"] },
}

Die Namen DB, MEDIA und LOADER müssen mit den EmDash-Adaptern übereinstimmen. Der Cron-Trigger führt geplante Veröffentlichungen, Plugin-Aufgaben, Backups und Wartung aus. Siehe Plugin-Sandbox, wenn die Website sandboxed Plugins verwendet.

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, sandbox } 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" }),
			sandboxRunner: sandbox(),
		}),
	],
});

Wenn die Website keine Marketplace-, Registry- oder sandboxed-Plugins verwendet, lassen Sie sandboxRunner und das LOADER-Binding weg.

Worker-Einstiegspunkt hinzufügen

Der Worker-Einstiegspunkt verbindet Astro mit dem Cron-Trigger und exportiert die Plugin-Brücke:

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

export { PluginBridge };

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

Der PluginBridge-Export ist harmlos, wenn kein sandboxed Plugin installiert ist. Behalten Sie ihn bei, falls dasselbe Projekt später Plugins aktivieren könnte.

Um allgemeine Wartung nach einem anderen Zeitplan als jede Minute auszuführen, übergeben Sie denselben Cron-Ausdruck an createScheduledHandler({ generalCron: "..." }) und an triggers.crons. Wenn sie sich unterscheiden, protokolliert der Handler den unerwarteten Trigger und ignoriert ihn.

Erstellen und bereitstellen

Erstellen und deployen Sie die Website einmal, damit Wrangler die benannte D1-Datenbank und den R2-Bucket bereitstellt. Wrangler verwendet den lokalen Login, der durch pnpm wrangler login erstellt wurde.

pnpm build
pnpm wrangler deploy

Mit dem standardmäßigen auto-Migrationsmodus wendet EmDash ausstehende Core-Migrationen an, wenn der deployte Worker seine erste Anfrage erhält. Verwenden Sie Core-Datenbank-Migrationen verwalten, wenn eine Deployment-Pipeline Migrationen vor neuem Code anwenden muss oder wenn Sie eine Migration inspizieren, prüfen oder wiederherstellen müssen.

Wenn die Datenbank leer ist (keine Collections) und der Einrichtungsassistent noch 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 — was zuerst gefunden wird — und in das Bundle eingebettet. Wenn keiner vorhanden ist, wird ein eingebauter Standard-Seed verwendet. Nachfolgende Deploys gegen eine bestehende Datenbank lassen deren Inhalt unberührt.

Um das Schema oder Inhaltsmodell einer bereits deployten Website zu ändern, siehe Eine deployte Website weiterentwickeln.

Worker nahe D1 platzieren

Cloudflare führt einen Worker standardmäßig nahe dem Besucher aus. EmDash-Server-gerenderte Anfragen machen mehrere D1-Roundtrips, daher verwenden Sie Targeted Placement, um den Worker nahe dem D1-Primary auszuführen und diese Anfragen schneller zu machen.

Wrangler akzeptiert placement.mode: "targeted" mit genau einem Selektor: region, host oder hostname. Wählen Sie den Wert, der den D1-Primary-Standort anvisiert, und fügen Sie das resultierende placement-Objekt zu wrangler.jsonc hinzu. Aktivieren Sie keine D1-Read-Replicas mit Targeted Placement. Belassen Sie EmDash’s session-Einstellung auf dem Standard "disabled", damit Lese- und Schreibvorgänge den nahen Primary verwenden.

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-Einrichtung, Optionen und Invalidierungsverhalten.

Workers Cache

Cloudflares Workers Cache platziert einen Edge-Cache vor Ihren Worker: übereinstimmende Anfragen werden bedient, ohne Ihren Worker überhaupt auszuführen.

Aktivierung

  1. Verwenden Sie Astros Cloudflare-Cache-Anbieter, damit Routen-Regeln und Astro.cache Cache-Header setzen und die Invalidierung cache.purge() verwendet.

    import { cacheCloudflare } from "@astrojs/cloudflare/cache";
    
    export default defineConfig({
     adapter: cloudflare(),
     cache: {
       provider: cacheCloudflare(),
     },
     routeRules: {
       "/": { maxAge: 300, swr: 86400 },
       // Andere öffentliche Routen können andere Cache-Lebenszeiten verwenden.
     },
    });

    Der @astrojs/cloudflare-Adapter erkennt cacheCloudflare() und aktiviert Workers Cache in der generierten Deployment-Konfiguration.

  2. Bereinigen Sie gecachte Antworten aus Worker-Code mit der Plattform-API. Dieser Aufruf benötigt keine Cloudflare-REST-Anmeldedaten.

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // Oder ausgewählte Tags bereinigen:
    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:

  1. Antworten ohne Cache-Control-Header werden trotzdem gecacht. Workers Cache wendet RFC 9111 heuristische Frische an — ein 200 ohne Header wird 2 Stunden gecacht. Geben Sie jeder benutzerdefinierten Route ein explizites Cache-Control (verwenden Sie private, no-store für alles Sitzungsabhängige).
  2. Gecachte Seiten werden mit angemeldeten Editoren geteilt. Der Cache läuft vor Ihrem Worker, kann also nicht basierend auf Request-Cookies umgangen werden. Ein angemeldeter Editor kann die gecachte anonyme Variante einer öffentlichen Seite erhalten — ohne die visuelle Bearbeitungs-Toolbar — bis der Eintrag abläuft. Editor-gerenderte Antworten selbst werden nie gespeichert (sie tragen private, no-store), also leckt nichts in die andere Richtung.

Nicht dasselbe wie cloudflareCache() von @emdash-cms/cloudflare

Bevorzugt: Workers CachingLegacy: cloudflareCache()
Konfiguration"cache": { "enabled": true } + cacheCloudflare() von @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } von @emdash-cms/cloudflare
SpeicherPlatform Workers CachingCache API (caches.open / put / match)
Bereinigungcache.purge() von cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretsKeine für BereinigungCF_ZONE_ID + CF_CACHE_PURGE_TOKEN

Verwenden Sie den bevorzugten Pfad für neue Websites. Behalten Sie cloudflareCache() nur bei, wenn Sie bereits von dessen Cache-API-Verhalten abhängig sind.

Verwechseln Sie auch keines davon mit dem Objekt-Cache (objectCache: kvCache({ binding: "CACHE" })), der Datenbankabfrageergebnisse in KV cached — eine separate Schicht unter dem Worker.

Benutzerdefinierte Domains

Das erste Deployment erhält eine workers.dev-URL. Die benutzerdefinierte Domain muss bereits eine aktive, von Cloudflare verwaltete Domain im selben Konto wie der Worker sein. Nachdem der Worker erfolgreich unter seiner workers.dev-URL antwortet, fügen Sie die Produktionsdomain als Wrangler-Route hinzu:

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

Deployen Sie erneut und überprüfen Sie beide Adressen. Die workers.dev-Adresse während des DNS-Tests verfügbar zu halten hilft, ein Routing-Problem von einem Anwendungsproblem zu unterscheiden.

Öffentlicher R2-Zugriff

Standardmäßig werden Medien über EmDash’s authentifizierte Medienroute bereitgestellt. Wenn der Bucket eine öffentliche benutzerdefinierte Domain hat, setzen Sie diesen Ursprung als publicUrl, damit generierte Medien-URLs ihn verwenden:

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

Öffentlicher Bucket-Zugriff gilt für jedes erreichbare Objekt, nicht nur Medien. Automatische JSON-Backups verwenden das backups/-Präfix im selben Speicher-Backend, daher sollten Sie dieses Präfix nicht über die öffentliche Domain exponieren. Medienspeicher wählen erklärt die sichere Grenze.

Bildtransformation

EmDash skaliert und rekodiert R2-Medien innerhalb des Workers über Cloudflares IMAGES-Binding. Die Image-Komponente von emdash/ui und Bilder in Rich Text rendern beide über den Bildendpunkt, den EmDash unter dem Cloudflare-Adapter installiert. Für Medien auf der internen Route /_emdash/api/media/file/… liest dieser Endpunkt die Quellbytes direkt vom 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 Transformationsendpunkt des Adapters, der die Datei per HTTP abruft, bevor er sie transformiert.

Sie müssen das Binding nicht deklarieren. @astrojs/cloudflare fügt es zur 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 offensichtlich:

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

Um zu sehen, was ein Deploy 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 Transformations. Jede einzigartige Kombination aus Quellbild und Parametern wird einmal pro Kalendermonat berechnet, und wiederholte Anfragen innerhalb dieses Monats sind kostenlos. Wenn eine Website 500 Quellbilder hat und eine Thumbnail-Größe und eine Hero-Größe für jedes Bild anfordert, zählen diese zwei Parametersätze als 1.000 transformierte Bilder für diesen Monat. Der Images Free-Plan deckt 5.000 einzigartige Transformationen pro Monat ab. Über diesem Limit hinaus werden gecachte Transformationen weiterhin ausgeliefert, aber neue geben einen 9422-Fehler zurück und die Bildanfrage schlägt fehl.

Cloudflare Access-Authentifizierung

Cloudflare Access kann die Passkey-Authentifizierung durch den an eine Access-Anwendung angehängten Identitätsanbieter ersetzen. Der Audience-Wert ist eine geheime Laufzeiteinstellung; halten Sie ihn aus astro.config.mjs heraus, indem Sie seine Umgebungsvariable benennen:

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

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

Setzen Sie CF_ACCESS_AUDIENCE mit pnpm wrangler secret put CF_ACCESS_AUDIENCE. Die Authentifizierungsanleitung erklärt Benutzerbereitstellung, Standardrollen und Rollensynchronisation.

E-Mail

Produktions-Worker haben keinen Standard-E-Mail-Zustelldienst. Magic-Link-Anmeldung, Team-Einladungen und Kommentarbenachrichtigungen geben E-Mail ist nicht konfiguriert zurück, bis ein E-Mail-Plugin aktiv ist.

Das Cloudflare-E-Mail-Plugin verwendet ein send_email-Binding. Onboarden und verifizieren Sie zuerst die Absenderdomain mit Cloudflare Email Sending. Cloudflare lehnt Nachrichten ab, deren Absenderadresse kein akzeptierter Absender ist.

Fügen Sie das Binding hinzu und registrieren Sie den Provider:

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

Nach dem Deployment aktivieren Sie das Plugin unter Erweiterungen und wählen Sie es unter Einstellungen → E-Mail. Das Senden schlägt fehl, bis der Absender akzeptiert und das Binding vorhanden ist.

Das Plugin verwendet das Binding namens EMAIL, es sei denn, seine binding-Option benennt ein anderes. Wenn es der einzige aktive E-Mail-Provider ist, wählt EmDash ihn automatisch. Wenn mehr als ein Provider aktiv ist, wählen Sie den Cloudflare-Provider unter Einstellungen → E-Mail. Die optionale replyTo-Adresse empfängt Antworten, ohne die akzeptierte Absenderadresse zu ändern.

Das AI-Search-Plugin benötigt sowohl eine native Plugin-Registrierung als auch ein ai_search_namespaces-Binding. Nach dem Deployment öffnen Sie Cloudflare AI Search im Admin, wählen Sie die Collections und führen Sie Alle Inhalte synchronisieren aus. Die erste Synchronisation indiziert Inhalte, die vor der Aktivierung des Plugins veröffentlicht wurden; Hooks halten spätere Änderungen synchronisiert.

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

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

Exponieren Sie die Suchroute von der Website:

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

Fügen Sie die Suchoberfläche zu einem Layout hinzu. Der Trigger-Slot akzeptiert einen Button, der zum Design der Website passt:

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

<AISearchSnippet apiUrl="/api/ai-search" placeholder="Inhalte suchen">
	<button slot="trigger" type="button">Suchen</button>
</AISearchSnippet>

Worker-Secrets

Speichern Sie geheime Werte mit pnpm wrangler secret put <NAME>. Schreiben Sie sie nicht in wrangler.jsonc und lesen Sie sie nicht aus Build-Zeit-import.meta.env-Werten.

EMDASH_ENCRYPTION_KEY verschlüsselt derzeit keine Plugin-Secrets oder andere gespeicherte Daten. Wenn er gesetzt ist, prüft EmDash sein Format beim Start. Ein fehlerhafter Wert erzeugt eine für Betreiber sichtbare Log-Nachricht, aber die Website bearbeitet weiterhin Anfragen. Plugin-Secrets bleiben als Klartext in der Datenbank.

EmDash liest seine Secrets zur Laufzeit aus process.env. Worker-Code liest Bindings aus env, importiert von cloudflare:workers. Lesen Sie Secrets niemals über import.meta.env: Vite ersetzt diese Werte zur Build-Zeit und kann sie in das Server-Bundle schreiben.

Das Preview-HMAC-Secret und der Commenter-IP-Salt werden generiert und in der Datenbank gespeichert, es sei denn, Sie stellen Laufzeit-Überschreibungen bereit. Secrets und Schlüsselverwaltung listet die genauen Variablen, Speicherorte und Rotationseffekte auf.

Preview-Deployments

Benannte Wrangler-Umgebungen erben keine Bindings. Erstellen Sie separate Preview-Ressourcen und schreiben Sie sie in die preview-Umgebung, bevor Sie bauen:

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

Die Preview-Umgebung muss jedes Binding wiederholen, das der Preview-Worker verwendet. Die Core-D1-, R2- und Sandbox-Bindings haben nach dem Schreiben der Ressourcen-Identifier durch Wrangler diese Form:

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

Verwenden Sie die von Wrangler geschriebene Preview-UUID. Wiederholen Sie optionale KV-, AI-Search-, E-Mail- und andere Bindings, wenn die Preview diese Funktionen verwendet. Fügen Sie Preview-spezifische Secrets mit pnpm wrangler secret put <NAME> --env preview hinzu.

Bauen und deployen Sie die Preview-Umgebung. Ihre erste Anfrage wendet ausstehende Core-Migrationen über den Standard-auto-Modus an.

pnpm build
pnpm wrangler deploy --env preview

Überprüfen Sie die Preview-URL, Admin-Anmeldung, Medien-Upload und jedes optionale Binding, bevor Sie sie teilen. Verweisen Sie niemals ein Preview-Binding auf eine Produktionsdatenbank oder einen Produktions-Bucket.

Deployment überprüfen

Nach dem Deployment fordern Sie eine öffentliche Seite an, melden Sie sich bei /_emdash/admin an, laden Sie eine Test-Mediendatei hoch und rufen Sie sie ab, und bestätigen Sie, dass der Scheduled-Handler in pnpm wrangler tail erscheint.

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”

Stellen Sie sicher, dass 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 Nachricht zu erfassen — dann erstellen Sie ein Issue mit dieser Ausgabe.