---
title: "Por que pausei meu CMS custom pra apostar no EmDash"
description: "Passei boas horas construindo um CMS na stack da Cloudflare (Workers + D1 + R2 + Hono) com Claude Code pra substituir o Keystatic. Funcionou. E mesmo assim resolvi pausar. Esse post conta o porquê, o que aprendi com a stack, e por que o EmDash chegou na hora certa."
published: "2026-05-23"
tags: ["cms", "cloudflare", "emdash", "astro"]
source: "https://marciotoledo.com/blog/por-que-pausei-meu-cms-custom-pra-apostar-no-emdash"
---

# Por que pausei meu CMS custom pra apostar no EmDash

Esse site usa Astro e implementei o Keystatic, e eu gosto bastante. Conteúdo no repo, admin leve, deploy via git. Pra projeto pessoal, recomendo sem hesitar.

Mas algumas coisas começaram a apertar quando pensei em rodar Keystatic em projetos de clientes da Toledo Interactive: ordenação ruim e sem persistir, sem drafts reais (precisa criar branch separado), sem armazenar dados, não tem como customizar muito o admin e os componentes custom dependem do `@keystatic/astro` que ainda não suporta Astro 6 até o momento de publicação desse post. Nenhum desses pontos é dealbreaker pra um site. Mas pra rodar cinco ou dez, vira atrito.

Em vez de pular pra outro CMS pronto, decidi construir o meu. Era pra ser projeto de aprendizado e ferramenta de produção ao mesmo tempo: rodar nas mesmas APIs que ofereço pros meus clientes (Workers, D1, R2), usar Claude Code o tempo todo como par de programação, e validar até onde dá pra ir com a stack da Cloudflare sem depender de SaaS de terceiro.

Foram aproximadamente 8 horas espalhadas ao longo de alguns dias. Cheguei num CMS funcional, deployado em `cms.marciotoledo.com`, com todo o conteúdo do site migrado pra dentro do D1. Funciona. E ainda assim, resolvi pausar antes de plugar ele no site público.

Esse post conta os dois lados. Primeiro o que eu construí (vale como referência técnica pra quem quer fazer algo parecido). Depois por que o EmDash chegou e fez sentido apostar nele em vez de seguir com o CMS.

## A stack que escolhi pra construir o CMS

Decisão número um: nada de framework de CMS pronto (Payload, Strapi, Directus). A ideia era controlar 100% da superfície e aprender. Stack:

- **Cloudflare Workers** como runtime. Plano free dá 100k requests/dia, mais que suficiente. O CMS é um Worker único hospedado em `cms.marciotoledo.com` (rota da zona).
- **D1** (SQLite gerenciada da Cloudflare) pro banco. Posts, works, tools, skills, quotes, tags, users, sessions, media metadata.
- **R2** pro storage de mídia. Uploads de imagem vão direto pra lá, servidos via rota do Worker (`/media/*`) com cache `immutable`.
- **Hono** como router HTTP. Pequeno (14KB gzipped), bindings nativos pra D1 e R2, suporte a Hono JSX (renderização server-side com sintaxe React, sem React no bundle).
- **Tailwind CSS via CDN** pra UI do admin. Não precisa build step, página carrega em 80ms.
- **Alpine.js + SortableJS via CDN** pra interações mínimas (toggles de idioma, drag-and-drop). Sem framework JS no bundle.
- **PBKDF2-SHA256 nativo** (via `crypto.subtle`) pra hash de senha. Tentei scrypt no início e estourou o limite de 10ms de CPU do Workers Free, com erro 1102. Trocar pra PBKDF2 resolveu na hora.
- **HMAC-signed cookie** pra sessão. Sem JWT, sem magic link, single-user com email/senha. \~80 linhas de código pro auth inteiro.

Bundle final do Worker: 158KB, 36KB gzipped. Bem dentro do limite do plano free (3MB).

## O que ele faz hoje

| Feature                                                              | Status |
| -------------------------------------------------------------------- | ------ |
| Login com sessão de 30 dias (rate limit 10/min/IP)                   | pronto |
| CRUD de posts com EasyMDE (markdown editor)                          | pronto |
| Upload de imagens pro R2 inline no editor                            | pronto |
| CRUD genérico de works, tools, skills, quotes                        | pronto |
| Drag-and-drop pra reordenar collections                              | pronto |
| Toggle PT/EN nos campos bilíngues, com persistência via localStorage | pronto |
| Slug auto-gerado do título + botão pra regenerar                     | pronto |
| API REST pública `/api/v1/*` com Bearer token                        | pronto |
| Backup semanal automatizado via cron (D1 → R2 com retenção de 12)    | pronto |
| Botão "Publicar site" pra disparar deploy hook do Pages              | armado |

Conteúdo migrado: 10 posts (com markdown + custom directives reescritas), 44 works, 2 tools, 2 skills, 29 quotes, 7 singletons. Tudo em D1, imagens em R2 com paths flat (`media/foo.jpg`, sem subpasta por slug pra desacoplar imagem do post).

## O que aprendi construindo isso

