Auf Cloudflare bereitstellen

Auf dieser Seite

Dieser Leitfaden stellt eine EmDash-Site auf Cloudflare Workers mit D1 für die Datenbank und R2 für Medien bereit. Beginnen Sie mit einer EmDash Cloudflare-Vorlage oder wenden Sie dieselbe Konfiguration auf eine bestehende Astro-Site an.

Voraussetzungen

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

Bindings konfigurieren

Die Cloudflare-Vorlagen enthalten den vollständigen Worker-Entry-Point und benannte D1- und R2-Bindings. Bei der ersten Bereitstellung erstellt Wrangler jede Ressource, wenn ihr konfigurierter Name noch nicht existiert. Behalten Sie die Namen in wrangler.jsonc bei; Wrangler verbindet spätere Bereitstellungen 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 Site Sandbox-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 Site keine Marketplace-, Registry- oder sandboxed-Plugins verwendet, lassen Sie sandboxRunner und das LOADER-Binding weg.

Worker-Entry-Point hinzufügen

Der Worker-Entry-Point verbindet Astro mit dem Cron Trigger und exportiert die Plugin-Bridge:

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 Sandbox-Plugin installiert ist. Behalten Sie ihn, wenn 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 und ignoriert der Handler den unerwarteten Trigger.

Build und Bereitstellung

Erstellen und stellen Sie die Site einmal bereit, damit Wrangler die benannte D1-Datenbank und den R2-Bucket bereitstellen kann. Wrangler verwendet den lokalen Login, der durch pnpm wrangler login erstellt wurde.

pnpm build
pnpm wrangler deploy

Im Standard-auto-Migrationsmodus wendet EmDash ausstehende Core-Migrationen an, wenn der bereitgestellte Worker seine erste Anfrage erhält. Verwenden Sie Core-Datenbankmigrationen verwalten, wenn eine Bereitstellungspipeline Migrationen anwenden muss, bevor neuer Code Traffic erhält, oder wenn Sie eine Migration inspizieren, prüfen oder wiederherstellen müssen.

Wenn die Datenbank leer ist (keine Sammlungen) und der Einrichtungsassistent 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 eingebunden. Wenn keines vorhanden ist, wird ein eingebauter Standard-Seed verwendet. Nachfolgende Bereitstellungen gegen eine bestehende Datenbank lassen deren Inhalt unverändert.

Um das Schema oder Content-Modell einer bereits bereitgestellten Site zu ändern, siehe Eine bereitgestellte Site weiterentwickeln.

Worker nahe bei D1 platzieren

Cloudflare führt standardmäßig einen Worker in der Nähe des Besuchers aus. EmDash server-gerenderte Anfragen führen mehrere D1-Roundtrips durch, verwenden Sie daher Targeted Placement, um den Worker in der Nähe des D1-Primärs 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-Primärstandort anvisiert, und fügen Sie das resultierende placement-Objekt zu wrangler.jsonc hinzu. Aktivieren Sie keine D1-Read-Replicas mit Targeted Placement. Behalten Sie EmDashs session-Einstellung auf dem Standardwert "disabled", damit Lese- und Schreibvorgänge den nahen Primär verwenden.

Object Cache

Um die Leselast auf D1 zu reduzieren, cachen Sie Inhalts- und Konfigurationsabfrageergebnisse in Cloudflare KV. Lesevorgänge werden von KV bereitgestellt, 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 Object Cache für KV-Setup, Optionen und Invalidierungsverhalten.

Workers Cache

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

Aktivieren

  1. Verwenden Sie Astros Cloudflare-Cache-Provider, damit Routenregeln und Astro.cache Cache-Header setzen und 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 unterschiedliche Cache-Lebensdauern verwenden.
     },
    });

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

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

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // Oder ausgewählte Tags löschen:
    await cache.purge({ tags: ["posts"] });

EmDash-Admin- und API-Antworten senden bereits Cache-Control: private, no-store und werden niemals gespeichert. Öffentliche Seiten steuern ihr eigenes Caching durch Cache-Control / routeRules / Astro.cache.

