Implantar no Cloudflare

Nesta página

Este guia implanta um site EmDash no Cloudflare Workers com D1 para o banco de dados e R2 para mídia. Comece com um template EmDash Cloudflare ou aplique a mesma configuração a um site Astro existente.

Pré-requisitos

  • Uma conta Cloudflare
  • As dependências do projeto instaladas
  • Wrangler autenticado no Cloudflare (pnpm wrangler login)

Configurar bindings

Os templates Cloudflare incluem o ponto de entrada completo do Worker e bindings D1 e R2 nomeados. No primeiro deploy, o Wrangler cria o recurso se o nome configurado ainda não existir. Mantenha os nomes em wrangler.jsonc; o Wrangler reconecta deploys posteriores aos mesmos recursos.

O template usa os seguintes bindings:

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

Os nomes DB, MEDIA e LOADER devem corresponder aos adaptadores EmDash. O Cron Trigger executa publicação agendada, tarefas de plugins, backups e manutenção. Veja Plugin sandbox se o site usa plugins sandboxed.

Configurar o 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, 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 o site não usa plugins de marketplace, registry ou sandboxed, omita sandboxRunner e o binding LOADER.

Adicionar o ponto de entrada do Worker

O ponto de entrada do Worker conecta o Astro ao Cron Trigger e exporta a ponte do plugin:

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

export { PluginBridge };

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

A exportação PluginBridge é inofensiva quando nenhum plugin sandboxed está instalado. Mantenha-a se o mesmo projeto puder habilitar plugins depois.

Para executar manutenção geral em um intervalo diferente de a cada minuto, passe a mesma expressão Cron para createScheduledHandler({ generalCron: "..." }) e para triggers.crons. Se diferirem, o handler registra e ignora o gatilho inesperado.

Compilar e implantar

Compile e implante o site uma vez para o Wrangler provisionar o banco D1 e o bucket R2 nomeados. O Wrangler usa o login local criado por pnpm wrangler login.

pnpm build
pnpm wrangler deploy

Com o modo de migração padrão auto, o EmDash aplica migrações core pendentes quando o Worker implantado recebe a primeira solicitação. Use Manage core database migrations quando um pipeline de implantação precisar aplicar migrações antes de o novo código receber tráfego, ou quando precisar inspecionar, verificar ou recuperar uma migração.

Se o banco estiver vazio (sem collections) 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 em tempo de build de .emdash/seed.json, do caminho em package.json#emdash.seed ou seed/seed.json — o que for encontrado primeiro — e incorporado ao bundle. Se nenhum estiver presente, um seed padrão embutido é usado. Deploys posteriores contra um banco existente deixam seu conteúdo intacto.

Para alterar o esquema ou o modelo de conteúdo de um site já implantado, veja Evolving a Deployed Site.

Colocar o Worker perto do D1

O Cloudflare executa um Worker perto do visitante por padrão. Solicitações EmDash renderizadas no servidor fazem várias idas e voltas ao D1, então use Targeted Placement para executar o Worker perto do primário D1 e acelerar essas solicitações.

O Wrangler aceita placement.mode: "targeted" com exatamente um seletor: region, host ou hostname. Escolha o valor que mira a localização do primário D1 e adicione o objeto placement resultante a wrangler.jsonc. Não habilite réplicas de leitura D1 com Targeted Placement. Mantenha a configuração session do EmDash no padrão "disabled", para que leituras e escritas usem o primário próximo.

Object cache

Para reduzir a carga de leitura no D1, coloque em cache resultados de consultas de conteúdo e configuração no Cloudflare KV. Leituras são servidas do KV em vez de consultar o banco a cada solicitação:

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

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

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

Workers Cache

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

Ativá-lo

  1. Use o provedor de cache Cloudflare do Astro para que regras de rota e Astro.cache definam cabeçalhos de cache e a invalidação use 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.
     },
    });

    O adaptador @astrojs/cloudflare detecta cacheCloudflare() e habilita Workers Cache na configuração de implantação gerada.

  2. Limpe respostas em cache do código do Worker com a API da plataforma. Esta chamada não precisa de credenciais REST do Cloudflare.

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // Or purge selected tags:
    await cache.purge({ tags: ["posts"] });

