Distribuire su Cloudflare

In questa pagina

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 una delle due risorse se il nome configurato non esiste ancora. Mantieni i nomi in wrangler.jsonc; Wrangler ricollega i deploy successivi alle stesse risorse.

Il template usa i binding seguenti:

{
	"$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 di EmDash. Il Cron Trigger esegue la pubblicazione pianificata, le attività dei plugin, i backup e la manutenzione. Consulta Plugin sandbox se il sito usa plugin in sandbox.

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 dei 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 in sandbox. Mantienilo se lo stesso progetto potrebbe abilitare 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 sono diverse, l’handler registra il trigger inatteso e lo ignora.

Compilare e distribuire

Compila e distribuisci il sito una volta, in modo che Wrangler effettui il provisioning del database D1 e del bucket R2 con nome. Wrangler usa il login locale creato da pnpm wrangler login.

pnpm build
pnpm wrangler deploy

Con la modalità di migrazione auto predefinita, EmDash applica le migrazioni core in sospeso quando il Worker distribuito riceve la sua prima richiesta. Usa Manage core database migrations quando una pipeline di deploy deve applicare le migrazioni prima che il nuovo codice riceva traffico, oppure quando devi ispezionare, controllare o ripristinare una migrazione.

Finché la procedura guidata di configurazione non è stata completata, EmDash applica anche una sola volta, alla prima richiesta, lo schema e la struttura di un file seed. I contenuti di esempio del seed vengono applicati solo quando un amministratore li sceglie nella procedura guidata di configurazione. Il seed viene letto in fase di build da .emdash/seed.json, dal percorso in package.json#emdash.seed o da seed/seed.json — quello trovato per primo — e incorporato nel bundle. Se nessuno è presente, viene usato un seed predefinito integrato. I deploy successivi su un database esistente non toccano il suo contenuto.

Per modificare lo schema o il modello di contenuto di un sito già distribuito, consulta Evolving a Deployed Site.

Pubblicazione pianificata

Su Cloudflare, EmDash pubblica le voci pianificate solo dall’handler scheduled del Worker, che Cloudflare chiama secondo la pianificazione di un Cron Trigger. Il Worker distribuito ha bisogno di entrambi i seguenti elementi:

Se manca uno dei due, le voci pianificate non vanno online. Nello sviluppo locale con astro dev, EmDash pubblica le voci pianificate tramite un timer, quindi una pianificazione che funziona in locale non conferma la configurazione distribuita.

Esegui emdash doctor dalla radice del progetto per controllare il Cron Trigger e l’handler scheduled nei file del progetto locale:

pnpm emdash doctor

Quando emdash doctor trova entrambi, l’output include un controllo scheduler wiring superato. Un controllo dello scheduler non riuscito descrive il problema e stampa la correzione. Il controllo riconosce il punto di ingresso Worker mostrato in questa pagina e il re-export su una riga che stampa come correzione, quindi un punto di ingresso scritto in un altro modo può non superare il controllo anche se funziona.

Se non compare alcun controllo dello scheduler, emdash doctor non ha trovato alcuna configurazione Wrangler nella directory corrente, oppure non ha trovato né il Cron Trigger né l’handler scheduled. In un progetto D1, il controllo database non riesce perché cerca un file SQLite locale (./data.db per impostazione predefinita). Questo errore non influisce sui controlli dello scheduler.

Distribuisci di nuovo dopo aver aggiunto o modificato il Cron Trigger o l’handler scheduled. Cloudflare può impiegare fino a 15 minuti per iniziare a eseguire un Cron Trigger nuovo o modificato. Una volta che il trigger inizia a funzionare, EmDash pubblica le voci il cui orario pianificato è già trascorso.

Per osservare il trigger, esegui pnpm wrangler tail. Ogni esecuzione stampa una riga come "* * * * *" @ 9/29/2026, 10:01:00 AM - Ok. Ok significa che l’esecuzione è terminata senza un errore non gestito. EmDash intercetta e registra gli errori di pubblicazione, quindi Ok non conferma che le voci siano state pubblicate. Un’esecuzione che pubblica voci registra anche un messaggio come [scheduled] Published 1 scheduled item(s), e gli errori dell’esecuzione compaiono nello stesso output.

Dopo il deploy, la dashboard di amministrazione mostra Scheduled publishing needs attention quando una voce pianificata ha superato il suo orario pianificato e nessuna esecuzione dello scheduler si è completata negli ultimi cinque minuti. Se il Cron Trigger viene eseguito meno spesso di ogni cinque minuti, l’avviso può comparire anche tra un’esecuzione e l’altra, finché la successiva non pubblica la voce.

Collocare il Worker vicino a D1

Cloudflare esegue per impostazione predefinita un Worker vicino al visitatore. Le richieste renderizzate lato server di EmDash fanno diversi round trip verso D1, quindi usa Targeted Placement per eseguire il Worker vicino al primario D1 e rendere più veloci queste richieste.

Wrangler accetta placement.mode: "targeted" con esattamente un selettore: region, host o hostname. Seleziona 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 insieme a Targeted Placement. Lascia l’impostazione session di EmDash al valore predefinito, "disabled", in modo che letture e scritture usino il primario vicino.

Cache degli oggetti

Per ridurre il carico di lettura su D1, memorizza nella cache di Cloudflare KV i risultati delle query di contenuti e configurazione. 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" }),
}),

