Como construí o eliel.work
Um portfólio de engenheiro full-stack sênior é um objeto estranho. Quem lê sabe ler código, então animação bonita sozinha não impressiona. Mas a página também precisa carregar no celular em menos de dois segundos, ranquear de forma orgânica e convencer um recrutador que não conhece a sua stack de que você leva o ofício a sério. As exigências são altas:
- Lighthouse no verde em todas as métricas no mobile com throttle, e de forma honesta, medido no PageSpeed e não apenas no meu computador.
- Animação que remeta ao cuidado da Apple sem virar um parque de diversões.
- Tipografia editorial que sustente a voz da página, em vez de uma pilha de utilitários fingindo ter personalidade.
- Suporte a dois idiomas (inglês como padrão e português como espelho) sem dobrar o custo de manutenção.
A seguir, como o site que você está lendo ficou pronto, incluindo os erros que cometi no caminho.
A stack
Sou usuário diário de Next.js, mas não foi ele que escolhi aqui.
Um portfólio é mais ou menos 80% de conteúdo estático e 20% de animação. O Next.js carrega o runtime do React em cada página, mesmo quando ela é estruturalmente estática, o que custa de 50 a 80 KB de JavaScript para hidratar componentes que nem se movem. Em um perfil de celular com throttle, essa diferença separa uma nota 100 de uma 92 no desempenho do Lighthouse. O Astro gera HTML no momento do build e só hidrata as ilhas que você declara.
A escolha, então, foi o Astro 5 com output: 'static', ilhas de React para o GSAP, coleções de conteúdo em MDX para os estudos de caso e para este blog, e Tailwind v4 com um bloco @theme que define todos os tokens a partir do arquivo DESIGN.md. O deploy fica no Cloudflare Pages, totalmente estático, sem necessidade de runtime na borda nesta primeira versão.
O custo dessa decisão é real: o Astro não me dá o mesmo modelo mental do Next.js que afiei ao longo de anos. Mas prefiro pagar esse preço uma vez a arcar com o peso no desempenho a cada carregamento de página.
Fraunces e Geist no lugar de Domaine e ABC Favorit
O design é inspirado de forma consciente na Resend: fundo preto, títulos editoriais em serifa de tamanho generoso, brilhos atmosféricos em cores de baixa opacidade e nenhuma sombra. A Resend usa as fontes Domaine Display e ABC Favorit, ambas proprietárias e caras.
Para um portfólio pessoal, pagar de 200 a 600 dólares por estilo de fonte é exagero. Estas foram as substituições:
- A Fraunces (variável, com optical sizing) substitui a Domaine Display nos títulos de 76 a 96 px. Com os recursos
ss01eligaativados, é a serifa gratuita mais próxima em personalidade. - A Geist substitui a ABC Favorit no corpo de texto. Tem menos personalidade que a original em 16 px, o que compenso com um espaçamento entre letras um pouco mais apertado.
- A Inter cuida dos rótulos de interface e a Geist Mono, do código.
As quatro são hospedadas no próprio site, via fontsource. A Fraunces variável é pré-carregada para o topo da página, e as demais carregam com font-display: swap.
Um detalhe me custou uma hora de depuração: o fontsource declara a família como 'Geist Sans', e não 'Geist'. Se você erra o nome, o navegador recai para a fonte do sistema sem reclamar.
GSAP dentro de ilhas de React
O mapa de animação tem quatro momentos pontuais, não um desfile:
- O título do topo. O
SplitTextdo GSAP quebra o<h1>em palavras e as revela com um leve atraso entre elas, saindo de um desfoque e subindo para a posição final. - A prévia dos trabalhos. O
ScrollTriggerprende a seção pela altura de uma tela enquanto três cartões de estudo de caso aparecem em sequência. - A faixa de números da FortCred. Os contadores animam de zero até o valor final em 1,6 segundo. Números acima de mil são exibidos com separador de milhar de acordo com o idioma.
- As transições entre páginas. O
<ClientRouter />nativo do Astro usa a View Transitions API do navegador. Os títulos dos cartões carregam umview-transition-name, de modo que o título se transforma suavemente ao navegar da listagem para a página dedicada.
Cada animação vive em um componente React que consome o GSAP pelo hook useGSAP, do pacote @gsap/react. O hook roda a animação dentro de um gsap.context() e faz a limpeza automaticamente quando o componente é desmontado, o que importa porque as transições de página do Astro destroem os componentes durante a navegação.
Importo essas ilhas na página .astro correspondente com client:visible, então o código da faixa de números só é baixado quando o usuário rola até ela. O topo da página, que aparece de imediato, usa client:load, já que não faz sentido esperar a verificação de visibilidade quando a seção é a primeira coisa na tela.
Um detalhe menos óbvio: a preferência prefers-reduced-motion: reduce desliga todas as ilhas. O conteúdo estático já está no estado visual final, então quem prefere menos movimento vê tudo na hora, sem nenhuma animação rodando.
A seta do botão que todo mundo copia
Os botões do site fazem aquele movimento visto em tantas landing pages: a seta aparece ao passar o mouse, deslizando para dentro enquanto o texto se ajusta um pouco. A implementação mais comum anima a largura com overflow-hidden, o que trava no Safari. A versão mais limpa usa colunas de grid do CSS:
<button class="group inline-flex items-center">
<span class="transition-transform group-hover:-translate-x-0.5">
{label}
</span>
<span class="grid grid-cols-[0fr] group-hover:grid-cols-[1fr]
transition-[grid-template-columns] duration-200">
<span class="overflow-hidden">
<ArrowRight class="ml-1.5 -translate-x-1.5 opacity-0
group-hover:translate-x-0 group-hover:opacity-100
transition-all duration-200" />
</span>
</span>
</button>
A coluna da seta anima de 0fr para 1fr, ou seja, a largura natural do conteúdo, sem nenhum valor fixo. O ícone faz um fade combinado com deslocamento, e o texto recebe um leve empurrão para a esquerda como ênfase. São 200 milissegundos com ease-out e nenhuma trava.
Performance: a tela de abertura que custou dois segundos
A primeira medição real do Lighthouse, feita no site publicado e não no meu computador, voltou com 82 no mobile. O bloqueio de thread e o deslocamento de layout já estavam zerados, porque o JavaScript é leve e nada se move durante o carregamento. O problema estava todo na renderização, com a maior pintura de conteúdo (o LCP) levando 3,6 segundos.
A causa era, com alguma ironia, o elemento de que eu mais gostava: a tela de abertura com a marca. O texto “eliel.work” surge com uma animação no primeiro carregamento e, da forma como eu havia construído, a parte “.work” começava invisível e só aparecia depois que o React hidratava o componente. O Lighthouse apontou justamente esse elemento como o LCP da página, e ele ficava oculto por cerca de dois segundos, à espera da hidratação. Na prática, eu havia transformado a minha própria marca no elemento mais lento do site.
A correção foi simples a ponto de ser constrangedora: entregar o texto completo já visível no HTML inicial. A animação de saída continua igual, ela apenas deixou de esconder o conteúdo antes da primeira pintura. Só essa mudança derrubou o Speed Index de 5,8 para 2,8 segundos.
Fica um aprendizado: uma animação de entrada bonita pode ser justamente o que atrasa a sua página. Antes de otimizar qualquer outra coisa, vale conferir qual elemento o Lighthouse aponta como LCP.
Dois ajustes menores completaram o trabalho:
- O avatar era uma imagem de 384 por 384 exibida em um espaço de 96 pixels. Um
srcsetcom versões de 128, 256 e 384 deixa o navegador escolher a menor adequada, o que no celular significa cerca de 5 KB em vez de 10. - O CSS era carregado como um arquivo separado, bloqueando a renderização. A opção
build.inlineStylesheets: 'always'do Astro insere os 10 KB diretamente no HTML e elimina essa requisição extra.
Onde tudo isso chegou:


