Como funciona o blog template Astro do Sinkra Hub: tour técnico completo
O blog template do Sinkra Hub é Astro 6 SSR + Cloudflare Workers + D1 + Drizzle, com esqueleto GEO fixo, API REST autenticada, fork por business × idioma e cinco ADRs canônicos sustentando as decisões. Este artigo cobre o tour técnico completo do código real, com paths e comandos verificáveis no monorepo.
Principais conclusões
- 01Template canônico vive em apps/blog-template; cada business × idioma é um fork independente em apps/blog-{business}-{lang}/.
- 02Stack: Astro 6 SSR + Cloudflare Workers + D1 + Drizzle, com Preact apenas na ilha de busca e zero JS desnecessário no cliente.
- 03Esqueleto GEO fixo emite Article + BreadcrumbList + FAQPage + SpeakableSpecification por artigo.
- 04API REST com Bearer auth; /api/search e /api/health são públicos. Publish dispara IndexNow + Google ping via waitUntil().
- 05Servido como subdiretório /blog para consolidar autoridade SEO no apex; ADR-026 documenta robots.txt Allow: / intencional.
Por que existe um template canônico em vez de cada business escrever o seu?
O hub atende quatro businesses (AllFluence, AIOX, Academia Lendária, Bilhon) com necessidade de blog. Sem template, cada business escreveria sua versão, três meses depois cada uma teria divergido em decisões fundamentais (cache, schema, sitemap), e atualizações de SEO técnico precisariam ser feitas N vezes. Com template, cada business é um fork em apps/blog-{business}-{lang}/ que herda decisões e identidade própria via wrangler.toml local.
O ADR-025 formaliza esse modelo: o template canônico é o source-of-truth, sync periódico propaga mudanças para os forks, e arquivos sinalizados como fork-specific (wrangler.toml, identidade visual, conteúdo) ficam preservados durante o sync. O ADR-023 formaliza por que cada idioma é um Worker separado em vez de um Worker poliglota: latência, simplicidade de configuração e isolamento de D1.
Que stack roda no template e por que cada peça?
O runtime é Cloudflare Workers (edge SSR). O framework é Astro 6 com adapter Cloudflare. O banco é D1 (SQLite na edge da Cloudflare) acessado via Drizzle ORM. A UI é Tailwind CSS 3 com Preact apenas na ilha de busca — todo o resto é HTML estático server-rendered, zero JavaScript desnecessário no cliente. TypeScript strict, Bun como package manager, e nodejs_compat ligado para o Drizzle conseguir importar node:async_hooks.
A escolha de Astro em vez de Next.js está formalizada no ADR-024: o adapter Cloudflare nativo da Astro entrega edge SSR com output: server sem ajuste, enquanto Next.js exige configuração mais pesada e tem cold start menos previsível em Workers. A escolha de D1 em vez de Postgres é dimensional: D1 é SQLite na edge com replicação geográfica e custo previsível para o volume típico de blog. Postgres entra quando o dataset cresce ou quando queries com JOIN pesado dominam.
Como funciona o fork por business × idioma?
O comando scripts/scaffold-blog-business.sh aiox pt-BR gera um fork pronto. Cada fork resultante é um Cloudflare Worker independente, com SITE_LANG pré-preenchido em wrangler.toml, identidade de marca via ORG_NAME, ORG_URL, ORG_LOGO_URL, e D1 próprio. Routes do Worker apontam para subpaths como {dominio}/blog/* em vez de subdomínio dedicado.
Conteúdo é nativo no idioma alvo, nunca tradução automática (ADR-027). A racional é dupla: tradução automática perde nuance e prejudica citação por LLM no idioma alvo, e o squad de produção (content-geo) é multi-fork por design — gera artigo nativo em qualquer idioma desde que receba --fork apps/blog-{business}-{lang}/ e leia o SITE_LANG. Tudo mais — schema, sitemap, llms.txt — é gerado automaticamente respeitando o SITE_LANG.
O que está no esqueleto GEO fixo de cada artigo?
Todo artigo renderizado em /{slug} tem a mesma estrutura visual: breadcrumb, H1, summary box com borda azul (marcado SpeakableSpecification), meta-linha com autor e datas, AI share buttons (Perplexity/ChatGPT/Claude/Gemini com prompt pré-preenchido), hero image, KeyTakeaways de 5 bullets, TableOfContents auto-gerado dos H2s, corpo HTML, tags, FAQ e relacionados.
JSON-LD emitido por artigo: Organization e WebSite (site-wide, vão em layouts/Base.astro); Article com image, wordCount, articleSection, keywords, autor com sameAs; BreadcrumbList; e FAQPage quando há FAQ. Os campos GEO ficam na própria tabela articles: hero_image_url, key_takeaways (JSON array), faq (JSON array de q/a), reading_time_min. As migrations 0001_geo_fields.sql e 0003_geo_squad_columns.sql contêm o schema.
Como a API REST do template está organizada?
Todos os endpoints sob /api/* exigem header Authorization: Bearer ${BLOG_KEY}, exceto /api/search e /api/health que são públicos por design. Operações disponíveis: criar artigo (entra como draft), listar com filtros, ler, atualizar, deletar e publicar. Publicar dispara IndexNow + Google sitemap ping em background via waitUntil(), sem bloquear a resposta.
Detalhes do roteamento: o middleware em src/middleware.ts faz canonicalização de host (301 de www para apex via SITE_URL), redirect de URL legada (/blog/artigos/{slug} → /blog/{slug}) e auth gate dos /api/*. src/lib/runtime-env.ts centraliza o acesso ao binding (import { env } from cloudflare:workers) — a API antiga Astro.locals.runtime.env foi removida em Astro 6 e o template já está migrado.
Que endpoints especializados servem LLMs e crawlers?
Quatro endpoints específicos para retrieval de IA: (1) /sitemap.xml com lastmod dinâmico por artigo (D1 query) e hreflang quando há idiomas alternativos; (2) /llms.txt no padrão emergente de llmstxt.org — H1 do site, blockquote da descrição, listas markdown agrupadas por categoria; (3) /llms-full.txt com corpus completo dos artigos publicados, HTML stripped, citação direta sem precisar crawl; (4) /robots.txt com Allow: / total intencional.
O ADR-026 explica a decisão do Allow: /: este blog existe para maximizar exposição em search, retrieval e training corpus. Bloquear ingestão de LLM seria fechar a porta da exposição — a defesa contra mau uso é que conteúdo gated/comercial nunca entra no blog (vai para outras propriedades). Para conteúdo que existe para citação, abrir é a estratégia correta.
Por que o blog vive em /blog (subdiretório) e não em subdomínio?
O blog é servido como {seu-dominio}/blog/*, não blog.{seu-dominio}. Razão: SEO consolidado. O Google concentra autoridade no domínio raiz em vez de fragmentar entre subdomínios, então autoridade do site principal transfere para o blog (e vice-versa). A combinação base: /blog + trailingSlash: ignore em astro.config.mjs mais o helper src/lib/paths.ts centralizam todos os links internos, então mudar o base path no futuro é uma linha só.
Detalhe não-óbvio: a combinação base + trailingSlash: never quebra a rota index do Astro (404 em /blog), por isso o template usa ignore e deixa o canonical tag consolidar SEO em vez de redirect. Para os artigos, a estrutura é /blog/{slug}, não /blog/artigos/{slug} — o segmento /artigos/ foi removido em abril de 2026 porque era redundante e URLs mais curtas citam mais limpo em LLMs.
Quais decisões estão registradas em ADR e por quê?
O template tem cinco ADRs canônicos sustentando as decisões maiores: ADR-023 Multi-Site Per Language (1 Worker por business × idioma); ADR-024 Canonical Stack (Astro 6.2 + CF Workers + D1 + Drizzle); ADR-025 Template + sync flow; ADR-026 robots.txt Allow: / intencional (citation > training-protection); ADR-027 Conteúdo nativo no idioma (não tradução automática).
O critério para virar ADR é simples: a decisão tem trade-off real e é difícil de reverter. Cada ADR documenta o problema, alternativas consideradas, decisão escolhida e consequências. Quando alguém propuser "vamos traduzir automaticamente" em seis meses, o ADR-027 é a resposta documentada — ou aceita o argumento original, ou produz um ADR-027 v2 com as razões novas. Sem ADRs, decisões viram folclore e voltam à mesa toda vez que time muda.
Como o blog template do Sinkra Hub se compara com Astrowind, Ghost, Hashnode e WordPress?
Templates de blog não são todos iguais — diferem em modelo de hospedagem, vendor lock-in, esqueleto técnico para LLM e custo de operação por business. A matriz abaixo cobre as 12 dimensões mais sensíveis para quem precisa decidir entre o template canônico do hub e alternativas SaaS ou open-source.
Top-line
| Atributo | Sinkra blog template | Astrowind | Ghost (SaaS) | Hashnode | WordPress |
|---|---|---|---|---|---|
| Render | Astro 6 SSR edge | Astro SSG | Node SSR | SSR proprietário | PHP SSR |
| Banco | D1 (SQLite na edge) | Markdown filesystem | MySQL | Proprietário | MySQL |
| Multi-business | Fork × business × idioma | Manual | 1 instância = 1 site | 1 conta = 1 blog | Multi-site (admin único) |
| License | Open-source (template) | MIT | MIT (Ghost core) | SaaS proprietário | GPLv2 |
| Vendor lock-in | Zero (self-host) | Zero | Médio (SaaS opcional) | Alto | Médio (plugins) |
| Esqueleto GEO fixo | Embutido (KT, FAQ, Speakable) | Não | Não | Não | Plugin |
| llms.txt + llms-full.txt | Embutido | Não | Não | Não | Plugin |
Matriz por dimensão (12)
| # | Dimensão | Sinkra template | Astrowind | Ghost | Hashnode | WordPress |
|---|---|---|---|---|---|---|
| 1 | Latência global | Edge sub-100ms | Static (CDN) | Regional | Edge | Depende host |
| 2 | Schema técnico embutido | Article+FAQPage+Breadcrumb+Speakable | Article básico | Article + JSON-LD | Article | Plugin (Yoast/Rank Math) |
| 3 | JS no cliente default | Zero (exceto busca Preact) | Zero | Theme-dependent | Theme-dependent | Theme + plugins |
| 4 | Multi-idioma per business | 1 Worker × business × idioma (ADR-023) | i18n manual | 1 site cobre N idiomas | 1 conta = 1 idioma | Plugin (WPML/Polylang) |
| 5 | Editor humano | API/CLI only | Filesystem (markdown) | WYSIWYG admin | WYSIWYG | Gutenberg |
| 6 | Publicação programática | API REST + skill content-geo | Filesystem commit | API REST | API GraphQL | API REST |
| 7 | Custo previsível | Pay-per-request CF | Static = $0 | $11–$199/m | Free + paid tier | Hosting + plugins |
| 8 | Newsletter built-in | Não | Não | Sim | Sim | Plugin |
| 9 | Comments built-in | Não | Não | Sim | Sim | Sim |
| 10 | Governance commit | 5 ADRs canônicos | Time individual | SaaS owner | SaaS owner | Plugin maintainers |
| 11 | Fork por idioma | scaffold-blog-business.sh | Manual | Não | Não | Multi-site |
| 12 | Robots.txt para LLM | Allow: / explícito (ADR-026) | Default (open) | Default | Default | Configurável |
Verdict: Sinkra blog template ganha quando o cenário é multi-business, GEO-first, governance formal e custo previsível em escala. Astrowind ganha em simplicidade absoluta para 1 site sem CMS. Ghost ganha em editor humano + newsletter + comments built-in. Hashnode ganha em distribuição via comunidade dev. WordPress ganha em ecossistema de plugins. A escolha depende do trade-off entre controle técnico (Sinkra) e conveniência operacional (SaaS).
Que tech-debt o template carrega assumidamente?
O CLAUDE.md do template lista três itens de tech-debt explicitamente: (1) Tailwind 3 + @astrojs/tailwind está deprecated em Astro 6, migração para Tailwind 4 + @tailwindcss/vite é EPIC futuro; (2) CSP API nativa do Astro 6 ainda não habilitada, vira sprint dedicado; (3) Built-in Fonts API do Astro 6 não usada, ainda usa link rel preload manual.
Listar tech-debt explicitamente é parte da disciplina KISS da Constitution: .claude/rules/kiss-no-overengineering.md tem cinco gates antes de adicionar qualquer estrutura nova, e o oposto — admitir débito conhecido em vez de esconder — é o que evita que o template vire monumento intocável. Quem migrar primeiro Tailwind 4 vira proposta de PR com EPIC anexado; quem propor CSP escreve a story; o template incorpora.
Perguntas frequentes
Posso trocar Astro por Next.js neste template?
Por que D1 e não Postgres?
O fork precisa ser deploy independente?
Como evitar drift entre forks e o template canônico?
E se o LLM citar errado meu conteúdo?
Sobre o autor
Equipe Editorial
Equipe responsável pela curadoria, revisão e publicação de artigos técnicos no blog.