Deploy su Cloudflare

In questa pagina

Cloudflare Workers fornisce un runtime veloce e distribuito globalmente per EmDash. Questa guida copre il deployment con D1 per il database e R2 per lo storage dei media.

Prerequisiti

  • Un account Cloudflare
  • Wrangler CLI installato (npm install -g wrangler)
  • Autenticato con Cloudflare (wrangler login)

Configurare i binding

Provisiona il database D1 di produzione e il bucket R2, poi crea wrangler.jsonc nella root del progetto con i binding per i loro ID e nomi immutabili. Il provisioning del database è separato dall’applicazione delle migrazioni di schema di EmDash.

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

Questi sono i binding che configuri tu. L’adattatore @astrojs/cloudflare ne aggiunge altri quando genera la configurazione del Worker distribuito. Uno di questi è il binding IMAGES che le trasformazioni media usano — vedi Trasformazione immagini.

I plugin sandboxati — installazioni dal marketplace e i plugin sotto sandboxed: [] — necessitano un binding worker_loaders e un punto di ingresso Worker che esporta PluginBridge. Vedi Plugin 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 } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // Richiesto — l'UI admin è un'app React
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

Migrare e distribuire

Le migrazioni a runtime rimangono automatiche per impostazione predefinita. Per migrazioni gestite dal deployment, compila il Worker e ispeziona il target D1 provisionato usando il suo UUID di account e database.

pnpm build
pnpm exec emdash migrate --status --json \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID"

Dopo aver esaminato e registrato l’impronta del target segnalata, applica le migrazioni e distribuisci lo stesso build.

pnpm exec emdash migrate \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID" \
  --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy

Il job di migrazione richiede CLOUDFLARE_API_TOKEN con permesso D1 Edit. Serializza i job per UUID di account e database. Vedi Gestire le migrazioni del database core per provisioning, concorrenza CI, modalità runtime e guida al ripristino.

Se il database è vuoto (nessuna collezione) e la procedura guidata di configurazione non è stata completata, EmDash applica anche un file seed al primo avvio. Il seed viene letto al momento del build da .emdash/seed.json, il percorso in package.json#emdash.seed, o seed/seed.json — qualsiasi venga trovato per primo — e incorporato nel bundle. Se nessuno è presente, viene usato un seed predefinito integrato. I deployment successivi contro un database esistente lasciano il suo contenuto invariato.

Per cambiare lo schema o il modello di contenuto di un sito già distribuito, vedi Evolvere un sito distribuito.

Attività pianificate

Cloudflare esegue pubblicazioni pianificate, attività dei plugin e manutenzione generale da un Cron Trigger.

Usa il punto di ingresso Worker standard:

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

export { PluginBridge };

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

Configura un Cron Trigger per la manutenzione generale in wrangler.jsonc:

{
	"triggers": {
		"crons": ["* * * * *"],
	},
}

Per usare un programma di manutenzione generale diverso, imposta generalCron in createScheduledHandler() e usa la stessa espressione in wrangler.jsonc.

Distribuire

Distribuisci su Cloudflare Workers:

wrangler deploy

Il tuo sito è ora online su https://my-emdash-site.<your-subdomain>.workers.dev.

Repliche di lettura

Per siti distribuiti globalmente, abilita la replica di lettura D1 per instradare le query di lettura verso repliche vicine invece di interrogare sempre il database primario. Questo riduce significativamente la latenza per i visitatori lontani dalla regione primaria.

emdash({
	database: d1({
		binding: "DB",
		session: "auto",
	}),
	storage: r2({ binding: "MEDIA" }),
}),

Devi anche abilitare la replica di lettura sul database D1 stesso nella dashboard di Cloudflare o tramite l’API REST.

Vedi Opzioni database — Repliche di lettura per le modalità di sessione e come funziona la consistenza basata su bookmark.

Cache degli oggetti

Per ridurre il carico di lettura su D1, memorizza nella cache i risultati delle query di contenuto e configurazione in Cloudflare KV. Le letture vengono servite da KV invece di interrogare il database ad ogni richiesta:

import { d1, r2, kvCache } from "@emdash-cms/cloudflare";

emdash({
	database: d1({ binding: "DB" }),
	storage: r2({ binding: "MEDIA" }),
	objectCache: kvCache({ binding: "CACHE" }),
}),

Vedi Cache degli oggetti 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 il Worker.

Abilitarla

  1. Attiva la cache di piattaforma in wrangler.jsonc:
{
	"cache": {
		"enabled": true,
	},
}
  1. Usa il provider cache Cloudflare di Astro così le regole di route / Astro.cache impostano gli header giusti e l’invalidazione usa il cache.purge() nativo:
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
	adapter: cloudflare(),
	cache: {
		provider: cacheCloudflare(),
	},
	routeRules: {
		"/": { maxAge: 300, swr: 86400 },
		// …
	},
});

