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
-
Use o provedor de cache Cloudflare do Astro para que regras de rota e
Astro.cachedefinam cabeçalhos de cache e a invalidação usecache.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/cloudflaredetectacacheCloudflare()e habilita Workers Cache na configuração de implantação gerada. -
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:
- Respostas sem cabeçalho
Cache-Controlainda são armazenadas em cache. O Workers Cache aplica frescor heurístico RFC 9111 — um200sem cabeçalho é armazenado por 2 horas. Dê a cada rota personalizada umCache-Controlexplícito (useprivate, no-storepara tudo dependente de sessão). - 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 Caching | Legacy: cloudflareCache() | |
|---|---|---|
| Config | "cache": { "enabled": true } + cacheCloudflare() de @astrojs/cloudflare/cache | cache: { provider: cloudflareCache() } de @emdash-cms/cloudflare |
| Storage | Platform Workers Caching | Cache API (caches.open / put / match) |
| Purge | cache.purge() de cloudflare:workers | Zone REST POST /zones/{id}/purge_cache |
| Secrets | Nenhum para purge | CF_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.
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.
Cloudflare AI Search
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.