Cloudflare Workers fornece um runtime rápido e distribuído globalmente para 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 no Cloudflare (
wrangler login)
Configurar bindings
Crie wrangler.jsonc na raiz do seu projeto com bindings D1 e R2. O Wrangler provisiona ambos os recursos no primeiro deploy se eles ainda não existirem.
{
"$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",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media",
},
],
}
Configurar EmDash
Atualize sua configuração Astro para usar 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 interface admin é uma app React
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
Primeiro boot
As migrações do banco de dados são executadas automaticamente na primeira requisição após o deploy, e em cada boot subsequente se houver algo novo para aplicar.
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 no primeiro boot. O seed é lido em tempo de build a partir de .emdash/seed.json, do caminho em package.json#emdash.seed ou de seed/seed.json — o que for encontrado primeiro — e é incluído no bundle. Se nenhum estiver presente, um seed padrão embutido é usado. Deploys subsequentes em um banco de dados existente não alteram seu conteúdo.
Para alterar o schema ou modelo de conteúdo de um site já implantado, consulte Evoluindo um site implantado.
Publicação agendada
No Cloudflare Workers, publicação agendada, cron de plugins e tarefas de manutenção são executados a partir de um Worker Cron Trigger. Novos templates do Cloudflare incluem essa configuração automaticamente. Se você está atualizando um projeto existente, exporte a entrada do Worker EmDash de @emdash-cms/cloudflare/worker:
export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";
Em seguida, adicione um Cron Trigger ao wrangler.jsonc:
{
"triggers": {
"crons": ["* * * * *"],
},
}
Deploy
Faça deploy no Cloudflare Workers:
wrangler deploy
Seu site está agora ativo em https://my-emdash-site.<your-subdomain>.workers.dev.
Read Replicas
Para sites distribuídos globalmente, habilite a replicação de leitura do 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.
Consulte Opções de banco de dados — Read Replicas para modos de sessão e como a consistência baseada em bookmarks funciona.
Object Cache
Para reduzir a carga de leitura no D1, faça cache dos 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 em cada requisição:
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
Consulte Object Cache para configuração do KV, opções e comportamento de invalidação.
Workers Cache
O Workers Cache do Cloudflare ("cache": { "enabled": true } no wrangler.jsonc) coloca um cache de borda na frente do seu Worker: requisições correspondentes são servidas sem executar seu Worker. Isso funciona bem com EmDash:
- As respostas admin e API do EmDash enviam
Cache-Control: private, no-storee nunca são armazenadas. - Suas páginas públicas controlam seu próprio cache através dos cabeçalhos
Cache-Controlque retornam.
Duas coisas a saber antes de habilitar:
- Respostas sem cabeçalho
Cache-Controlainda são cacheadas. Workers Cache aplica frescor heurístico RFC 9111 — um200sem nenhum cabeçalho é cacheado por 2 horas. Dê a cada rota personalizada umCache-Controlexplícito (useprivate, no-storepara qualquer coisa dependente de sessão). - Páginas cacheadas são compartilhadas com editores logados. O cache roda antes do seu Worker, então não pode ser contornado com base em cookies da requisição. Um editor logado pode receber a variante anônima cacheada de uma página pública — sem a barra de ferramentas de edição visual — até que a entrada expire. Respostas renderizadas por editores nunca são armazenadas (possuem
private, no-store), então nada vaza na outra direção.
Domínio personalizado
Adicione um domínio personalizado no painel do Cloudflare:
- Vá para Workers & Pages > seu worker
- Clique em Custom Domains > Add Custom Domain
- Insira seu domínio e siga as instruções de configuração DNS
Acesso público ao R2
Para servir mídia diretamente do R2 (recomendado para performance):
- No painel do Cloudflare, vá para R2 > seu bucket
- Clique em Settings > Public access
- Habilite o acesso público e anote a URL pública
- Atualize sua configuração de armazenamento:
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxx.r2.dev"
}),
Autenticação Cloudflare Access
Se sua organização usa Cloudflare Access, você pode usá-lo como provedor de autenticação em vez de passkeys, fornecendo 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,
},
}),
}),
Consulte o guia de autenticação para todas as opções de configuração.
No Workers, o único handler embutido email:deliver é um stub de console de desenvolvimento, então
fluxos dependentes de email — login por magic-link, convites de equipe e notificações de
comentários — falham com “Email is not configured” em produção. O
plugin cloudflareEmail() entrega emails reais através do
Cloudflare Email Sending
usando um binding Worker nativo send_email, sem chaves API externas.
1. Registrar um domínio de envio
No painel do Cloudflare, vá para Email e verifique o domínio (ou endereço) de onde você envia. Email Sending rejeita mensagens de remetentes não verificados.
2. Adicionar o binding
Declare um binding send_email no 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, então ative o plugin em Admin → Extensions e escolha-o como provedor em Settings → Email.
Opções
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
from | string | { email, name? } | — (obrigatório) | Endereço do remetente em um domínio registrado para Email Sending. |
replyTo | string | — | Reply-To opcional, útil quando from é um endereço de subdomínio no-reply. |
binding | string | "EMAIL" | Nome do binding send_email no 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 quando habilitada. Configure-a 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 texto cifrado criptografado é armazenado. 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: sobrescritas de valores estáveis
EmDash gera automaticamente o secret HMAC de preview e o salt de hash de IP de comentaristas e os persiste no banco de dados no primeiro uso. As variáveis de ambiente abaixo são sobrescritas para casos onde você precisa fixar o valor você mesmo — por exemplo, quando um Worker de preview em um processo separado precisa compartilhar o secret com seu site principal.
| Variável | Propósito |
|---|---|
EMDASH_PREVIEW_SECRET | Sobrescrita para o secret HMAC de preview auto-gerado. |
EMDASH_IP_SALT | Sobrescrita para o salt de hash de IP de comentaristas auto-gerado. |
EMDASH_AUTH_SECRET | Opcional. Se definido, é usado como fonte de salt de IP (a menos que EMDASH_IP_SALT também esteja definido, que tem precedência), mantendo os hashes de IP de 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 — consulte 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",
},
],
},
},
}
Resolução de problemas
”D1 binding not found”
Verifique se o nome do binding no 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ê vir erros de schema, acompanhe os logs do Worker (wrangler tail) e reproduza o erro para capturar a mensagem subjacente — depois abra uma issue com essa saída.