@jeff_drumgod
EN

Como construí um blog MCP-native com D1, R2 e Vectorize

Um blog editável por IA, otimizado para SEO + GEO, rodando inteiro no edge da Cloudflare. Receita técnica, decisões e armadilhas.

Published: · Updated: · 5 min read
edge cloudflare ai mcp architecture

Antes de você ler outro post genérico sobre "como começar um blog", vou te mostrar algo diferente: o blog que você está lendo agora foi construído em uma noite, é editável por IA via Model Context Protocol, serve menos de 100KB de HTML por post e roda inteiro no edge da Cloudflare, sem origem, sem container, sem k8s.

Neste artigo eu mostro a arquitetura completa, as decisões que tomamos (eu e meus agentes), e o que não funcionou no caminho.

O problema com blogs em 2026

A maioria dos blogs técnicos hoje cai em uma das três categorias:

  1. Static-site generators (Hugo, Jekyll): rápidos para servir, dolorosos para editar fora do editor. IA precisa abrir PR.
  2. Headless CMS (Contentful, Sanity, Strapi): editor confortável, mas latência alta (500ms+ por página) e API gateway extra.
  3. WordPress / Ghost: maduros, mas pesados, com plugin sprawl e ataques de XSS bem documentados.

Eu queria os três benefícios: edição rápida, performance de SSG e integração nativa com IA sem nenhum dos custos. A resposta acabou sendo simples: o edge. E ainda mais sendo programador por natureza, construir algo simples hoje em dia, é como brincadeira de criança.

Diagrama da arquitetura: cliente edge worker, D1, R2, Vectorize - jeff.drumgod.com.br.png

Stack em uma frase

Cloudflare Workers servem cada request, D1 guarda o conteúdo (Markdown), R2 guarda imagens, Vectorize indexa para busca semântica. Um servidor MCP deixa o ChatGPT (ou qualquer cliente) publicar/editar/traduzir posts via tool calls.

A receita inteira tem 5 partes:

Camada Tecnologia Custo mensal*
Compute Cloudflare Workers $0 (tier free já é suficiente para começar)
DB D1 (SQLite) $0 — incluído
Storage R2 $0 — abaixo do free tier
Vector Vectorize $0 — abaixo do free tier
AI Workers AI $0 — neuron credits

Para volume típico de blog pessoal (10-50k pageviews/mês). (obviamente dependendo do seu nicho de conteúdo e seu engajamento social, isso pode variar).

Schema mínimo viável

O schema D1 cabe em 100 linhas de SQL:

CREATE TABLE posts (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  uuid            TEXT UNIQUE NOT NULL,
  slug            TEXT UNIQUE NOT NULL,
  primary_locale  TEXT NOT NULL DEFAULT 'en-US',
  title           TEXT NOT NULL,
  excerpt         TEXT,
  body_md         TEXT NOT NULL,
  cover_image     TEXT,
  status          TEXT CHECK(status IN ('draft','published','archived')),
  published_at    TEXT,
  created_at      TEXT DEFAULT (datetime('now')),
  updated_at      TEXT DEFAULT (datetime('now'))
);

Note três decisões deliberadas:

  • Markdown direto, não MDX: LLMs geram Markdown de forma natural. JSX em posts gerados por IA é frágil. Um caractere fora do lugar quebra o build inteiro.
  • uuid separado de slug: o slug pode mudar (rebranding, fix de typo). O uuid não, e é a chave canônica para integrações externas.
  • Sem campo html: HTML é derivado em runtime. Salvar HTML pré-renderizado parece otimização, mas torna invalidação de cache um pesadelo quando o renderer muda.

Tabelas auxiliares

As tabelas auxiliares podem fazer o que você espera: post_translations para i18n, post_tags (junção compartilhada com a tabela tags global do site), post_faqs para JSON-LD FAQPage, e post_revisions para auditoria. E o que mais você imaginar.

MCP server: a peça que muda tudo

O MCP server expõe as tools. E as que você mais vai usar, seriam algo como:

// posts.outline — gera outline a partir de tópico
{ topic: "Edge Functions vs Serverless tradicional" }
→ { title, hook, sections: [...], questions_to_answer: [...] }

// posts.suggest_metadata — analisa rascunho, sugere ajustes
{ body_md: "## Por que ...\n\n..." }
→ { slug, title, excerpt, seo_title, seo_description, tags, faqs }

