Deploy no Cloudflare

Nesta página

O Cloudflare Workers fornece um runtime rápido e distribuído globalmente para o EmDash. Este guia cobre o deploy com D1 para o banco de dados e R2 para armazenamento de mídia.

Pré-requisitos

  • Uma conta Cloudflare
  • Wrangler CLI instalado (npm install -g wrangler)
  • Autenticado com Cloudflare (wrangler login)

Configurar bindings

Provisione o banco de dados D1 de produção e o bucket R2, depois crie wrangler.jsonc na raiz do projeto com bindings para seus IDs e nomes imutáveis. O provisionamento do banco de dados é separado da aplicação das migrações de schema do 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",
		},
	],
}

Estes são os bindings que você configura. O adaptador @astrojs/cloudflare adiciona mais quando gera a configuração do Worker implantado. Um deles é o binding IMAGES que as transformações de mídia usam — veja Transformação de Imagens.

Plugins sandboxados — instalações do marketplace e os plugins em sandboxed: [] — precisam de um binding worker_loaders e um ponto de entrada Worker que exporta PluginBridge. Veja Plugin Sandbox.

Configurar EmDash

A seguinte configuração Astro usa os bindings 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(), // Obrigatório — a UI admin é um app React
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

Migrar e implantar

As migrações em runtime permanecem automáticas por padrão. Para migrações gerenciadas por deploy, compile o Worker e inspecione o alvo D1 provisionado usando seu UUID de conta e banco de dados.

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

Após revisar e registrar a impressão digital do alvo reportada, aplique as migrações e faça deploy do mesmo build.

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

O job de migração requer CLOUDFLARE_API_TOKEN com permissão D1 Edit. Serialize jobs por UUID de conta e banco de dados. Veja Gerenciar Migrações do Banco de Dados Core para provisionamento, concorrência CI, modos de runtime e orientação de recuperação.

Se o banco de dados estiver vazio (sem coleções) e o assistente de configuração não tiver sido concluído, o EmDash também aplica um arquivo seed na primeira inicialização. O seed é lido no momento do build de .emdash/seed.json, do caminho em package.json#emdash.seed, ou seed/seed.json — o que for encontrado primeiro — e incorporado no bundle. Se nenhum estiver presente, um seed padrão embutido é usado. Deploys subsequentes contra um banco de dados existente deixam seu conteúdo intacto.

Para alterar o schema ou modelo de conteúdo de um site já implantado, veja Evoluindo um Site Implantado.

Tarefas agendadas

O Cloudflare executa publicação agendada, tarefas de plugins e manutenção geral a partir de um Cron Trigger.

Use o ponto de entrada Worker padrão:

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

export { PluginBridge };

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

Configure um Cron Trigger para manutenção geral em wrangler.jsonc:

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

Para usar um cronograma de manutenção geral diferente, defina generalCron em createScheduledHandler() e use a mesma expressão em wrangler.jsonc.

Implantar

Faça deploy no Cloudflare Workers:

wrangler deploy

Seu site está online em https://my-emdash-site.<your-subdomain>.workers.dev.

Réplicas de leitura

Para sites distribuídos globalmente, habilite a replicação de leitura D1 para rotear consultas de leitura para réplicas próximas em vez de sempre acessar o banco de dados primário. Isso reduz significativamente a latência para visitantes distantes da região primária.

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

Você também precisa habilitar a replicação de leitura no próprio banco de dados D1 no painel do Cloudflare ou via REST API.

Veja Opções de Banco de Dados — Réplicas de Leitura para modos de sessão e como funciona a consistência baseada em bookmark.

Cache de objetos

Para reduzir a carga de leitura no D1, armazene em cache os resultados de consultas de conteúdo e configuração no Cloudflare KV. As leituras são servidas do KV em vez de consultar o banco de dados a cada requisição:

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

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

Veja Cache de Objetos para configuração do KV, opções e comportamento de invalidação.

Workers Cache

O Workers Cache do Cloudflare coloca um cache de borda na frente do seu Worker: requisições correspondentes são servidas sem executar o Worker.

Habilitar

  1. Ative o cache de plataforma em wrangler.jsonc:
{
	"cache": {
		"enabled": true,
	},
}
  1. Use o provedor de cache Cloudflare do Astro para que as regras de rota / Astro.cache definam os headers corretos e a invalidação use o cache.purge() nativo:
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

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

Com cacheCloudflare(), o adaptador @astrojs/cloudflare também injeta "cache": { "enabled": true } na configuração Wrangler gerada quando ausente — listá-lo explicitamente no seu próprio wrangler.jsonc torna a intenção óbvia.

  1. Purgue do Worker com a API de plataforma (sem credenciais REST do Cloudflare):
import { cache } from "cloudflare:workers";

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

As respostas de admin e API do EmDash já enviam Cache-Control: private, no-store e nunca são armazenadas. Páginas públicas controlam seu próprio cache através de Cache-Control / routeRules / Astro.cache.