Con cacheCloudflare(), l’adattatore @astrojs/cloudflare inietta anche "cache": { "enabled": true } nella configurazione Wrangler generata quando manca — elencarlo esplicitamente nel tuo wrangler.jsonc rende l’intento ovvio.

  1. Purga dal Worker con l’API di piattaforma (nessuna credenziale REST Cloudflare):
import { cache } from "cloudflare:workers";

await cache.purge({ purgeEverything: true });
// o: await cache.purge({ tags: ["posts"] });

Le risposte admin 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. Workers Cache applica la freschezza euristica RFC 9111 — una 200 senza header viene memorizzata per 2 ore. Dai ad ogni route personalizzata un Cache-Control esplicito (usa private, no-store per qualsiasi cosa dipendente dalla sessione).
  2. Le pagine memorizzate sono condivise con gli editor connessi. La cache viene eseguita prima del Worker, quindi non può essere aggirata in base ai cookie di richiesta. Un editor connesso potrebbe ricevere la variante anonima memorizzata di una pagina pubblica — senza la toolbar di editing visuale — fino alla scadenza della voce. Le risposte renderizzate dall’editor non vengono mai memorizzate (portano private, no-store), quindi nulla trapela nell’altra direzione.

Non è lo stesso di cloudflareCache() da @emdash-cms/cloudflare

Preferito: Workers CachingLegacy: cloudflareCache()
Configurazione"cache": { "enabled": true } + cacheCloudflare() da @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } da @emdash-cms/cloudflare
StoragePiattaforma Workers CachingCache API (caches.open / put / match)
Purgacache.purge() da cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretsNessuno per la purgaCF_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.

Inoltre non confondere nessuno dei due con la cache degli oggetti (objectCache: kvCache({ binding: "CACHE" })), che memorizza i risultati delle query del database in KV — un livello separato sotto il Worker.

Dominio personalizzato

Aggiungi un dominio personalizzato nella dashboard di Cloudflare:

  1. Vai a Workers & Pages > il tuo worker
  2. Clicca Custom Domains > Add Custom Domain
  3. Inserisci il tuo dominio e segui le istruzioni di configurazione DNS

Accesso pubblico R2

Per servire media direttamente da R2 (consigliato per le prestazioni):

  1. Nella dashboard di Cloudflare, vai a R2 > il tuo bucket
  2. Clicca Settings > Public access
  3. Abilita l’accesso pubblico e annota l’URL pubblico
  4. Aggiorna la configurazione dello storage:
storage: r2({
  binding: "MEDIA",
  publicUrl: "https://pub-xxx.r2.dev"
}),

Trasformazione immagini

EmDash ridimensiona e ricodifica i media R2 all’interno del Worker, attraverso il binding IMAGES di Cloudflare. Il componente Image da emdash/ui e le immagini nel rich text renderizzano entrambi attraverso l’endpoint immagine che EmDash installa sotto l’adattatore 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 del bucket — vedi Accesso pubblico R2 — usano invece l’endpoint di trasformazione dell’adattatore, che fa fetch del 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 a runtime è cloudflare-binding: imageService non impostato, la stringa stessa, o { runtime: "cloudflare-binding" }. Ogni altro valore — "passthrough", "compile", "cloudflare", "custom" — omette il binding. Elencarlo nel tuo wrangler.jsonc rende l’intento ovvio:

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

Per vedere cosa riceve effettivamente un deploy, leggi la configurazione generata piuttosto che wrangler.jsonc. Un build scrive .wrangler/deploy/config.json, che punta wrangler deploy al file unificato (dist/server/wrangler.json per impostazione predefinita). Cerca una voce images lì.

Cloudflare fattura queste trasformazioni come trasformazioni Images. Ogni combinazione unica di immagine sorgente e parametri viene fatturata una volta per mese solare, e le richieste ripetute entro quel mese sono gratuite. Il piano Images gratuito 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 immagine fallisce.

Autenticazione Cloudflare Access

Se la tua organizzazione usa Cloudflare Access, puoi usarlo come provider di autenticazione invece dei passkey, dando single sign-on attraverso il tuo provider di identità esistente. La seguente configurazione lo abilita:

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,
    },
  }),
}),

Vedi la guida all’autenticazione per le opzioni di configurazione complete.