// posts.publish — cria + publica em um request
{ title, body_md, tags, primary_locale, status: 'published' }
→ { uuid, slug, public_url, edit_url }

Na prática, eu escrevo o miolo (a opinião, os dados coletados no teste ou estudo, a história), e o meu agente, ou o ChatGTP diretamente na interface chatgpt.com propõe a revisão organizada das ideias: SEO, FAQs, tags consistentes com o resto do site. Tempo médio entre rascunho e publicação cai consideravelmente, e sua ideia de conteúdo não cai no esquecimento.

No final, você pode ainda voltar e revisar o conteúdo, sem perder o timming daquela postagem.

Princípio: o autor do conteúdo escreve o que só ele sabe, seu conhecimento tácito. A IA faz o que qualquer modelo bem-treinado conseguiria escrever depois de ler o rascunho.

Editor do control panel com preview lado a lado - jeff.drumgod.com.br.png

SEO + GEO + IA: três audiências, um post

O mesmo post precisa ser otimizado para:

  1. Google Search (SEO clássico), JSON-LD BlogPosting, FAQPage, hreflang, sitemap.
  2. Google AI Overviews / Perplexity (GEO — Generative Engine Optimization), meta robots com max-snippet:-1, max-image-preview:large, headings com IDs estáveis para citação por âncora.
  3. LLMs de fundação que crawlam para treinar, llms.txt indexado, posts.json (JSON Feed) com content_text completo machine-readable.

llms.txt incremental

A convenção llms.txt está virando o robots.txt da era LLM. Em vez de um único arquivo estático, eu sirvo um endpoint dinâmico que lista os 50 posts mais recentes:

# jeffdrumgod.com.br — Blog

> Feed JSON: https://jeffdrumgod.com.br/posts.json

## Posts recentes
- [Como construímos um blog MCP-native...](https://...) — Receita técnica completa.
- [Hello World!](https://...) — Apenas um exemplo!

O crawler de IA pega o índice, segue o link, lê o post completo no posts.json (que entrega Markdown bruto, sem ruído visual). Resultado: a IA cita você com mais precisão.

Headings com âncora estável

Cada <h2> e <h3> ganha um id derivado do texto, com <a href="#id" class="post-anchor">#</a> ao lado. Isso permite que LLMs façam citações com URL + fragmento (/blog/post#secao-x), exatamente como o Wikipedia faz.

Performance: 100KB por post

Números de exemplos de post (medido com curl -w):

  • HTML: ~95KB (gzip)
  • TTFB médio: 60ms (origem); ~25ms (CDN HIT)
  • LCP image (cover): pré-carregada com fetchpriority="high"
  • Zero JavaScript de runtime para o blog (View Transitions é opcional)

O segredo: render Markdown server-side com marked + shiki (engine JavaScript). Sem hidratação, sem framework client. O HTML que sai do Worker é o HTML final.

Trade-offs honestos

Nada disto é mágica. As decisões custam:

  • D1 não é Postgres. Sem JSONB, sem full-text search robusto (use Vectorize como substituto), sem extensions. Aceitei porque um blog não precisa disso obrigatoriamente.
  • Editor é texto puro com preview. Sem WYSIWYG. Pra mim é feature; pode não ser pra você.
  • Workers AI tem context window de 8k tokens no llama-3.1-8b (no caso, no worker). Então, tradução automática cria "chunks" do body, fazendo pedaços de ~3500 chars. Funciona, mas fronteira de seção pode confundir o modelo.
  • Cache invalidation depende de purge manual via API CF. Precisei construi mecanismo e regras do que precisa ser limpo e quanto, como em posts.publish/update/delete.

Próximos passos, ideias

No backlog para o segundo trimestre:

  1. OG image dinâmico via Satori + resvg em Worker — gerar uma capa de social com título sobreposto nas cores base e estilo do tema do blog.
  2. Comentários self-hosted — provavelmente Cusdis ou Giscus, mas estou olhando uma versão MCP-native onde leitor faz pergunta e a resposta vai para o /QnA namespace.
  3. Related posts via Vectorize — já tenho o índice; falta expor apenas no template (atualmente atrás de um feature flag).

Se você está construindo algo parecido — ou quer trazer essa stack pro seu produto — agende uma conversa. Ou me siga em @jeff_drumgod.

Get the latest by email

The best links and articles, straight to your inbox. No spam, unsubscribe anytime.

By subscribing, you agree to receive occasional emails. You can unsubscribe at any time.