Respostas de administração 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 via Cache-Control / routeRules / Astro.cache.

Duas coisas a saber antes de ativar:

  1. Respostas sem cabeçalho Cache-Control ainda são armazenadas em cache. O Workers Cache aplica frescor heurístico RFC 9111 — um 200 sem cabeçalho é armazenado por 2 horas. Dê a cada rota personalizada um Cache-Control explícito (use private, no-store para tudo dependente de sessão).
  2. Páginas em cache são compartilhadas com editores conectados. O cache roda antes do Worker, então não pode ser contornado com base em cookies da solicitação. Um editor conectado pode receber a variante anônima em cache de uma página pública — sem a barra de edição visual — até a entrada expirar. Respostas renderizadas pelo editor em si nunca são armazenadas (carregam private, no-store), então nada vaza na outra direção.

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

Preferido: Workers CachingLegacy: cloudflareCache()
Config"cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cachecache: { provider: cloudflareCache() } de @emdash-cms/cloudflare
StoragePlatform 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 sites novos. Mantenha cloudflareCache() apenas se já depender do comportamento da Cache API.

Também não confunda nenhum deles com o object cache (objectCache: kvCache({ binding: "CACHE" })), que coloca em cache resultados de consultas ao banco no KV — uma camada separada sob o Worker.

Domínios personalizados

A primeira implantação recebe uma URL workers.dev. O domínio personalizado já deve ser um domínio ativo gerenciado pelo Cloudflare na mesma conta do Worker. Depois que o Worker responder com sucesso em sua URL workers.dev, adicione o domínio de produção como rota Wrangler:

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

Implante de novo e verifique ambos os endereços. Manter o endereço workers.dev disponível ao testar DNS ajuda a distinguir um problema de roteamento de um problema de aplicação.

Acesso público ao R2

Por padrão, a mídia é servida pela rota de mídia autenticada do EmDash. Se o bucket tiver um domínio personalizado público, defina essa origem como publicUrl para que as URLs de mídia geradas a usem:

storage: r2({
	binding: "MEDIA",
	publicUrl: "https://media.example.com",
}),

O acesso público ao bucket se aplica a todo objeto alcançável, não só à mídia. Backups JSON automáticos usam o prefixo backups/ no mesmo backend de storage, então não exponha esse prefixo pelo domínio público. Choose media storage explica o limite seguro.

Transformação de imagens

O EmDash redimensiona e recodifica mídia R2 dentro do Worker, pelo binding IMAGES do Cloudflare. O componente Image de emdash/ui e imagens em rich text ambos renderizam pelo 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 de origem direto do binding R2, sem fetch HTTP. Essas transformações continuam funcionando atrás do Cloudflare Access e com global_fetch_strictly_public. Mídia servida de uma URL de bucket — veja Public R2 Access — usa o endpoint de transformação próprio do adaptador, que busca o arquivo por HTTP antes de transformar.

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

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

Para ver o que um deploy realmente obtém, 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 fatura essas transformações como Images transformations. Cada combinação única de imagem de origem e parâmetros é faturada uma vez por mês calendário, e solicitações repetidas nesse mês são gratuitas. Se um site tem 500 imagens de origem e solicita um tamanho de miniatura e um de herói para cada imagem, esses dois conjuntos de parâmetros contam como 1.000 imagens transformadas naquele mês. O plano Images Free cobre 5.000 transformações únicas por mês. Além desse limite, transformações em cache ainda são servidas, mas novas retornam erro 9422 e a solicitação de imagem falha.

Autenticação Cloudflare Access

O Cloudflare Access pode substituir a autenticação por passkey pelo provedor de identidade anexado a um aplicativo Access. O valor de audience é uma configuração secreta de runtime; mantenha-o fora de astro.config.mjs nomeando sua variável de ambiente:

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

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50,
			Editors: 40,
		},
	}),
}),

Defina CF_ACCESS_AUDIENCE com pnpm wrangler secret put CF_ACCESS_AUDIENCE. O authentication guide explica provisionamento de usuários, papéis padrão e sincronização de papéis.

E-mail