Algumas coisas que só descobri colocando a mão.

**A stack da Cloudflare é absurdamente boa pra esse caso.** Cold start zero, latência baixa, custo previsível. O conjunto D1 + R2 + Workers cobre 100% do que um CMS pequeno precisa, sem precisar de Postgres, Redis, S3, ou qualquer outra peça externa. Pra projetos da Toledo Interactive (sites institucionais, blogs, landing com formulário), isso é o nível certo de complexidade.

**Workers Free tem armadilhas reais.** O limite de 10ms de CPU é mais apertado do que parece. Scrypt estoura. Markdown rendering pesado também (não cheguei a testar, mas vi gente reclamando). Pra um CMS read-mostly que serve HTML estático e API JSON, é tranquilo. Pra qualquer transformação CPU-intensa, plano pago é necessário.

**Image Transformations da Cloudflare não funciona sobre origem externa no plano free.** O `/cdn-cgi/image/.../<url-externa>` retorna 403. Solução: o bucket R2 vai ser montado como binding no Pages do site também, expondo em `marciotoledo.com/media/*`. Assim o helper de imagem do site continua usando path local e a transformação funciona.

**Schema em código declarativo é poderoso.** Cada singleton e cada collection do CMS é descrita por um objeto TypeScript (`{ fields: [...], hasSlug: true, sortable: true }`), e a UI inteira é gerada genericamente desse schema. Adicionar um campo novo é trocar uma linha, não escrever um form do zero. Esse é o padrão de Sanity e Payload, e funciona bem mesmo numa versão minimalista.

**Claude Code mudou minha velocidade de desenvolvimento.** Foram dias inteiros em que eu descrevia o que queria e ia validando o resultado, sem escrever quase nada de código manualmente. Pra projeto de aprendizado, vale principalmente porque eu ainda lia tudo e entendia o que estava sendo gerado. Pra projeto de produção crítica, talvez deixaria de fazer review-line-by-line e isso preocupa. Mas a aceleração é real.

**Construir CMS é um buraco sem fundo.** Eu cobri o básico. Falta: media library com busca, image cropping, versionamento de conteúdo, multi-user com permissões, 2FA, preview de drafts no site público, integração com forms de contato. Cada um desses é uma feature séria, com edge cases. A versão "boa o suficiente" pra Marcio Toledo não é a versão "boa o suficiente" pra clientes pagantes.

## Aí o EmDash apareceu

