A arquitetura deste blog: como o formigadev é construído
A arquitetura deste blog
O primeiro post já contou por que esse site existe. Esse aqui é sobre como ele é montado por dentro: o que é o Analog.js, por que cada peça da stack está aqui, como um post nasce até virar página no ar, e o que mudou de verdade quando fui atrás de SEO e performance de verdade (com número, não achismo).
O que é o Analog.js
Se você nunca ouviu falar: Analog.js é um meta-framework pra Angular. "Meta" porque ele não substitui o Angular, ele organiza um projeto Angular inteiro em volta de convenções prontas, do jeito que o Next.js faz com React ou o SvelteKit faz com Svelte.
Na prática, ele resolve três coisas que, sem ele, eu teria que montar na mão:
- Roteamento por arquivo. Não existe uma lista de rotas em lugar nenhum do
código. O arquivo
src/app/pages/blog/[slug].page.tsjá significa a rota/blog/:slug. O nome e a posição do arquivo dentro depages/definem a URL. - Conteúdo em Markdown. Analog.js lê arquivos
.mdde uma pasta de conteúdo e entrega isso pronto pra um componente Angular consumir, com front-matter (aquele bloco---no topo do arquivo) virando um objeto tipado. - Build com Vite. Todo o build (dev server, bundle de produção, pré-renderização de página) roda em cima do Vite, que é bem mais rápido que o build padrão do Angular CLI pra esse tamanho de projeto.
Por baixo, continua sendo um app Angular normal: componentes standalone, Signals, os mesmos conceitos de sempre. O Analog.js só tira o trabalho de configurar o resto em volta.
Um exemplo real: é assim que a página de um post pega o conteúdo do arquivo Markdown certo, só perguntando pelo parâmetro da rota:
// src/app/pages/blog/[slug].page.ts
export const routeMeta: RouteMeta = {};
export default class BlogPost {
readonly post$ = injectContent<PostAttributes>({
param: 'slug',
subdirectory: 'posts',
});
}
Sem routeMeta explícito, sem registrar rota em nenhum outro arquivo. O nome
[slug].page.ts já é a definição da rota.
A árvore de diretórios
src/
├── app/
│ ├── components/ # search, comments, share-buttons, dark-mode-toggle...
│ ├── pages/ # roteamento por arquivo (@analogjs/router)
│ │ ├── blog/
│ │ │ ├── [slug].page.ts
│ │ │ └── index.page.ts
│ │ ├── tags/[tag].page.ts
│ │ ├── index.page.ts
│ │ └── sobre.page.ts
│ ├── pipes/
│ ├── app.ts # shell: nav, footer, JSON-LD do site
│ └── seo.service.ts # meta tags, hreflang, canonical, JSON-LD
├── content/
│ ├── posts/*.md # posts em português
│ └── en/posts/*.md # mesma slug, versão em inglês
└── server/routes/api/ # rotas de servidor, se precisar
scripts/
├── generate-search-index.mjs # roda no prebuild
├── generate-seo-files.mjs # sitemap, robots, rss
├── generate-og-card.mjs # card de compartilhamento por post
└── upload-image.mjs # sobe imagem pro Firebase Storage
content/ é uma árvore paralela: content/posts/x.md e content/en/posts/x.md
compartilham o mesmo slug e são resolvidos pelo locale em runtime, não por
rota duplicada.
A stack, peça por peça
- Angular 22 + Analog.js: base do site, como já expliquei acima.
marked+marked-shiki+marked-gfm-heading-id: transformam o Markdown em HTML, com highlight de código de verdade (não um <pre> sem cor) e umidautomático em cada heading, pra dar pra linkar uma seção específica de um post.flexsearch: motor de busca que roda inteiro no navegador do leitor. Sem back-end de busca, sem chamada de rede a cada letra digitada.giscus: comentários usando GitHub Discussions como banco de dados. Sem tabela de comentário pra manter, sem endpoint de moderação.- Firebase Hosting: serve os arquivos estáticos atrás de CDN.
- Firebase Storage: guarda as imagens que não são branding do site (diagrama de post, card de compartilhamento), pra não deixar o repositório pesado com binário.
Cada peça entrou porque resolve uma coisa específica, não porque "é o que se usa". O critério foi sempre: dá pra fazer isso sem servidor rodando o tempo todo?
Como um post nasce
- Escrevo o post em português (
src/content/posts/), com front-matter (.md title,description,date,tags). - Escrevo a versão em inglês do zero (
src/content/en/posts/), mesmo.md slug. Não é tradução linha a linha, é o mesmo assunto pensado pra quem lê em inglês. - Se o post precisa de diagrama, gero o SVG e subo pro Firebase Storage com
upload-image.mjs. - Gero o card de compartilhamento (a imagem que aparece quando alguém manda o
link no WhatsApp): um script (
generate-og-card.mjs) monta um HTML com o título de verdade sobreposto no mesmo fundo da capa da página, um navegador headless tira um screenshot 1200x630, e isso também sobe pro Storage. - Um script de validação confere se os dois arquivos (pt e en) existem e têm o front-matter completo antes de eu seguir.
npm run buildgera o HTML de cada rota já pronto (pré-renderizado).- Deploy pro Firebase Hosting.
Nenhum desses passos precisa de um serviço externo de CMS. Publicar é escrever arquivo, rodar script, subir.
SEO: o que entra em cada página
Todo esse trabalho de SEO não é genérico, é o que o SeoService
(src/app/seo.service.ts) monta pra cada rota, toda vez que a página muda:
- Título e description próprios por página, não um texto fixo repetido em todo lugar.
- Canonical e hreflang (
pt-BR,en,x-default) apontando pra versão certa em cada idioma, pro Google não achar que são páginas duplicadas. - Open Graph e Twitter Card: título, descrição, imagem. Em posts, a imagem é o card gerado no passo 4 acima (JPEG com o título real), não um SVG genérico. SVG é ruim pra isso porque boa parte dos leitores de link preview (WhatsApp, por exemplo) não renderiza esse formato.
og:image:alt: descrição da imagem pra quem recebe o link e usa leitor de tela.- JSON-LD: dado estruturado (
BlogPosting,Person,WebSite) que ajuda buscador a entender quem escreveu, quando publicou, do que se trata. - Sitemap e RSS gerados automaticamente no prebuild, a partir da lista real de posts, sem manutenção manual.
Tudo isso já nasce pronto no HTML porque o site é pré-renderizado (mais sobre isso na próxima seção). O Google não precisa executar JavaScript pra ver o conteúdo.
Essa chamada é o que roda toda vez que a página de um post é montada:
this.seo.update({
title: post.attributes.title,
description: post.attributes.description,
path: `/blog/${post.attributes.slug}`,
image: ogImageUrl(post.attributes.slug),
imageAlt: post.attributes.title,
type: 'article',
publishedTime: isoDate,
});
Um método só, chamado com os dados do post, e o SeoService decide sozinho
título, canonical, hreflang, Open Graph e JSON-LD a partir disso.
Performance: o que mudou quando fui medir de verdade
Documentar arquitetura é fácil de fazer parecer bonito no papel. O teste de verdade é rodar o Lighthouse contra o site publicado e ver o que aparece. Rodei, e apareceu coisa real:
- CSS bloqueando o primeiro render. O CSS global (uns 3KB) carregava como <link> externo, obrigando o navegador a esperar esse download antes de pintar a página. Como é o mesmo arquivo em toda rota, um passo no build agora cola o CSS direto dentro do <head> de cada HTML pré-renderizado. Menos uma requisição no caminho crítico.
- Foto de autor gigante pra um avatar pequeno. A imagem tinha 449x484 e 84KB, exibida a 56px de largura. Cortei pra 280x280 e recomprimi: 22KB. Uma imagem 4x menor que ninguém percebe diferença, porque o tamanho de exibição sempre foi pequeno. Resultado em produção: Performance 99, LCP (tempo até o maior elemento visível aparecer) em 2.1s, Best Practices 96.
Exemplo de fix pequeno com efeito real: um passo a mais no build agora troca o <link> de CSS por um <style> inline em cada página gerada, porque o arquivo é o mesmo (3KB) em toda rota e não vale a pena pagar uma requisição extra pra baixá-lo:
// scripts/inline-critical-css.mjs (roda depois do vite build)
const css = readFileSync(cssPath, 'utf-8');
const html = readFileSync(pagePath, 'utf-8');
writeFileSync(pagePath, html.replace(linkTag, `<style>${css}</style>`));
Acessibilidade: o que a auditoria achou
No dia 18 de julho de 2026, no Google I/O Extended João Pessoa, evento do GDG João Pessoa, assisti a palestra de Angelo Dias, "Acessibilidade Web escalável: como a IA Generativa está salvando a WCAG na nuvem", sobre usar IA generativa, Gemini e Shift-Left Testing pra apoiar acessibilidade desde cedo no desenvolvimento. Foi o empurrão que faltava pra parar de teorizar e rodar uma auditoria de verdade neste post.
Essa parte custou mais barato do que eu esperava, e é a que mais teria impacto real num leitor usando leitor de tela ou navegando sem mouse. As regras usadas como referência vêm do WCAG (Web Content Accessibility Guidelines), o padrão internacional que define o que conta como "acessível" na prática, com critérios objetivos e mensuráveis, como o de contraste mínimo do primeiro item abaixo:
- Contraste de cor insuficiente. As tags de cada post (
#angular,#ssr...) tinham contraste de 3.45:1 no tema escuro, abaixo do mínimo de 4.5:1 exigido pra texto pequeno. Causa real: umopacity: 0.75no elemento pai esmaecia a tag junto com a data ao lado, quando só a data devia ficar mais apagada. Troquei porcolorno elemento certo, sem mexer na tag. - Imagem sem
alt. A imagem de capa do post não tinha atributoalt. Como ela é puramente decorativa (o título real já existe como texto separado, sobreposto por CSS), o certo aqui não é escrever uma descrição, é marcaralt=""explicitamente: isso diz pro leitor de tela "pule essa imagem, ela não carrega informação". - Página sem <main>. O <router-outlet> no shell do app
(
src/app/app.ts) não estava dentro de nenhum marco <main>. Sem isso, quem navega por teclado ou leitor de tela perde o atalho de pular direto pro conteúdo, tendo que passar pelo menu toda vez.
Os dois últimos, lado a lado (antes/depois):
<!-- antes -->
<img ngSrc="/images/post-cover-bg.svg" width="1200" height="630" />
<router-outlet />
<!-- depois -->
<img ngSrc="/images/post-cover-bg.svg" alt="" width="1200" height="630" />
<main>
<router-outlet />
</main>
Resultado em produção: Acessibilidade 100.
O placar final
Juntando as três seções acima, medido contra a página deste post em produção: Performance 99, Acessibilidade 100, Best Practices 96, SEO 100. Nenhum desses números veio de otimização prematura, vieram de rodar a ferramenta e consertar o que ela realmente achou.

O que ficou de fora, de propósito
- Nenhum CMS. Post é arquivo Markdown no repositório, versionado com o resto do código.
- Nenhum backend próprio. Comentário vive no GitHub Discussions via giscus, busca roda no navegador do leitor.
- Nenhuma imagem de capa por post dentro da página. Todo post usa o mesmo fundo compartilhado, com o título sobreposto via CSS. Menos decisão de design por post, mais consistência visual entre eles.
Cada uma dessas é uma peça a menos rodando em produção, o que no fim é o motivo de esse blog não ter exigido praticamente nenhuma manutenção desde que foi ao ar.
Dúvidas ou sugestão? Deixa aqui em baixo, a ideia é crescer junto com a comunidade.