Consulta Object Cache per la configurazione di KV, le opzioni e il comportamento di invalidazione.

Workers Cache

Workers Cache di Cloudflare mette una cache edge davanti al tuo Worker: le richieste corrispondenti vengono servite senza eseguire affatto il tuo Worker.

Abilitarlo

  1. Usa il provider di cache Cloudflare di Astro, in modo che le regole di route e Astro.cache impostino gli header di cache e l’invalidazione usi cache.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/cloudflare rileva cacheCloudflare() e abilita Workers Cache nella configurazione di deploy generata.

  2. Svuota le risposte in cache dal codice del Worker con l’API della piattaforma. Questa chiamata non richiede credenziali dell’API REST di 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 abilitarla:

  1. Le risposte senza header Cache-Control vengono comunque memorizzate nella cache. Workers Cache applica la freschezza euristica della RFC 9111: una 200 senza alcun header viene memorizzata per 2 ore. Assegna a ogni route personalizzata un Cache-Control esplicito (usa private, no-store per tutto ciò che dipende dalla sessione).
  2. Le pagine in cache sono condivise con gli editor autenticati. La cache viene eseguita prima del tuo Worker, quindi non può essere aggirata in base ai cookie della richiesta. Un editor autenticato può ricevere la variante anonima in cache di una pagina pubblica — senza la barra degli strumenti di modifica visuale — finché la voce non scade. Le risposte renderizzate per l’editor non vengono mai memorizzate (riportano private, no-store), quindi non trapela nulla nella direzione opposta.

Workers Caching è separato dalla cache degli oggetti (objectCache: kvCache({ binding: "CACHE" })), che memorizza in KV, sotto il Worker, i risultati delle query del database.

Domini personalizzati

Il primo deploy riceve un URL workers.dev. Il dominio personalizzato deve essere già 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 il test del DNS aiuta a distinguere un problema di routing da un problema dell’applicazione.