Duas coisas para saber antes de habilitar:

  1. Respostas sem header Cache-Control ainda são armazenadas em cache. O Workers Cache aplica frescor heurístico RFC 9111 — uma 200 sem header é armazenada por 2 horas. Dê a cada rota personalizada um Cache-Control explícito (use private, no-store para qualquer coisa dependente de sessão).
  2. Páginas em cache são compartilhadas com editores logados. O cache é executado antes do Worker, então não pode ser contornado com base em cookies de requisição. Um editor logado pode receber a variante anônima em cache de uma página pública — sem a barra de ferramentas de edição visual — até a entrada expirar. As respostas renderizadas pelo editor nunca são armazenadas (contêm private, no-store), então nada vaza na direção oposta.

Não é o mesmo que cloudflareCache() de @emdash-cms/cloudflare

Preferido: Workers CachingLegado: cloudflareCache()
Config"cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } de @emdash-cms/cloudflare
ArmazenamentoPlataforma Workers CachingCache API (caches.open / put / match)
Purgecache.purge() de cloudflare:workersZone REST POST /zones/{id}/purge_cache
SecretsNenhum para purgeCF_ZONE_ID + CF_CACHE_PURGE_TOKEN

Use o caminho preferido para novos sites. Mantenha cloudflareCache() apenas se já depende do seu comportamento da Cache API.

Também não confunda nenhum desses com cache de objetos (objectCache: kvCache({ binding: "CACHE" })), que armazena resultados de consultas do banco de dados no KV — uma camada separada sob o Worker.

Domínio personalizado

Adicione um domínio personalizado no painel do Cloudflare:

  1. Vá para Workers & Pages > seu worker
  2. Clique em Custom Domains > Add Custom Domain
  3. Insira seu domínio e siga as instruções de configuração DNS

Acesso público R2

Para servir mídia diretamente do R2 (recomendado para performance):

  1. No painel do Cloudflare, vá para R2 > seu bucket
  2. Clique em Settings > Public access
  3. Habilite o acesso público e anote a URL pública
  4. Atualize sua configuração de armazenamento:
storage: r2({
  binding: "MEDIA",
  publicUrl: "https://pub-xxx.r2.dev"
}),

Transformação de imagens

O EmDash redimensiona e recodifica mídia R2 dentro do Worker, através do binding IMAGES do Cloudflare. O componente Image de emdash/ui e imagens em rich text renderizam através do endpoint de imagem que o EmDash instala sob o adaptador Cloudflare. Para mídia na rota interna /_emdash/api/media/file/…, esse endpoint lê os bytes fonte diretamente do binding R2, sem um fetch HTTP. Essas transformações continuam funcionando atrás do Cloudflare Access e com global_fetch_strictly_public. Mídia servida de uma URL do bucket — veja Acesso Público R2 — usa o endpoint de transformação próprio do adaptador, que busca o arquivo via HTTP antes de transformá-lo.

Você não precisa declarar o binding. O @astrojs/cloudflare o adiciona à configuração Worker que gera durante astro build, da mesma forma que adiciona cache para Workers Caching. Faz isso quando o serviço de imagem em runtime é cloudflare-binding: imageService não definido, a própria string, ou { runtime: "cloudflare-binding" }. Qualquer outro valor — "passthrough", "compile", "cloudflare", "custom" — omite o binding. Listá-lo no seu próprio wrangler.jsonc torna a intenção óbvia:

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

Para ver o que um deploy realmente recebe, leia a configuração gerada em vez de wrangler.jsonc. Um build escreve .wrangler/deploy/config.json, que aponta wrangler deploy para o arquivo mesclado (dist/server/wrangler.json por padrão). Procure uma entrada images lá.

O Cloudflare cobra essas transformações como transformações Images. Cada combinação única de imagem fonte e parâmetros é cobrada uma vez por mês calendário, e requisições repetidas dentro desse mês são gratuitas. O plano Images gratuito cobre 5.000 transformações únicas por mês. Além desse limite, transformações em cache ainda são servidas, mas novas retornam um erro 9422 e a requisição de imagem falha.

Autenticação Cloudflare Access

Se sua organização usa Cloudflare Access, você pode usá-lo como provedor de autenticação em vez de passkeys, proporcionando single sign-on através do seu provedor de identidade existente. A seguinte configuração o habilita:

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

Veja o guia de autenticação para opções completas de configuração.

