Questa guida distribuisce un sito EmDash su Cloudflare Workers con D1 per il database e R2 per i media. Parti da un template EmDash Cloudflare o applica la stessa configurazione a un sito Astro esistente.
Prerequisiti
- Un account Cloudflare
- Le dipendenze del progetto installate
- Wrangler autenticato con Cloudflare (
pnpm wrangler login)
Configurare i binding
I template Cloudflare includono il punto di ingresso Worker completo e binding D1 e R2 con nome. Al primo deploy, Wrangler crea la risorsa se il nome configurato non esiste ancora. Mantieni i nomi in wrangler.jsonc; Wrangler riconnette i deploy successivi alle stesse risorse.
Il template usa i seguenti binding:
{
"$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": ["* * * * *"] },
}
I nomi DB, MEDIA e LOADER devono corrispondere agli adapter EmDash. Il Cron Trigger esegue pubblicazione programmata, attività dei plugin, backup e manutenzione. Vedi Plugin sandbox se il sito usa plugin sandboxed.
Configurare EmDash
La seguente configurazione Astro usa i binding D1 e 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(),
}),
],
});
Se il sito non usa plugin marketplace, registry o sandboxed, ometti sandboxRunner e il binding LOADER.
Aggiungere il punto di ingresso Worker
Il punto di ingresso Worker collega Astro al Cron Trigger ed esporta il bridge del plugin:
import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";
export { PluginBridge };
export default {
...handler,
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;
L’export PluginBridge è innocuo quando non è installato alcun plugin sandboxed. Conservalo se lo stesso progetto potrebbe abilitare i plugin in seguito.
Per eseguire la manutenzione generale con una pianificazione diversa da ogni minuto, passa la stessa espressione Cron a createScheduledHandler({ generalCron: "..." }) e a triggers.crons. Se differiscono, l’handler registra e ignora il trigger inatteso.
Compilare e distribuire
Compila e distribuisci il sito una volta per far sì che Wrangler provveda al database D1 e al bucket R2 con nome. Wrangler usa il login locale creato da pnpm wrangler login.
pnpm build
pnpm wrangler deploy
Con la modalità di migrazione predefinita auto, EmDash applica le migrazioni core in sospeso quando il Worker distribuito riceve la prima richiesta. Usa Manage core database migrations quando una pipeline di distribuzione deve applicare le migrazioni prima che il nuovo codice riceva traffico, o quando devi ispezionare, verificare o ripristinare una migrazione.
Se il database è vuoto (nessuna collection) e la procedura guidata di configurazione non è stata completata, EmDash applica anche un file seed al primo avvio. Il seed viene letto in fase di build da .emdash/seed.json, dal percorso in package.json#emdash.seed o da seed/seed.json — il primo trovato — e incorporato nel bundle. Se non ne è presente nessuno, viene usato un seed predefinito integrato. I deploy successivi contro un database esistente lasciano intatto il suo contenuto.
Per modificare lo schema o il modello di contenuto di un sito già distribuito, vedi Evolving a Deployed Site.
Collocare il Worker vicino a D1
Cloudflare esegue un Worker vicino al visitatore per impostazione predefinita. Le richieste EmDash renderizzate sul server effettuano diversi round trip D1, quindi usa Targeted Placement per eseguire il Worker vicino al primario D1 e accelerare quelle richieste.
Wrangler accetta placement.mode: "targeted" con esattamente un selettore: region, host o hostname. Scegli il valore che punta alla posizione del primario D1 e aggiungi l’oggetto placement risultante a wrangler.jsonc. Non abilitare le repliche di lettura D1 con Targeted Placement. Mantieni l’impostazione session di EmDash al valore predefinito "disabled", così letture e scritture usano il primario vicino.
Object cache
Per ridurre il carico di lettura su D1, memorizza in cache i risultati delle query di contenuto e configurazione in Cloudflare KV. Le letture vengono servite da KV invece di interrogare il database a ogni richiesta:
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
Vedi Object Cache per la configurazione KV, le opzioni e il comportamento di invalidazione.
Workers Cache
Il Workers Cache di Cloudflare mette una cache edge davanti al tuo Worker: le richieste corrispondenti vengono servite senza eseguire affatto il Worker.
Abilitarlo
-
Usa il provider di cache Cloudflare di Astro affinché le regole di route e
Astro.cacheimpostino le intestazioni di cache e l’invalidazione usicache.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’adapter
@astrojs/cloudflarerilevacacheCloudflare()e abilita Workers Cache nella configurazione di distribuzione generata. -
Elimina le risposte in cache dal codice Worker con l’API della piattaforma. Questa chiamata non richiede credenziali REST Cloudflare.
import { cache } from "cloudflare:workers"; await cache.purge({ purgeEverything: true }); // Or purge selected tags: await cache.purge({ tags: ["posts"] });
Le risposte di amministrazione e API di EmDash inviano già Cache-Control: private, no-store e non vengono mai memorizzate. Le pagine pubbliche controllano la propria cache tramite Cache-Control / routeRules / Astro.cache.
Due cose da sapere prima di abilitarlo:
- Le risposte senza intestazione
Cache-Controlvengono comunque memorizzate in cache. Workers Cache applica la freschezza euristica RFC 9111 — un200senza intestazione viene memorizzato in cache per 2 ore. Dai a ogni route personalizzata unCache-Controlesplicito (usaprivate, no-storeper tutto ciò che dipende dalla sessione). - Le pagine in cache sono condivise con gli editori connessi. La cache viene eseguita prima del Worker, quindi non può essere aggirata in base ai cookie della richiesta. Un editor connesso può ricevere la variante anonima in cache di una pagina pubblica — senza la barra di modifica visuale — fino alla scadenza della voce. Le risposte renderizzate dall’editor stesse non vengono mai memorizzate (portano
private, no-store), quindi nulla filtra nell’altra direzione.
Non è la stessa cosa di cloudflareCache() da @emdash-cms/cloudflare
| Preferito: Workers Caching | Legacy: cloudflareCache() | |
|---|---|---|
| Config | "cache": { "enabled": true } + cacheCloudflare() da @astrojs/cloudflare/cache | cache: { provider: cloudflareCache() } da @emdash-cms/cloudflare |
| Storage | Platform Workers Caching | Cache API (caches.open / put / match) |
| Purge | cache.purge() da cloudflare:workers | Zone REST POST /zones/{id}/purge_cache |
| Secrets | Nessuno per il purge | CF_ZONE_ID + CF_CACHE_PURGE_TOKEN |
Usa il percorso preferito per i nuovi siti. Mantieni cloudflareCache() solo se dipendi già dal suo comportamento Cache API.
Non confondere nessuno dei due con l’object cache (objectCache: kvCache({ binding: "CACHE" })), che memorizza in cache i risultati delle query del database in KV — un livello separato sotto il Worker.
Domini personalizzati
La prima distribuzione riceve un URL workers.dev. Il dominio personalizzato deve già essere un dominio attivo gestito da Cloudflare nello stesso account del Worker. Dopo che il Worker risponde correttamente al suo URL workers.dev, aggiungi il dominio di produzione come route Wrangler:
{
"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}
Distribuisci di nuovo e verifica entrambi gli indirizzi. Mantenere disponibile l’indirizzo workers.dev durante i test DNS aiuta a distinguere un problema di routing da un problema dell’applicazione.
Accesso R2 pubblico
Per impostazione predefinita, i media vengono serviti tramite la route media autenticata di EmDash. Se il bucket ha un dominio personalizzato pubblico, imposta quell’origine come publicUrl affinché gli URL media generati lo usino:
storage: r2({
binding: "MEDIA",
publicUrl: "https://media.example.com",
}),
L’accesso pubblico al bucket si applica a ogni oggetto raggiungibile, non solo ai media. I backup JSON automatici usano il prefisso backups/ nello stesso backend di storage, quindi non esporre quel prefisso tramite il dominio pubblico. Choose media storage spiega il confine sicuro.
Trasformazione delle immagini
EmDash ridimensiona e ricodifica i media R2 nel Worker, tramite il binding IMAGES di Cloudflare. Il componente Image da emdash/ui e le immagini nel rich text vengono entrambi renderizzati tramite l’endpoint immagine che EmDash installa sotto l’adapter Cloudflare. Per i media sulla route interna /_emdash/api/media/file/…, quell’endpoint legge i byte sorgente direttamente dal binding R2, senza un fetch HTTP. Quelle trasformazioni continuano a funzionare dietro Cloudflare Access e con global_fetch_strictly_public. I media serviti da un URL di bucket — vedi Public R2 Access — prendono invece l’endpoint di trasformazione proprio dell’adapter, che recupera il file via HTTP prima di trasformarlo.
Non devi dichiarare il binding. @astrojs/cloudflare lo aggiunge alla configurazione Worker che genera durante astro build, allo stesso modo in cui aggiunge cache per Workers Caching. Lo fa quando il servizio immagine runtime è cloudflare-binding: imageService non impostato, la stringa stessa, o { runtime: "cloudflare-binding" }. Qualsiasi altro valore — "passthrough", "compile", "cloudflare", "custom" — omette il binding. Elencarlo nel proprio wrangler.jsonc rende chiara l’intenzione:
{
"images": {
"binding": "IMAGES",
},
}
Per vedere cosa ottiene effettivamente un deploy, leggi la configurazione generata invece di wrangler.jsonc. Una build scrive .wrangler/deploy/config.json, che punta wrangler deploy al file unito (dist/server/wrangler.json per impostazione predefinita). Cerca lì una voce images.
Cloudflare fattura queste trasformazioni come Images transformations. Ogni combinazione unica di immagine sorgente e parametri viene fatturata una volta per mese di calendario, e le richieste ripetute in quel mese sono gratuite. Se un sito ha 500 immagini sorgente e richiede una dimensione thumbnail e una hero per ogni immagine, quei due set di parametri contano come 1.000 immagini trasformate per quel mese. Il piano Images Free copre 5.000 trasformazioni uniche al mese. Oltre quel limite, le trasformazioni in cache vengono ancora servite, ma quelle nuove restituiscono un errore 9422 e la richiesta dell’immagine fallisce.
Autenticazione Cloudflare Access
Cloudflare Access può sostituire l’autenticazione con passkey con il provider di identità collegato a un’applicazione Access. Il valore audience è un’impostazione segreta di runtime; tienilo fuori da astro.config.mjs nominando la sua variabile d’ambiente:
import { access } from "@emdash-cms/cloudflare";
emdash({
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audienceEnvVar: "CF_ACCESS_AUDIENCE",
roleMapping: {
Admins: 50,
Editors: 40,
},
}),
}),
Imposta CF_ACCESS_AUDIENCE con pnpm wrangler secret put CF_ACCESS_AUDIENCE. La authentication guide spiega il provisioning degli utenti, i ruoli predefiniti e la sincronizzazione dei ruoli.
I Worker di produzione non hanno un servizio di consegna email predefinito. L’accesso con magic link, gli inviti al team e le notifiche dei commenti restituiscono Email is not configured finché un plugin email non è attivo.
Il plugin email di Cloudflare usa un binding send_email. Prima onboard e verifica il dominio mittente con Cloudflare Email Sending. Cloudflare rifiuta i messaggi il cui From non è un mittente accettato.
Aggiungi il binding e registra il 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",
}),
],
}),
Dopo la distribuzione, attiva il plugin in Extensions e selezionalo in Settings → Email. L’invio fallisce finché il mittente non è accettato e il binding non esiste.
Il plugin usa il binding chiamato EMAIL a meno che la sua opzione binding non ne nomini un altro. Se è l’unico provider email attivo, EmDash lo seleziona automaticamente. Se è attivo più di un provider, scegli il provider Cloudflare in Settings → Email. L’indirizzo opzionale replyTo riceve le risposte senza cambiare il From accettato. Un plugin può impostare replyTo su un singolo messaggio, che sovrascrive questa opzione per quel messaggio.
Cloudflare AI Search
Il plugin AI Search necessita sia di una registrazione di plugin nativo sia di un binding ai_search_namespaces. Dopo averli distribuiti, apri Cloudflare AI Search nell’amministrazione, scegli le collection ed esegui Sync All Content. La sincronizzazione iniziale indicizza il contenuto pubblicato prima dell’abilitazione del plugin; gli hook mantengono sincronizzate le modifiche successive.
import { aiSearch } from "@emdash-cms/cloudflare/plugins";
emdash({
plugins: [aiSearch()],
}),
{
"ai_search_namespaces": [{ "binding": "AI_SEARCH", "namespace": "default" }],
}
Esponi la route di ricerca dal sito:
export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
Aggiungi l’interfaccia di ricerca a un layout. Lo slot del trigger accetta un pulsante che corrisponde al design del sito:
---
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>
Secret del Worker
Memorizza i valori segreti con pnpm wrangler secret put <NAME>. Non metterli in wrangler.jsonc e non leggerli da valori import.meta.env di build.
EMDASH_ENCRYPTION_KEY cifra le impostazioni del plugin dichiarate come secret. Impostalo prima di salvare un secret del plugin e conservalo separatamente dai backup D1. Durante la rotazione, fornisci prima la nuova chiave e mantieni quelle più vecchie dopo le virgole finché ogni secret del plugin non è stato salvato di nuovo. Secrets and key management descrive rotazione e ripristino.
Il bridge del plugin legge questo binding di secret del Worker direttamente. La route generata delle impostazioni di amministrazione lo legge tramite process.env. Con nodejs_compat, Cloudflare popola process.env per impostazione predefinita per le date di compatibilità a partire da 2025-04-01. I progetti fissati a una data precedente devono anche aggiungere nodejs_compat_populate_process_env prima di salvare impostazioni crittografate.
EmDash legge i suoi secret da process.env a runtime. Il codice Worker legge i binding da env, importato da cloudflare:workers. Non leggere mai i secret tramite import.meta.env: Vite sostituisce quei valori in fase di build e può scriverli nel bundle del server.
Il secret HMAC di anteprima e il sale IP dei commentatori vengono generati e memorizzati nel database a meno che non fornisci override a runtime. Secrets and key management elenca le variabili esatte, le posizioni di archiviazione e gli effetti della rotazione.
Distribuzioni di anteprima
Gli ambienti Wrangler con nome non ereditano i binding. Crea risorse di anteprima separate e scrivile nell’ambiente preview prima di compilare:
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’ambiente di anteprima deve ripetere ogni binding usato dal Worker di anteprima. I binding core D1, R2 e sandbox hanno questa forma dopo che Wrangler ha scritto gli identificatori delle risorse:
{
"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" }],
},
},
}
Usa l’UUID di anteprima scritto da Wrangler. Ripeti i binding opzionali KV, AI Search, email e altri quando l’anteprima usa quelle funzioni. Aggiungi secret solo di anteprima con pnpm wrangler secret put <NAME> --env preview.
Compila e distribuisci l’ambiente di anteprima. La sua prima richiesta applica le migrazioni core in sospeso tramite la modalità predefinita auto.
pnpm build
pnpm wrangler deploy --env preview
Verifica l’URL di anteprima, l’accesso all’amministrazione, il caricamento dei media e qualsiasi binding opzionale prima di condividerlo. Non puntare mai un binding di anteprima a un database o bucket di produzione.
Verificare la distribuzione
Dopo la distribuzione, richiedi una pagina pubblica, accedi a /_emdash/admin, carica e recupera un file media di prova e conferma che l’handler programmato appaia in pnpm wrangler tail.
Risoluzione dei problemi
«D1 binding not found»
Verifica che il nome del binding in wrangler.jsonc corrisponda alla configurazione del database:
// Must match: d1({ binding: "DB" })
"binding": "DB"
«R2 binding not found»
Controlla che il bucket R2 sia correttamente collegato:
// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"
Errori di migrazione
Se vedi errori di schema, segui i log del Worker (wrangler tail) e riproduci l’errore per catturare il messaggio sottostante — poi apri una issue con quell’output.