Senza siteUrl, EMDASH_SITE_URL o SITE_URL, il setup registra come origine del sito l’indirizzo su cui viene eseguito, e le passkey create durante il setup funzionano solo su quell’indirizzo. Esegui il setup sul dominio di produzione, oppure imposta EMDASH_SITE_URL su quel dominio prima del setup. Se imposti EMDASH_SITE_URL su un altro dominio dopo il setup, l’accesso passa a quel dominio e le passkey create sul vecchio indirizzo smettono di funzionare lì.

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 in modo che gli URL dei media generati la 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 all’interno del Worker, tramite il binding IMAGES di Cloudflare. Il componente Image di emdash/ui e le immagini nel rich text vengono entrambi renderizzati tramite l’endpoint immagini che EmDash installa con 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. Queste trasformazioni continuano a funzionare dietro Cloudflare Access e con global_fetch_strictly_public. I media serviti da un URL del bucket — vedi Accesso R2 pubblico — usano 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, nello stesso modo in cui aggiunge cache per Workers Caching. Lo fa ogni volta che il servizio immagini di runtime è cloudflare-binding: imageService non impostato, la stringa stessa oppure { runtime: "cloudflare-binding" }. Qualsiasi altro valore — "passthrough", "compile", "cloudflare", "custom" — lascia fuori il binding. Elencarlo nel tuo wrangler.jsonc rende evidente l’intenzione:

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

Per vedere cosa ottiene effettivamente un deploy, leggi la configurazione generata anziché wrangler.jsonc. Una build scrive .wrangler/deploy/config.json, che indirizza 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 entro quel mese sono gratuite. Se un sito ha 500 immagini sorgente e richiede una dimensione di miniatura e una dimensione hero per ogni immagine, quei due insiemi 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 comunque 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 indicando il nome della 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.

Email

I Worker di produzione non hanno un servizio di consegna email predefinito. L’accesso con link magico, gli inviti al team e le notifiche dei commenti restituiscono Email is not configured finché non è attivo un plugin email.

Il plugin email di Cloudflare usa un binding send_email. Prima registra e verifica il dominio mittente con Cloudflare Email Sending. Cloudflare rifiuta i messaggi il cui indirizzo 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: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]",
		}),
	],
}),

Dopo il deploy, attiva il plugin in Extensions e selezionalo in Settings → Email. L’invio fallisce finché il mittente non viene accettato e il binding non esiste.

Il plugin usa il binding chiamato EMAIL, a meno che la sua opzione binding non indichi un altro nome. 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 replyTo opzionale riceve le risposte senza modificare l’indirizzo From accettato. Un plugin può impostare replyTo su un singolo messaggio, il che sostituisce questa opzione per quel messaggio.

Il plugin AI Search richiede sia una registrazione come plugin nativo sia 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 i contenuti pubblicati 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 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 inserirli in wrangler.jsonc e non leggerli dai valori import.meta.env in fase di build.

EMDASH_ENCRYPTION_KEY cifra le impostazioni dei plugin dichiarate come secret. Impostala prima di salvare il secret di un plugin e conservala separatamente dai backup D1. Durante la rotazione, fornisci prima la nuova chiave e mantieni le chiavi precedenti dopo le virgole finché ogni secret dei plugin non è stato salvato di nuovo. Secrets and key management descrive rotazione e ripristino.

Il bridge dei plugin legge direttamente questo binding di secret del Worker. La route delle impostazioni di amministrazione generata lo legge tramite process.env. Con nodejs_compat, Cloudflare popola process.env per impostazione predefinita per le date di compatibilità dal 2025-04-01 in poi. I progetti fissati a una data precedente devono anche aggiungere nodejs_compat_populate_process_env prima di salvare impostazioni cifrate.

EmDash legge i propri secret da process.env in fase di esecuzione. Il codice del 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 salt degli IP dei commentatori vengono generati e memorizzati nel database, a meno che tu non fornisca override di 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 della 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

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 funzionalità. Aggiungi i secret solo per l’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à auto predefinita.

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 condividerla. Non puntare mai un binding di anteprima a un database o a un bucket di produzione.

Verificare la distribuzione

Dopo il deploy, richiedi una pagina pubblica, accedi a /_emdash/admin, carica e recupera un file media di test e conferma che l’handler scheduled compaia in pnpm wrangler tail.

Risoluzione dei problemi

«D1 binding not found»

Verifica che il nome del binding in wrangler.jsonc corrisponda alla configurazione del tuo database:

// Must match: d1({ binding: "DB" })
"binding": "DB"

«R2 binding not found»

Controlla che il bucket R2 sia collegato correttamente:

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