São 95 no mobile, 99 no desktop e 100 em acessibilidade, práticas recomendadas e SEO nos dois. Os cinco pontos que faltam no celular vêm da tela de abertura e da animação do topo, uma escolha consciente. Eu poderia remover as duas e alcançar 100, mas considero que o momento de marca compensa esses pontos. O importante é saber com clareza o que se está trocando.
Houve ainda uma rodada dedicada à acessibilidade, que resolveu o que o Lighthouse corretamente apontava: um texto de rodapé com contraste pouco abaixo do mínimo recomendado, um título que pulava de h1 para h3, a logo da Forge Labs sendo anunciada duas vezes por leitores de tela, um link “Saiba mais” sem contexto e um seletor de idioma cujo rótulo acessível não correspondia ao texto visível. Nada disso é perceptível para a maioria das pessoas, mas é a diferença entre 92 e 100 e, mais importante, entre um site que qualquer pessoa consegue usar e um que exclui parte do público.
i18n sem dobrar o código
O roteamento de idiomas do Astro 5 coloca o inglês na raiz (/) e o português em /pt/. As coleções de conteúdo são organizadas por pasta de idioma (src/content/work/en/ e src/content/work/pt/), e os helpers de busca em src/lib/content.ts filtram pelo prefixo. As tags hreflang, incluindo x-default, são geradas em todas as páginas por um componente <SeoHead>, e o sitemap relaciona cada URL ao seu equivalente no outro idioma.
Trato o lado em inglês como canônico para a busca internacional. O espelho em português atende à parte do meu trabalho que acontece no Brasil: clientes, conteúdo para a comunidade e qualquer pessoa que prefira ler em português. Adicionar um novo estudo de caso é criar um único arquivo MDX em cada pasta de idioma, sem tocar em código.
Levando o site ao Google e um alerta de phishing inesperado
Ter um site rápido é metade do trabalho. A outra metade é fazer o Google saber que ele existe. A parte mecânica é pouco glamourosa, mas merece cuidado: criar uma propriedade do tipo Domínio no Search Console (verificada por um registro TXT no DNS, o que cobre todos os subdomínios de uma vez), enviar o sitemap e submeter as páginas principais pela Inspeção de URL para acelerar a indexação. O Bing leva poucos minutos, porque importa os dados direto do Search Console, e hoje o índice do Bing também alimenta as buscas com inteligência artificial.
Um detalhe que registro para quem passar pelo mesmo susto: o Search Console acusou “não foi possível buscar” o sitemap por vários dias. O arquivo estava correto, respondendo com status 200 e XML válido, inclusive para o robô do Google. É um comportamento conhecido. O índice é processado de imediato, mas os sitemaps internos entram em uma fila separada e exibem esse status até o robô realmente acessá-los. Em um domínio novo, isso pode levar dias. Não há o que corrigir, é só aguardar.
Em seguida veio a parte que eu não esperava. O Search Console acusou um problema de segurança, possível phishing, em um portfólio estático e sem nenhum formulário.
A explicação está na própria propriedade do tipo Domínio, que abrange todos os subdomínios. Eu mantenho uma instância do n8n, uma ferramenta de automação, hospedada por conta própria em um subdomínio. O robô de segurança do Google encontrou a tela de login dessa ferramenta, um formulário de um produto conhecido em um domínio pessoal que ele não reconhecia, e a classificou como possível roubo de credenciais. É um falso positivo comum para quem hospeda esse tipo de aplicação. O problema é que o alerta não faz essa distinção e pode exibir um aviso vermelho de “site enganoso” no domínio inteiro, inclusive no portfólio que envio a recrutadores.
A correção foi algo que, de qualquer forma, eu deveria ter feito desde o início: colocar o login da ferramenta atrás do Cloudflare Access, a camada Zero Trust disponível no plano gratuito. Agora qualquer acesso público ao subdomínio é interceptado pela tela de autenticação da própria Cloudflare, um padrão que o Google reconhece como legítimo, e o login nunca fica exposto. O único cuidado foi com os recursos que precisam continuar públicos, como os webhooks de entrada. Para eles, criei uma exceção específica e mantive todo o restante protegido.
O aprendizado mais valioso desta etapa não é técnico. Uma propriedade do tipo Domínio audita o seu domínio por completo, o que inclui projetos paralelos, ambientes de teste e ferramentas internas. Se você expõe serviços em subdomínios da sua marca pessoal, trate-os com o mesmo cuidado da página principal. O ideal é manter qualquer ferramenta com login em um domínio separado e protegida antes que um robô a encontre.
O que eu faria diferente
Se começasse de novo, provavelmente escolheria a mesma stack. Os pontos que eu ajustaria:
- O cache das imagens de Open Graph. Hoje elas são geradas a cada build pelo satori. Um controle baseado em hash do conteúdo reduziria o tempo de build na integração contínua.
- O Lenis em aparelhos mais lentos. Eu reduziria um pouco o coeficiente de inércia para Androids de entrada, já que o padrão fica ótimo em um MacBook com chip M1, mas um pouco pesado em um celular de dois anos atrás.
- A organização dos subdomínios desde o início. O alerta de phishing não teria acontecido se os projetos paralelos não dividissem o domínio da minha marca. Hoje, eu colocaria qualquer ferramenta com login em um domínio separado e protegida desde o primeiro dia.
No fim, o conteúdo é só metade da história. O código completo está no repositório, e dá para ver como cada decisão se sustenta navegando pelos estudos de caso, que é onde o raciocínio de engenharia aparece diante de restrições reais de produção.