Pular para o conteúdo
bruno.lombardi
← Todos os posts
  • 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.

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.

CamadaEscolhaPor quê
FrameworkNext.js 16, App RouterServer Components por padrão, saída estática
UIReact 19Pequenas ilhas de cliente só onde preciso
LinguagemTypeScript, strictTraduções e conteúdo são tipados
EstiloTailwind CSS v4Os tokens de design são variáveis CSS comuns
AnimaçãoMotionSprings e valores ligados à rolagem sem o peso
ConteúdoMDX no repositórioPosts 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>:

src/app/globals.css
: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:

src/i18n/dictionaries/pt-BR.ts
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:

src/i18n/rich.tsx
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.

  1. Guardar o idioma em estado do React em vez da URL, e carregar o outro dicionário quando o seletor recebe o mouse.
  2. Esmaecer a página por 140 ms com a Web Animations API, que só anima opacity e transform.
  3. Aplicar o novo texto de forma síncrona com flushSync, enquanto nada está visível.
  4. Voltar com o fade e atualizar <html lang>, o título e a barra de endereço com history.replaceState.
src/i18n/provider.tsx
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 transform e opacity. 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 compositor

Como 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/mdx
Por 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#

  1. 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. ↩