- nextjs
- architecture
- design
Como este site é construído
As ideias, a stack e as pequenas decisões por trás do brunolombardi.me: uma página que se comporta como um trace, temas solarizados, traduções ao vivo e animações leves até em aparelhos antigos.
▸ Publicado em 5 min de leituraPT
Resumo
- A home é um trace: cada seção é um span de uma requisição atravessando um sistema.
- Next.js 16, React 19, TypeScript, Tailwind v4 e Motion, com uma lista de dependências propositalmente curta.
- O idioma troca no lugar, com um único fade leve, e as animações só mexem em transform e opacity.
- Tudo é tipado: uma tradução faltando quebra o build.
Neste artigo
A maioria dos portfólios é uma lista: um título, uma grade de logos, uma linha do tempo. Eu queria que o meu parecesse algo feito de propósito por um engenheiro, então dei a ele uma regra: a página se comporta como uma requisição atravessando um sistema.
Essa única ideia explica quase tudo o que você vê. A navegação à esquerda é uma espinha que se preenche conforme você rola. Minha carreira é desenhada como um trace distribuído, com cada experiência sendo um span. Até o tempo de leitura deste blog é uma barrinha, porque uma leitura mais longa é um span mais longo.
A stack#
Mantive a lista curta de propósito. O navegador só precisa de Next.js, React e Motion; todo o resto roda no build.
| Camada | Escolha | Por quê |
|---|---|---|
| Framework | Next.js 16, App Router | Server Components por padrão, saída estática |
| UI | React 19 | Pequenas ilhas de cliente só onde preciso |
| Linguagem | TypeScript, strict | Traduções e conteúdo são tipados |
| Estilo | Tailwind CSS v4 | Os tokens de design são variáveis CSS comuns |
| Animação | Motion | Springs e valores ligados à rolagem sem o peso |
| Conteúdo | MDX no repositório | Posts amigáveis a code review |
Na home, aperte ↓ ou simplesmente role a página para iniciar o trace.
Dois temas, um conjunto de tokens#
O tema claro é um pergaminho solarizado inspirado no Noctis Lux, com um acento quente cor de argila. O tema escuro é um verde-azulado profundo. Ambos são definidos uma única vez, como variáveis CSS, e alternados por um atributo data-theme no <html>:
:root,
:root[data-theme="light"] {
--bg: #fbf5e6;
--ink: #173f45;
--accent: #d2603a;
}
:root[data-theme="dark"] {
--bg: #04242b;
--ink: #ece5cf;
--accent: #ee8460;
}O Tailwind v4 mapeia essas variáveis para utilitários (bg-bg, text-ink, text-accent), então nenhum componente contém uma cor fixa. Mudar um tema é mudar uma variável.
Traduções são tipadas#
O site fala inglês e português do Brasil. O en.ts define o tipo Dictionary, e todo outro idioma precisa respeitá-lo:
import type { Dictionary } from "./en";
export const ptBR: Dictionary = {
hero: {
headlines: ["Construo software que continua {simples} conforme cresce."],
},
// ...todas as outras chaves, ou o build falha
};Esqueceu uma chave e o tsc se recusa a compilar. A marcação {palavra} indica a palavra que recebe o estilo de destaque, tratada por um helper de poucas linhas:
export function rich(text: string): ReactNode[] {
return text.split(/(\{[^}]+\})/g).map((part, i) =>
part.startsWith("{") ? <span key={i} className="text-accent italic">{part.slice(1, -1)}</span> : part,
);
}Trocar de idioma sem recarregar#
Eu não queria que mudar de idioma parecesse navegar. A home guarda o idioma em estado no cliente, então a página nunca remonta, a posição de rolagem sobrevive e o hero não recomeça.
- Guardar o idioma em estado do React em vez da URL, e carregar o outro dicionário quando o seletor recebe o mouse.
- Esmaecer a página por 140 ms com a Web Animations API, que só anima
opacityetransform. - Aplicar o novo texto de forma síncrona com
flushSync, enquanto nada está visível. - Voltar com o fade e atualizar
<html lang>, o título e a barra de endereço comhistory.replaceState.
const out = root.animate(
[{ opacity: 1, transform: "translateY(0)" }, { opacity: 0, transform: "translateY(8px)" }],
{ duration: 140, fill: "forwards" },
);
await out.finished;
flushSync(() => setState({ lang: next, dict: nextDict }));Animação que continua leve#
Um site animado não deve punir um celular de cinco anos atrás. As regras são simples:
- Animar apenas
transformeopacity. Largura, altura e top forçam layout a cada quadro. - Manter as mudanças de estado pequenas. O título digitado vive em um componente próprio, então cada tecla re-renderiza uns cinquenta nós em vez da página inteira.
- Pausar tudo que fica em loop quando sai da tela.
- Respeitar
prefers-reduced-motion: sem digitação, sem carrossel e com troca de idioma instantânea.
- animate={{ width: "100%" }} // layout a cada quadro
+ animate={{ scaleX: 1 }} // só no compositorComo este blog funciona#
Conteúdo#
Os posts são arquivos MDX no repositório, em src/content/blog/<slug>/en.mdx e pt-BR.mdx. O slug é o mesmo nos dois idiomas, e o inglês é o fallback. O frontmatter é validado no build, então um erro de digitação em uma data quebra o build em vez de ir para produção.
Blocos de código#
O destaque de sintaxe é feito pelo Shiki no build, com as mesmas gramáticas do VS Code1. A saída com tema duplo faz as cores seguirem o tema do site só com CSS, e nada é destacado no navegador.
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdxPor que não um framework de conteúdo?
Eu queria menos peças móveis. MDX mais uma pequena camada de conteúdo que lê os arquivos, valida e ordena já basta para um blog pessoal, e não há nada para migrar quando uma ferramenta muda de rumo.
Construindo com uma dupla de IA#
Eu construí este site com o Claude Code como dupla de programação. O que fez funcionar foi um arquivo AGENTS.md com as regras: usar os tokens de design, manter os textos nos dicionários, tirar cada fato do meu perfil e nunca inventar um. Trate o agente como um novo colega de time e escreva o que você diria a ele no primeiro dia.
O melhor prompt é um bom conjunto de restrições.
Footnotes#
-
O Shiki reutiliza as gramáticas TextMate por trás do VS Code, então o destaque aqui é igual ao que você vê no seu editor. ↩