Il plugin AI Search indicizza il contenuto EmDash pubblicato e aggiunge un’interfaccia di ricerca intelligente al tuo sito.

  1. Registra il plugin nell’array plugins passato a EmDash:

    import { aiSearch } from "@emdash-cms/cloudflare/plugins";
    
    // ...
    plugins: [
    	formsPlugin(),
    	aiSearch(),
    ],
  2. Aggiungi il binding del namespace AI Search alla configurazione del Worker:

    {
    	"ai_search_namespaces": [
    		{
    			"binding": "AI_SEARCH",
    			"namespace": "default",
    		},
    	],
    }
  3. Crea l’endpoint di ricerca usato dall’interfaccia di ricerca:

    export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
  4. Aggiungi l’interfaccia di ricerca al layout del tuo sito. Lo slot trigger può contenere qualsiasi pulsante che si adatta al design del tuo sito:

    ---
    import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
    ---
    
    <AISearchSnippet apiUrl="/api/ai-search" placeholder="Cerca...">
    	<button slot="trigger" type="button">Cerca</button>
    </AISearchSnippet>
  5. Distribuisci il sito:

    pnpm exec wrangler deploy
  6. Apri Cloudflare AI Search nel pannello admin di EmDash, seleziona le collezioni da indicizzare e clicca Sync All Content.

    Questa sincronizzazione iniziale è richiesta: gli hook di contenuto del plugin si attivano solo per contenuto creato o aggiornato dopo la sua abilitazione, quindi qualsiasi cosa pubblicata prima resta assente dall’indice fino a quando non esegui una sincronizzazione completa.

Il contenuto pubblicato o aggiornato dopo la configurazione viene mantenuto sincronizzato automaticamente. La stessa pagina mostra il progresso dell’indicizzazione.

Email

Su Workers, l’unico handler email:deliver integrato è uno stub della console di sviluppo, quindi i flussi dipendenti dall’email — login magic-link, inviti del team e notifiche dei commenti — falliscono con “Email is not configured” in produzione. Il plugin cloudflareEmail() consegna email reali attraverso Cloudflare Email Sending usando un binding Worker nativo send_email, senza chiavi API esterne.

1. Integrare un dominio mittente

Nella dashboard di Cloudflare, vai a Email e verifica il dominio (o indirizzo) da cui invii. Email Sending rifiuta i messaggi da mittenti non verificati.

2. Aggiungere il binding

Dichiara un binding send_email in wrangler.jsonc:

{
	"send_email": [{ "name": "EMAIL" }],
}

3. Registrare il provider

Aggiungi il plugin alla tua integrazione emdash():

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]", // opzionale
			binding: "EMAIL", // opzionale, predefinito "EMAIL"
		}),
	],
}),

4. Attivare e selezionare

Distribuisci, poi attiva il plugin sotto Admin → Extensions e sceglilo come provider sotto Settings → Email.

Opzioni

OpzioneTipoPredefinitoDescrizione
fromstring | { email, name? }— (richiesto)Indirizzo mittente su un dominio integrato per Email Sending.
replyTostringReply-To opzionale, utile quando from è un indirizzo di sottodominio no-reply.
bindingstring"EMAIL"Nome del binding send_email in wrangler.jsonc.

Variabili d’ambiente

Consigliato: chiave di crittografia

EMDASH_ENCRYPTION_KEY è la chiave per crittografare i segreti dei plugin a riposo (token webhook, chiavi Turnstile, ecc.). La chiave viene validata all’avvio; la crittografia dei segreti dei plugin la usa una volta abilitata. Impostala ad ogni deployment così i segreti sono protetti senza un successivo cambio di configurazione.

La chiave è fornita da te e mai memorizzata nel database; solo il testo cifrato viene memorizzato. Perderla significa perdere ogni segreto cifrato con essa.

Genera una chiave e memorizzala come secret del Worker con i seguenti comandi:

npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY

Opzionale: override di valori stabili

EmDash auto-genera il segreto HMAC di anteprima e il sale hash dell’IP del commentatore e li persiste nel database al primo utilizzo. Le variabili d’ambiente sotto sono override per i casi in cui devi fissare il valore tu stesso — per esempio, quando un Worker di anteprima in un processo separato deve condividere il segreto con il tuo sito principale.

VariabileScopo
EMDASH_PREVIEW_SECRETOverride per il segreto HMAC di anteprima auto-generato.
EMDASH_IP_SALTOverride per il sale hash dell’IP del commentatore auto-generato.
EMDASH_AUTH_SECRETOpzionale. Se impostato, viene usato come sorgente del sale IP (a meno che non sia impostato anche EMDASH_IP_SALT, che ha la precedenza), mantenendo gli hash IP dei commentatori stabili per le installazioni che già dipendono da esso. Lascialo non impostato per un nuovo deployment.

Accedi alle variabili d’ambiente nella tua configurazione usando import.meta.env o il binding env di Cloudflare.

Per l’inventario completo di ogni segreto che EmDash usa — incluse le posizioni di storage, i passaggi di rotazione e cosa si rompe quando una chiave viene persa — vedi Segreti e gestione delle chiavi.

Deployment di anteprima

Distribuisci un branch di anteprima:

wrangler deploy --env preview

Aggiungi una sezione ambiente a wrangler.jsonc:

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "emdash-db-preview",
				},
			],
		},
	},
}

Risoluzione dei problemi

”D1 binding not found”

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

// Deve corrispondere: d1({ binding: "DB" })
"binding": "DB"

“R2 binding not found”

Controlla che il bucket R2 sia correttamente collegato:

// Deve corrispondere: 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 un issue con quell’output.