Zwei Dinge, die Sie wissen sollten, bevor Sie es aktivieren:

  1. Antworten ohne Cache-Control-Header werden dennoch gecacht. Workers Cache wendet RFC 9111 heuristische Frische an — eine 200 ohne Header wird 2 Stunden lang gecacht. Geben Sie jeder benutzerdefinierten Route einen expliziten Cache-Control (verwenden Sie private, no-store für alles sitzungsabhängige).
  2. Gecachte Seiten werden mit angemeldeten Redakteuren geteilt. Der Cache läuft vor Ihrem Worker, sodass er nicht basierend auf Request-Cookies umgehen kann. Ein angemeldeter Redakteur erhält möglicherweise die gecachte anonyme Variante einer öffentlichen Seite — ohne die visuelle Bearbeitungssymbolleiste — bis der Eintrag abläuft. Von Redakteuren gerenderte Antworten selbst werden niemals gespeichert (sie tragen private, no-store), sodass nichts in die andere Richtung leckt.

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

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

Verwenden Sie den bevorzugten Pfad für neue Sites. Behalten Sie cloudflareCache() nur bei, wenn Sie bereits von seinem Cache-API-Verhalten abhängen.

Verwechseln Sie auch keines von beiden mit Object Cache (objectCache: kvCache({ binding: "CACHE" })), das Datenbankabfrageergebnisse in KV cached — eine separate Schicht unter dem Worker.

Custom Domains

Die erste Bereitstellung erhält eine workers.dev-URL. Die benutzerdefinierte Domain muss bereits eine aktive Domain sein, die von Cloudflare im selben Konto wie der Worker verwaltet wird. 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 }],
}

Stellen Sie erneut bereit und überprüfen Sie beide Adressen. Das Verfügbarhalten der workers.dev-Adresse während des DNS-Tests hilft, ein Routing-Problem von einem Anwendungsproblem zu unterscheiden.

Öffentlicher R2-Zugriff

Standardmäßig werden Medien über EmDashs authentifizierte Medienroute bereitgestellt. Wenn der Bucket eine öffentliche benutzerdefinierte Domain hat, setzen Sie diesen Origin 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 für Medien. Automatische JSON-Backups verwenden das backups/-Präfix im selben Speicher-Backend, also exponieren Sie dieses Präfix nicht über die öffentliche Domain. Medienspeicher wählen erklärt die sichere Grenze.

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 werden beide über den Bild-Endpoint gerendert, den EmDash unter dem Cloudflare-Adapter installiert. Für Medien auf der internen /_emdash/api/media/file/…-Route liest dieser Endpoint die Quellbytes direkt aus dem R2-Binding ohne 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 er sie transformiert.

Sie müssen das Binding nicht deklarieren. @astrojs/cloudflare fügt es der Worker-Konfiguration hinzu, die es während astro build generiert, auf die gleiche Weise wie es cache für Workers Caching hinzufügt. Es tut dies immer dann, wenn der Runtime-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. Es in Ihrem eigenen wrangler.jsonc aufzulisten macht die Absicht offensichtlich:

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

Um zu sehen, was eine Bereitstellung tatsächlich erhält, lesen Sie die generierte Konfiguration anstelle von wrangler.jsonc. Ein Build schreibt .wrangler/deploy/config.json, das wrangler deploy auf die zusammengeführte Datei zeigt (dist/server/wrangler.json standardmäßig). Suchen Sie dort nach einem images-Eintrag.

Cloudflare berechnet diese Transformationen als Images transformations. Jede eindeutige Kombination aus Quellbild und Parametern wird einmal pro Kalendermonat abgerechnet, und wiederholte Anfragen innerhalb dieses Monats sind kostenlos. Wenn eine Site 500 Quellbilder hat und eine Thumbnail-Größe und eine Hero-Größe für jedes Bild anfordert, zählen diese beiden Parametersätze als 1.000 transformierte Bilder für diesen Monat. Der Images Free-Plan umfasst 5.000 eindeutige Transformationen pro Monat. Nach diesem Limit werden gecachte Transformationen weiterhin bereitgestellt, 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 Identitätsanbieter ersetzen, der an eine Access-Anwendung angehängt ist. 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. Der Authentifizierungsleitfaden erklärt Benutzerbereitstellung, Standardrollen und Rollensynchronisation.