A Cloudflare lançou o [EmDash](https://emdashcms.com/) em abril de 2026. Não tinha radar pra ele quando comecei o projeto. Quando vi, foi um soco no estômago e ao mesmo tempo um alívio.

Vou listar exatamente por que ele bateu fundo:

1. **Mesma stack que eu escolhi.** Astro + Workers + D1 + R2. Não um SaaS proprietário, não um Postgres caro. A infra que eu já entendo, no provedor que eu já uso.
2. **Open-source MIT.** Sem licenciamento por site, sem cobrança por seat. Hospedando no Cloudflare, custo mínimo: $5/mês de Workers Paid pra ter os limites maiores e desbloquear features como Worker Loader (sandboxing de plugins). Sem surpresa no fim do mês.
3. **Spiritual successor do WordPress.** Essa parte é importante pra mim. Tenho mais de uma década evitando WordPress (segurança, performance, manutenção, a experiência de admin que envelheceu mal). Sempre faltou um substituto sério pra quando o cliente precisa do "eu mesmo edito meu site" de verdade, sem ser Webflow ou Wix. Sanity é técnico demais pra cliente não-dev, Strapi é pesado demais pra hospedar, Payload é caro pra escalar. EmDash propõe ser o que o WordPress foi: o default razoável.
4. **Integração nativa com Astro.** Uma linha no `astro.config.mjs`. O CMS roda no mesmo Worker do site, lê do mesmo D1, escreve no mesmo R2. Sem ponte, sem API entre eles.
5. **Admin pronto em `/_emdash/admin`.** Schema vive na DB (não em código), seedado de um `seed.json`. Editar schema é editar JSON e rodar reseed. Tipo Strapi mas mais leve.
6. **MCP server embutido.** Documentação acessível direto via Claude Code como ferramenta. Plus: ferramentas pra agentes IA interagirem com conteúdo do site direto da CLI.
7. **Plugin system com sandboxing.** Webhooks, forms, integrações. Plugins rodam isolados em Dynamic Workers (no Cloudflare). Comunidade já tá publicando coisa interessante.
8. **190 releases em \~1 ano.** Ritmo de quem está entregando. 10k stars no GitHub. Beta declarado, mas com supply-chain hardening sério (cooldown de release, bloqueio de subdeps exóticos). Não é vibe-coded.

Resumindo: é literalmente o CMS que eu estava construindo, mas pronto, mantido por uma comunidade, com features que eu não ia ter tempo de fazer.

## A conta de custo de oportunidade

A pergunta óbvia: você gastou várias horas (não muitas por causa do Claude Code) e vai jogar fora?

Não exatamente. A conta é outra.

**Cenário A: continuar com o CMS custom.**

- Fase 2 (refatorar o site pra consumir a API do CMS, remover Keystatic + React): estimadas 25-35 horas (bem menos com Claude Code).
- Validação paralela (1-2 semanas usando o admin de verdade).
- Manutenção contínua: \~20h/ano de patches, edge cases, upgrades de stack. Sozinho.
- Adicionar features que clientes vão pedir (forms, multi-user, media library decente): mais 100-200h ao longo de 6-12 meses (ou bem menos usando Claude Code).
- Resultado: CMS proprietário que só eu conheço, com superfície de bug que só eu vejo.

**Cenário B: pausar o CMS e apostar no EmDash.**

- Construir o site da [toledointeractive.com](https://toledointeractive.com) no EmDash do zero (já está instalado, schema starter, lab real).
- 2-4 semanas usando ele em produção pra sentir UX, validar i18n bilíngue, descobrir limitações.
- Contribuir com issues e PRs (já tenho stack mental do problema). Especialização em vez de manutenção isolada.
- Resultado: stack que clientes podem herdar, comunidade que carrega o peso comigo, features novas chegam sem eu trabalhar.

A diferença é energia. Manter um CMS proprietário consome energia que eu prefiro gastar entregando projeto pra cliente.

## O que faço agora

Pausei o CMS. Não deletei nada. O Worker continua no ar em `cms.marciotoledo.com`, com todo o conteúdo migrado, como referência e backup. O site do `marciotoledo.com` segue rodando em Keystatic em produção, intocado. A Fase 2 (refator pra consumir do CMS) está pausada por tempo indefinido.

Em paralelo, instalei o EmDash em um ambiente de testes e vou construir o site da agência nele de verdade. Daqui umas semanas, com EmDash debaixo da unha, decido se migro este site também ou se sigo no Keystatic enquanto o EmDash amadurece pra cobrir os casos mais complexos (i18n bilíngue por field, markdown com directives custom, design opinionado).

> **TIP**
>
> Se você está pensando em construir um CMS próprio em 2026: vale como projeto de aprendizado. Não vale como caminho pra ter um CMS de produção mantível sozinho. Olha o EmDash antes.

> **NOTE**
>
> Ouvi falar do EmDash quando ele estava na versão 0.10. Comecei a usar oficialmente na 0.14.0. Apenas para registro futuro 🙃

## O que valeu

Mesmo pausando o projeto, valeu cada hora. O que ganhei:

- **Domínio real da stack Cloudflare.** Workers, D1, R2, secrets, cron triggers, deploy via wrangler, bindings, rate limits do plano free. Tudo isso é base pra qualquer projeto que eu monte da agência daqui pra frente.
- **Senso de proporção do que é construir um CMS.** Os 80% mais visíveis são os 20% do trabalho. Os 20% chatos (auth, cache invalidation, edge cases de upload, gestão de schema, reorder, busca) são os 80% que ninguém vê e dão dor de cabeça.
- **Validação de Claude Code como par de programação.** A diferença de velocidade quando o código pode ser descrito em texto e revisado em tela é gigante. Pra projetos onde eu domino o domínio, é multiplicador real. Pra projetos onde eu não domino, é faca de dois gumes.
- **Material pra esse post.** Esse aqui é um caso onde a jornada vale mais que o destino.

## Resumo

Construí um CMS custom em uma semana pra substituir o Keystatic, na stack Workers + D1 + R2 + Hono + Astro. Funcionou. Logo depois a Cloudflare lançou o EmDash, que é literalmente o mesmo CMS, pronto, open-source, com comunidade.

Pausei o meu, montei o EmDash em outro projeto da agência pra validar, e vou contribuir com a comunidade dele em vez de manter código que só eu conheço. O CMS custom continua no ar como referência. O site `marciotoledo.com` segue em Keystatic até a decisão final.

Não foi tempo perdido. Foi o melhor jeito de eu chegar com base própria pra entender o que um CMS sério precisa fazer, e por que vale apostar num projeto coletivo em vez de carregar o meu sozinho.

## Referências

- Não sabe o que é Astro? [astro.build](https://astro.build/)
- Não sabe o que é Keystatic? [keystatic.com](https://keystatic.com/)
- EmDash CMS. [emdashcms.com](https://emdashcms.com/)
- EmDash no GitHub. [github.com/emdash-cms/emdash](https://github.com/emdash-cms/emdash)
- Cloudflare. [Workers Free Plan limits](https://developers.cloudflare.com/workers/platform/limits/)
- Cloudflare. [D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/)
- Cloudflare. [R2 pricing](https://developers.cloudflare.com/r2/pricing/)
- Hono. [hono.dev](https://hono.dev/)
- Marcio Toledo. [Alternativas de CMS para Astro em 2026](/blog/alternativas-cms-astro-2026) (post anterior, comparação técnica)
- Marcio Toledo. [Preços e planos dos CMSs para Astro em 2026](/blog/precos-e-planos-de-cms-para-astro-em-2026) (post anterior, números)
