Distribuire su Cloudflare

In questa pagina

Questa guida distribuisce un sito EmDash su Cloudflare Workers con D1 come database e R2 per i media. Inizia con 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 ogni risorsa se il suo 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 adattatori EmDash. Il Cron Trigger esegue pubblicazioni pianificate, attività dei plugin, backup e manutenzione. Vedi Sandbox dei plugin 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(), // Richiesto — l'UI admin è un'app React
		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 connette Astro al Cron Trigger ed esporta il ponte 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 nessun plugin in sandbox è installato. 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 differiscono, l’handler registra e ignora il trigger inatteso.

Compilare e distribuire

Compila e distribuisci il sito una volta per far provisionare a Wrangler il database D1 e il 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 Gestire le migrazioni del database core quando una pipeline di distribuzione deve applicare le migrazioni prima che il nuovo codice riceva traffico o quando devi ispezionare, verificare o recuperare una migrazione.

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 della compilazione da .emdash/seed.json, dal percorso in package.json#emdash.seed, o da seed/seed.json — il primo trovato — e incorporato nel bundle. Se nessuno è presente, viene usato un seed predefinito incorporato. I deploy successivi contro un database esistente lasciano il suo contenuto intatto.

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

Posizionare il Worker vicino a D1

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

Wrangler accetta placement.mode: "targeted" con esattamente un selettore: region, host o hostname. Seleziona il valore che punta alla posizione del D1 primario 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", in modo che letture e scritture usino il primario vicino.

Cache degli oggetti

Per ridurre il carico di lettura su D1, metti in 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 posiziona una cache edge davanti al tuo Worker: le richieste corrispondenti vengono servite senza eseguire il tuo Worker affatto.

Abilitarlo

  1. Usa il provider di cache Cloudflare di Astro così che le regole di rotta 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 },
       // Altre rotte pubbliche possono usare diverse durate di cache.
     },
    });

    L’adattatore @astrojs/cloudflare rileva cacheCloudflare() e abilita Workers Cache nella configurazione di distribuzione generata.

  2. Purga le risposte in cache dal codice Worker con l’API della piattaforma. Questa chiamata non necessita di credenziali REST Cloudflare.

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // O purga tag selezionati:
    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 abilitarlo:

  1. Le risposte senza header Cache-Control vengono comunque messe in cache. Workers Cache applica la freschezza euristica RFC 9111 — un 200 senza alcun header viene memorizzato per 2 ore. Dai a ogni rotta personalizzata un Cache-Control esplicito (usa private, no-store per qualsiasi cosa dipendente dalla sessione).
  2. Le pagine in cache sono condivise con gli editor connessi. La cache si esegue prima del tuo Worker, quindi non può essere aggirata basandosi sui cookie della richiesta. Un editor connesso potrebbe ricevere la variante anonima in cache di una pagina pubblica — senza la barra degli strumenti di modifica visuale — fino alla scadenza dell’entry. Le risposte renderizzate dall’editor non vengono mai memorizzate (portano private, no-store), quindi nulla trapela nell’altra direzione.

Non è la stessa cosa 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
StoragePlatform Workers CachingCache API (caches.open / put / match)
Purgacache.purge() da cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretNessuno 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.

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

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 con successo al suo URL workers.dev, aggiungi il dominio di produzione come rotta Wrangler:

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

Distribuisci di nuovo e verifica entrambi gli indirizzi. Mantenere l’indirizzo workers.dev disponibile durante i test DNS aiuta a distinguere un problema di routing da un problema applicativo.

Accesso pubblico R2

Per impostazione predefinita, i media vengono serviti attraverso la rotta autenticata dei media di EmDash. Se il bucket ha un dominio pubblico personalizzato, imposta quell’origine come publicUrl in modo che gli URL dei 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 attraverso il dominio pubblico. Scegliere lo storage dei media spiega il confine sicuro.

Trasformazione delle immagini

EmDash ridimensiona e ri-codifica i media R2 all’interno del Worker, attraverso il binding IMAGES di Cloudflare. Il componente Image da emdash/ui e le immagini nel testo ricco passano entrambi attraverso l’endpoint delle immagini che EmDash installa sotto l’adattatore Cloudflare. Per i media sulla rotta 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 il proprio endpoint di trasformazione dell’adattatore, 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 ogni volta che il servizio immagini 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 tuo wrangler.jsonc rende l’intento ovvio:

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

Per vedere cosa ottiene 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 un’entry images lì.

Cloudflare fattura queste trasformazioni come Images transformations. Ogni combinazione unica di immagine sorgente e parametri viene fatturata una volta per mese solare, e le richieste ripetute entro quel mese sono gratuite. Se un sito ha 500 immagini sorgente e richiede una dimensione thumbnail e una dimensione 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 le nuove restituiscono un errore 9422 e la richiesta di 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 di 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 guida all’autenticazione 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 L’email non è configurata fino a quando un plugin email non è attivo.

Il plugin email di Cloudflare usa un binding send_email. Prima integra e verifica il dominio del mittente con Cloudflare Email Sending. Cloudflare rifiuta i messaggi il cui indirizzo mittente 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 la distribuzione, attiva il plugin sotto Estensioni e selezionalo sotto Impostazioni → Email. L’invio fallisce fino a quando il mittente non è accettato e il binding non esiste.

Il plugin usa il binding chiamato EMAIL a meno che la sua opzione binding ne nomini un altro. Se è l’unico provider email attivo, EmDash lo seleziona automaticamente. Se più di un provider è attivo, scegli il provider Cloudflare sotto Impostazioni → Email. L’indirizzo replyTo opzionale riceve le risposte senza cambiare l’indirizzo mittente accettato.

Il plugin AI Search necessita sia di una registrazione di plugin nativo che di un binding ai_search_namespaces. Dopo averli distribuiti, apri Cloudflare AI Search nell’admin, scegli le collezioni ed esegui Sincronizza tutto il contenuto. La sincronizzazione iniziale indicizza il contenuto pubblicato prima dell’abilitazione del plugin; gli hook mantengono i cambiamenti successivi sincronizzati.

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

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

Esponi la rotta 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="Cerca contenuti">
	<button slot="trigger" type="button">Cerca</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 compilazione.

EMDASH_ENCRYPTION_KEY attualmente non cifra i secret dei plugin o altri dati memorizzati. Se impostato, EmDash ne verifica il formato all’avvio. Un valore malformato produce un messaggio di log per gli operatori, ma il sito continua a gestire le richieste. I secret dei plugin rimangono in chiaro nel database.

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 al momento della compilazione e può scriverli nel bundle del server.

Il secret HMAC di preview e il salt IP dei commentatori vengono generati e memorizzati nel database a meno che non si forniscano override di runtime. Secret e gestione delle chiavi elenca le variabili esatte, le posizioni di storage e gli effetti della rotazione.

Deploy di preview

Gli ambienti Wrangler con nome non ereditano i binding. Crea risorse di preview 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 preview deve ripetere ogni binding che il Worker di preview usa. I binding core D1, R2 e sandbox hanno questa forma dopo che Wrangler scrive 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 preview scritto da Wrangler. Ripeti i binding opzionali KV, AI Search, email e altri quando il preview usa quelle funzionalità. Aggiungi secret specifici per il preview con pnpm wrangler secret put <NAME> --env preview.

Compila e distribuisci l’ambiente preview. 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 preview, l’accesso admin, il caricamento dei media e qualsiasi binding opzionale prima di condividerlo. Non puntare mai un binding di preview 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 test, e conferma che l’handler pianificato appare in pnpm wrangler tail.

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 crea un issue con quell’output.