E-Mail

Produktions-Worker haben standardmäßig keinen E-Mail-Zustelldienst. Magic-Link-Anmeldung, Team-Einladungen und Kommentarbenachrichtigungen geben Email is not configured zurück, bis ein E-Mail-Plugin aktiv ist.

Das Cloudflare-E-Mail-Plugin verwendet ein send_email-Binding. Onboarding und Verifizierung der Absenderdomain zuerst mit Cloudflare Email Sending. Cloudflare lehnt Nachrichten ab, deren From-Adresse 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: "cms@mails.example.com", name: "My Site CMS" },
			replyTo: "hello@example.com",
		}),
	],
}),

Aktivieren Sie nach der Bereitstellung das Plugin unter Extensions und wählen Sie es unter Settings → Email aus. Das Senden schlägt fehl, bis der Absender akzeptiert ist und das Binding existiert.

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 aus. Wenn mehr als ein Provider aktiv ist, wählen Sie den Cloudflare-Provider unter Settings → Email. Die optionale replyTo-Adresse empfängt Antworten, ohne die akzeptierte From-Adresse zu ändern. Ein Plugin kann replyTo für eine einzelne Nachricht setzen, was diese Option für diese Nachricht überschreibt.

Das AI Search-Plugin benötigt sowohl eine Native-Plugin-Registrierung als auch ein ai_search_namespaces-Binding. Nachdem Sie sie bereitgestellt haben, öffnen Sie Cloudflare AI Search im Admin, wählen Sie die Sammlungen und führen Sie Sync All Content aus. Die anfängliche 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 Site:

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

Fügen Sie die Suchschnittstelle zu einem Layout hinzu. Der Trigger-Slot akzeptiert einen Button, der zum Design der Site passt:

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

Worker-Secrets

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

EMDASH_ENCRYPTION_KEY verschlüsselt Plugin-Einstellungen, die als Secrets deklariert sind. Setzen Sie es, bevor Sie ein Plugin-Secret speichern, und bewahren Sie es getrennt von D1-Backups auf. Während der Rotation geben Sie zuerst den neuen Schlüssel an und behalten ältere Schlüssel nach Kommas bei, bis jedes Plugin-Secret erneut gespeichert wurde. Secrets und Schlüsselverwaltung beschreibt Rotation und Wiederherstellung.

Die Plugin-Bridge liest dieses Worker-Secret-Binding direkt. Die generierte Admin-Einstellungsroute liest es durch process.env. Mit nodejs_compat füllt Cloudflare standardmäßig process.env für Kompatibilitätsdaten am oder nach dem 2025-04-01. Projekte, die auf ein früheres Datum festgelegt sind, müssen auch nodejs_compat_populate_process_env hinzufügen, bevor sie verschlüsselte Einstellungen speichern.

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

Das Preview-HMAC-Secret und das Commenter-IP-Salt werden generiert und in der Datenbank gespeichert, es sei denn, Sie geben Laufzeit-Overrides an. Secrets und Schlüsselverwaltung listet die exakten Variablen, Speicherorte und Rotationseffekte auf.

Preview-Bereitstellungen

Benannte Wrangler-Umgebungen erben keine Bindings. Erstellen Sie separate Preview-Ressourcen und schreiben Sie sie in die preview-Umgebung vor dem 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

Die Preview-Umgebung muss jedes Binding wiederholen, das der Preview-Worker verwendet. Die Core-D1-, R2- und Sandbox-Bindings haben diese Form, nachdem Wrangler die Ressourcen-IDs geschrieben hat:

{
	"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-Only-Secrets mit pnpm wrangler secret put <NAME> --env preview hinzu.

Erstellen und stellen Sie die Preview-Umgebung bereit. Ihre erste Anfrage wendet ausstehende Core-Migrationen durch 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 es teilen. Richten Sie niemals ein Preview-Binding auf eine Produktionsdatenbank oder einen Bucket.

Bereitstellung überprüfen

Fordern Sie nach der Bereitstellung 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 geplante 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”

Ü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 Nachricht zu erfassen — reichen Sie dann ein Issue mit dieser Ausgabe ein.