O plugin AI Search indexa conteúdo EmDash publicado e adiciona uma interface de busca inteligente ao seu site.

  1. Registre o plugin no array plugins passado ao EmDash:

    import { aiSearch } from "@emdash-cms/cloudflare/plugins";
    
    // ...
    plugins: [
    	formsPlugin(),
    	aiSearch(),
    ],
  2. Adicione o binding do namespace AI Search à configuração do Worker:

    {
    	"ai_search_namespaces": [
    		{
    			"binding": "AI_SEARCH",
    			"namespace": "default",
    		},
    	],
    }
  3. Crie o endpoint de busca usado pela interface de busca:

    export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
  4. Adicione a interface de busca ao layout do seu site. O slot de trigger pode conter qualquer botão que combine com o design do seu site:

    ---
    import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
    ---
    
    <AISearchSnippet apiUrl="/api/ai-search" placeholder="Buscar...">
    	<button slot="trigger" type="button">Buscar</button>
    </AISearchSnippet>
  5. Faça deploy do site:

    pnpm exec wrangler deploy
  6. Abra Cloudflare AI Search no painel admin do EmDash, selecione as coleções para indexar e clique em Sync All Content.

    Esta sincronização inicial é necessária: os hooks de conteúdo do plugin só disparam para conteúdo criado ou atualizado após sua habilitação, então qualquer coisa publicada antes permanece ausente do índice até você executar uma sincronização completa.

Conteúdo publicado ou atualizado após a configuração é mantido em sincronia automaticamente. A mesma página mostra o progresso da indexação.

Email

No Workers, o único handler email:deliver embutido é um stub do console de desenvolvimento, então fluxos dependentes de email — login magic-link, convites de equipe e notificações de comentários — falham com “Email is not configured” em produção. O plugin cloudflareEmail() entrega email real através do Cloudflare Email Sending usando um binding Worker nativo send_email, sem chaves de API externas.

1. Integrar um domínio remetente

No painel do Cloudflare, vá para Email e verifique o domínio (ou endereço) de onde você envia. O Email Sending rejeita mensagens de remetentes não verificados.

2. Adicionar o binding

Declare um binding send_email em wrangler.jsonc:

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

3. Registrar o provedor

Adicione o plugin à sua integração 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]", // opcional
			binding: "EMAIL", // opcional, padrão "EMAIL"
		}),
	],
}),

4. Ativar e selecionar

Faça deploy, depois ative o plugin em Admin → Extensions e escolha-o como provedor em Settings → Email.

Opções

OpçãoTipoPadrãoDescrição
fromstring | { email, name? }— (obrigatório)Endereço remetente em um domínio integrado para Email Sending.
replyTostringReply-To opcional, útil quando from é um endereço de subdomínio no-reply.
bindingstring"EMAIL"Nome do binding send_email em wrangler.jsonc.

Variáveis de ambiente

Recomendado: chave de criptografia

EMDASH_ENCRYPTION_KEY é a chave para criptografar secrets de plugins em repouso (tokens de webhook, chaves Turnstile, etc.). A chave é validada na inicialização; a criptografia de secrets de plugins a usa uma vez habilitada. Defina em cada deploy para que os secrets sejam protegidos sem uma alteração de configuração posterior.

A chave é fornecida por você e nunca armazenada no banco de dados; apenas o ciphertext criptografado é. Perdê-la significa perder cada secret criptografado com ela.

Gere uma chave e armazene-a como secret do Worker com os seguintes comandos:

npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY

Opcional: overrides de valor estável

O EmDash auto-gera o secret HMAC de preview e o salt de hash de IP do comentarista e os persiste no banco de dados no primeiro uso. As variáveis de ambiente abaixo são overrides para casos onde você precisa fixar o valor — por exemplo, quando um Worker de preview em um processo separado precisa compartilhar o secret com seu site principal.

VariávelPropósito
EMDASH_PREVIEW_SECRETOverride para o secret HMAC de preview auto-gerado.
EMDASH_IP_SALTOverride para o salt de hash de IP do comentarista auto-gerado.
EMDASH_AUTH_SECRETOpcional. Se definido, é usado como fonte do salt de IP (a menos que EMDASH_IP_SALT também esteja definido, que tem precedência), mantendo os hashes de IP dos comentaristas estáveis para instalações que já dependem dele. Deixe não definido para um novo deploy.

Acesse variáveis de ambiente na sua configuração usando import.meta.env ou o binding env do Cloudflare.

Para o inventário completo de cada secret que o EmDash usa — incluindo locais de armazenamento, etapas de rotação e o que quebra quando uma chave é perdida — veja Secrets e Gerenciamento de Chaves.

Deploys de preview

Faça deploy de uma branch de preview:

wrangler deploy --env preview

Adicione uma seção de ambiente ao wrangler.jsonc:

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

Solução de problemas

”D1 binding not found”

Verifique se o nome do binding em wrangler.jsonc corresponde à sua configuração de banco de dados:

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

“R2 binding not found”

Verifique se o bucket R2 está corretamente vinculado:

// Deve corresponder: r2({ binding: "MEDIA" })
"binding": "MEDIA"

Erros de migração

Se você ver erros de schema, acompanhe os logs do Worker (wrangler tail) e reproduza o erro para capturar a mensagem subjacente — depois abra um issue com essa saída.