how-i-build

2026

how-i-build

Este site é o primeiro projeto documentado pelo próprio How I Build: um template e um registro das decisões tomadas para construí-lo

No arPúblicoAutor únicoNext.jsNext.jsTypeScriptTypeScriptBunBun

Alguns dos projetos em que eu trabalho são de clientes. Outros envolvem código que não faz sentido publicar porque ele é parte do próprio produto. Em quase todos, o que eu fiz fica fechado, e para quem olha de fora sobra só uma landing page ou uma linha no currículo. O que eu quero preservar aqui é o que não aparece nessas coisas: por que tomei cada decisão, quais problemas encontrei e o que considerei antes de escolher um caminho.

Foi por isso que criei o How I Build. O primeiro projeto que decidi documentar foi o próprio site, porque eu queria testar a ideia em algo que pudesse mostrar por completo, incluindo o código. O repositório reúne o template que dá estrutura ao site e o conteúdo que eu escrevo sobre os projetos. Mantive as duas partes separadas para que o template não precise saber nada sobre mim ou sobre o conteúdo publicado. As decisões abaixo começam, em grande parte, dessa separação.

Um repositório, duas branches

O template e o conteúdo do site precisam evoluir juntos, mas não deveriam ficar misturados. Eu queria poder usar o mesmo repositório para desenvolver o template sem colocar meu conteúdo dentro dele, e ao mesmo tempo manter o site que publico aqui atualizado com essas mudanças.

Decisão

Fiquei com um repositório e duas branches. A main contém o template e a site contém o conteúdo. Um workflow faz o merge de main → site a cada push, mantendo o site atualizado sem precisar repetir as mudanças manualmente.

Trade-offs

  • Toda mudança no template passa por dois merges antes de chegar em produção.
  • Arquivos que as duas branches precisam alterar, como a configuração e o índice de conteúdo, podem gerar conflitos. Nesses casos, a site sempre vence.
  • Quem clona o template também recebe uma segunda branch que talvez não precise. Por isso, o workflow simplesmente não faz nada quando essa branch não existe.

O idioma padrão não tem prefixo

Eu queria que o inglês fosse o idioma padrão do site, mas sem colocar /en em todas as URLs. Ao mesmo tempo, o português precisava continuar disponível em um caminho próprio. Isso deixava duas alternativas: prefixar os dois idiomas, ou não diferenciar os idiomas na URL. A primeira tirava a possibilidade de usar / para o inglês; a segunda criava problemas de indexação, compartilhamento e geração de páginas estáticas.

Decisão

Inglês responde em /projects, português em /pt/projects. /en/* redireciona para o caminho sem prefixo, então cada página tem exatamente um endereço. Saiu na v0.4.0.

Trade-offs

  • O proxy fica mais complexo que um redirect simples: rewrite, redirect canônico e preferência lida só na raiz
  • Montar um link passa a exigir um helper em vez de concatenar string
  • A preferência do leitor vale só em /. Um link profundo compartilhado abre no idioma em que foi compartilhado; isso é deliberado, mas pode surpreender

O tema é aplicado antes da página pintar

O tema precisava estar correto antes da primeira pintura da página. Ler o cookie no servidor parecia uma solução natural, mas criava dois problemas: a primeira visita não tem cookie, então quem prefere o tema escuro poderia ver um flash branco, e a leitura do cookie também tirava as rotas da renderização estática.

Decisão

Um script síncrono no <head>, antes de qualquer coisa renderizar. Sem cookie no servidor e sem estado React. A classe na <html> já está correta antes da hidratação, então o controle não precisa começar com um estado e corrigi-lo depois. Saiu na v0.5.0 e virou três opções na v0.8.0.

Trade-offs

  • Um script inline no documento, além de suppressHydrationWarning na <html>
  • O next/script não serve: ele adia conteúdo inline para depois da primeira pintura, que é justamente o que esta solução precisa evitar
  • O aria-pressed só fica correto durante a hidratação, porque o servidor realmente não sabe a preferência do leitor

Sem banner de cookies

O site guarda duas coisas: um cookie com o idioma escolhido e uma entrada de localStorage com o tema escolhido. As duas só são escritas quando o leitor usa o controle que as define.

Isso me levou a uma pergunta simples: o que exatamente o leitor estaria recusando? Se ele escolhe português, por exemplo, eu preciso guardar essa escolha para lembrar dela depois. Se ele recusar esse armazenamento, a funcionalidade que acabou de pedir deixa de funcionar.

Decisão

Sem banner. Uma página de privacidade no lugar: o que é guardado, por quê e como apagar. Saiu na v0.16.0.

Trade-offs

  • Colocar analytics depois significa adicionar o banner e reescrever aquela página
  • Quem espera um banner pode interpretar a ausência como esquecimento, e não como uma decisão

Um workflow que nunca rodou

O workflow que faz o merge da main na site foi escrito para disparar em release: published. Ele nunca rodou uma vez. Não falhou — nunca começou. Não houve execução vermelha nem notificação; a branch de conteúdo simplesmente ficou para trás.

O GitHub não inicia workflows a partir de eventos criados pelo GITHUB_TOKEN, e o release-please publica as releases usando esse token.

A parte desconfortável é que eu já tinha usado essa mesma propriedade de propósito, uma issue antes, para impedir que o workflow de release entrasse em loop com o próprio commit. O mecanismo era conhecido; eu só o apliquei ao contrário.

Decisão

Disparar em push na main, que é uma ação humana. Corrigido na v0.17.1.

Trade-offs

  • O sync passa a rodar a cada merge, e não a cada release. Isso acontece mais vezes do que o desenho original, mas mantém as branches mais próximas.
  • O commit do próprio release-please continua não disparando o workflow, então um bump de versão espera o próximo merge.

O blog mora dentro do mesmo site

Nem tudo que eu quero escrever é um case. Tem opinião, tem coisa nova que eu testei e quis registrar, e tem nota curta que não sustenta um registro de decisões inteiro. Eu queria um lugar meu para isso, e a saída óbvia era abrir um blog separado em alguma plataforma pronta, deixando este site só com os projetos.

Decisão

O blog ficou aqui, no mesmo repositório e no mesmo modelo de conteúdo dos cases: imports estáticos, um arquivo por idioma e o mesmo build conferindo os dois. Começou na v0.20.0 e se acomodou na v0.26.0, com índice, capas, tags, arquivo por mês e busca.

Trade-offs

  • Um post só entra no site quando existe em português e em inglês. O build recusa um texto que só tem um idioma, o que protege os cases e cria atrito justamente na nota curta.
  • As tags são escritas por idioma, então /blog/tags/processo e /blog/tags/process são páginas diferentes. A troca de idioma só encontra a equivalente porque ela ocupa a mesma posição nos dois arquivos, e quem garante isso é o build.
  • Sete releases seguidas foram para o blog antes de ele ter conteúdo meu de verdade. O que sustenta essa decisão daqui em diante é escrever, e essa parte não depende de código.

Referências