Workers de produção não têm serviço padrão de entrega de e-mail. Login por magic link, convites de equipe e notificações de comentários retornam Email is not configured até um plugin de e-mail estar ativo.

O plugin de e-mail do Cloudflare usa um binding send_email. Primeiro integre e verifique o domínio do remetente com Cloudflare Email Sending. O Cloudflare rejeita mensagens cujo From não seja um remetente aceito.

Adicione o binding e registre o provedor:

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

Após a implantação, ative o plugin em Extensions e selecione-o em Settings → Email. O envio falha até o remetente ser aceito e o binding existir.

O plugin usa o binding chamado EMAIL a menos que sua opção binding nomeie outro. Se for o único provedor de e-mail ativo, o EmDash o seleciona automaticamente. Se mais de um provedor estiver ativo, escolha o provedor Cloudflare em Settings → Email. O endereço opcional replyTo recebe respostas sem alterar o From aceito. Um plugin pode definir replyTo em uma mensagem individual, o que substitui esta opção para essa mensagem.

O plugin AI Search precisa de um registro de plugin nativo e de um binding ai_search_namespaces. Após implantá-los, abra Cloudflare AI Search na administração, escolha as collections e execute Sync All Content. A sincronização inicial indexa conteúdo publicado antes de o plugin ser habilitado; hooks mantêm alterações posteriores sincronizadas.

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

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

Exponha a rota de busca a partir do site:

export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";

Adicione a interface de busca a um layout. O slot do gatilho aceita um botão que combine com o design do site:

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

Segredos do Worker

Armazene valores secretos com pnpm wrangler secret put <NAME>. Não os coloque em wrangler.jsonc nem os leia de valores import.meta.env de build.

EMDASH_ENCRYPTION_KEY criptografa configurações de plugin declaradas como segredos. Defina-o antes de salvar um segredo de plugin e preserve-o separado dos backups D1. Durante a rotação, forneça a nova chave primeiro e retenha chaves mais antigas após vírgulas até que cada segredo de plugin tenha sido salvo de novo. Secrets and key management descreve rotação e recuperação.

A ponte do plugin lê este binding de segredo do Worker diretamente. A rota gerada de configurações da administração o lê por process.env. Com nodejs_compat, o Cloudflare preenche process.env por padrão para datas de compatibilidade a partir de 2025-04-01. Projetos fixados em uma data anterior também devem adicionar nodejs_compat_populate_process_env antes de salvar configurações criptografadas.

O EmDash lê seus segredos de process.env em runtime. Código do Worker lê bindings de env, importado de cloudflare:workers. Nunca leia segredos por import.meta.env: o Vite substitui esses valores em tempo de build e pode escrevê-los no bundle do servidor.

O segredo HMAC de preview e o salt de IP do comentador são gerados e armazenados no banco a menos que você forneça substituições em runtime. Secrets and key management lista as variáveis exatas, locais de armazenamento e efeitos de rotação.

Implantações de preview

Ambientes Wrangler nomeados não herdam bindings. Crie recursos de preview separados e escreva-os no ambiente preview antes de compilar:

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

O ambiente de preview deve repetir cada binding que o Worker de preview usa. Os bindings core D1, R2 e sandbox têm esta forma depois que o Wrangler escreve os identificadores de recurso:

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

Use o UUID de preview escrito pelo Wrangler. Repita bindings opcionais de KV, AI Search, e-mail e outros quando o preview usar esses recursos. Adicione segredos só de preview com pnpm wrangler secret put <NAME> --env preview.

Compile e implante o ambiente de preview. Sua primeira solicitação aplica migrações core pendentes pelo modo padrão auto.

pnpm build
pnpm wrangler deploy --env preview

Verifique a URL de preview, o login da administração, o upload de mídia e qualquer binding opcional antes de compartilhá-la. Nunca aponte um binding de preview para um banco ou bucket de produção.

Verificar a implantação

Após a implantação, solicite uma página pública, entre em /_emdash/admin, faça upload e recupere um arquivo de mídia de teste, e confirme que o handler agendado aparece em pnpm wrangler tail.

Solução de problemas

”D1 binding not found”

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

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

“R2 binding not found”

Verifique se o bucket R2 está corretamente vinculado:

// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"

Erros de migração

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