Diário do Projeto
rafazingano.com.br

De nove HTMLs duplicados a 21 componentes

O header estava copiado nove vezes, com nove variações — seis delas com o mesmo bug de layout. Transformar layout em sistema é menos sobre escrever componente e mais sobre descobrir o que realmente se repete.

Página de experiência profissional do site, com a linha do tempo de cargos alternando dos dois lados de um trilho vertical.

Performance Baseline

Arquivos HTML duplicados
9 → 0
Componentes criados
21

Nove arquivos HTML, 2.756 linhas, e a mesma estrutura repetida em todos. A parte tediosa era óbvia. A parte interessante foi o que apareceu quando comecei a comparar as cópias lado a lado.

O bug que estava em seis dos nove arquivos

O header era fixed e ocupava a largura da tela. Só que o max-width do container estava aplicado no próprio elemento fixo:

O que estava em 6 dos 9 layouts
<header class="fixed top-0 w-full max-w-[1120px] mx-auto bg-surface shadow-sm">
  <!-- logo, nav, botão -->
</header>

Resultado: em telas maiores que 1120px, o fundo branco do header parava no meio e o conteúdo da página aparecia por baixo nas laterais. Só a home acertava, com o container num wrapper interno:

A forma correta, que só a home usava
<nav class="fixed inset-x-0 top-0 bg-surface shadow-sm">
  <div class="mx-auto max-w-page px-gutter">
    <!-- logo, nav, botão -->
  </div>
</nav>

Esse é o argumento a favor de componentizar que nenhuma métrica captura: com nove cópias, o bug existe seis vezes e é corrigido zero. Com um componente, existe uma vez e some para sempre.

Encontrei outros na mesma varredura: classes que o Tailwind nunca gerou porque não existiam (rounded-DEFAULT, Transition-opacity), sobras de ferramenta de design que viraram atributo (flat no shadows, docked full-width top-0), e dezenas de dark: órfãs — o layout tinha variantes de tema escuro, mas nenhuma paleta escura definida.

O que virou componente, e por quê

O critério não foi “aparece mais de uma vez”. Foi “muda junto”: se dois trechos sempre mudam pelo mesmo motivo, são o mesmo componente.

Header e Footer eram os óbvios. O menu virou dado em src/data/site.ts e o item ativo passou a ser derivado da rota, em vez de marcado à mão em cada arquivo — que era como as nove cópias divergiam.

Timeline foi o achado mais interessante. A página de experiência e o diário de projeto tinham o mesmo desenho — trilho vertical, cards alternando dos dois lados — implementado de dois jeitos completamente diferentes. Um com pseudo-elementos em CSS puro e nth-child(odd), outro com divs posicionados em absoluto. Nenhum dos dois sabia da existência do outro.

src/components/ui/TimelineEntry.astro
---
interface Props {
  lado: 'esquerda' | 'direita';
  data?: string;
  destaque?: boolean;
}

const { lado, data, destaque = false } = Astro.props;
const cardEsquerda = lado === 'esquerda';
---

<div class="group relative mb-16 flex w-full flex-col md:flex-row md:items-center">
  <div
    class:list={[
      'hidden w-1/2 items-center md:flex',
      cardEsquerda ? 'order-2 justify-start pl-12' : 'order-1 justify-end pr-12',
    ]}
  >
    {data && <span class="font-display text-label-sm uppercase">{data}</span>}
  </div>

  <div
    aria-hidden="true"
    class:list={[
      'absolute left-6 top-2 z-20 size-4 -translate-x-1/2 rounded-full border-4 border-background md:left-1/2',
      destaque ? 'bg-primary' : 'bg-surface-variant group-hover:bg-primary',
    ]}
  />

  <div
    class:list={[
      'w-full pl-16 md:w-1/2 md:pl-0',
      cardEsquerda ? 'order-1 md:pr-12 md:text-right' : 'order-2 md:pl-12',
    ]}
  >
    <slot />
  </div>
</div>

Hoje as duas páginas usam esse componente. A alternância virou i % 2, e o card entra por <slot> — o que permite conteúdo bem diferente de cada lado sem duplicar o esqueleto.

Chip revelou o oposto: o que parecia um componente eram quatro. As tags de tecnologia, as categorias do blog, os metadados do post e o badge da hero tinham estilos distintos e eu quase criei quatro arquivos. Viraram um componente com quatro variantes, porque mudam pelo mesmo motivo — se eu ajustar o raio dos chips, ajusto todos.

Onde eu não componentizei

Resistir também é decisão. As páginas de conteúdo — home, sobre, contato — têm markup próprio e não viraram um PaginaGenerica configurável por props. Cada uma tem um arranjo diferente e forçar um molde só geraria um componente com quinze props booleanas, que é pior que a duplicação que ele resolveria.

O critério que apliquei: componentizei o que se repete, não o que se parece.

O resultado

21 componentes, 2.805 linhas de .astro no total — contra 2.756 linhas dos nove HTMLs originais. O código não encolheu, e nem era essa a intenção.

O que mudou foi a superfície de alteração. Mudar o menu era editar nove arquivos e torcer para não esquecer nenhum; agora é editar um array. E os textos saíram do markup para src/data/, o que separou de vez o que é estrutura do que é conteúdo.

AstroComponentizaçãoRefatoração