19 KiB
Design: endurecimento técnico do portfólio
Status: aprovado para planejamento; implementação não iniciada Data: 2026-09-04
Objetivo
Reduzir fragilidade operacional, JavaScript global e código sem consumidores sem redesenhar o portfólio. O resultado mantém a identidade preto/papel/verde ácido, a tipografia e a narrativa visual, enquanto torna projetos, foco, movimento, dependências, analytics, deploy e validação previsíveis.
Estado de checkout que faz parte do alvo
A implementação futura trabalhará no checkout atual, sem worktree. Essa decisão é deliberada porque as alterações não commitadas abaixo são parte do estado-alvo e haverá um único editor; a pesquisa paralela é somente leitura:
data/project-health.jsonmodificado pelo usuário: não regenerar nem reverter nesta iniciativa.lib/portfolio-data.tsmodificado pelo usuário: evoluir a partir do conteúdo atual, preservando a atualização apenas em desenvolvimento.globals.cssna raiz, não rastreado: não editar, mover, importar ou apagar. O stylesheet ativo éapp/globals.css.lib/portfolio-data.load.test.tsnão rastreado: manter como baseline e atualizar suas expectativas somente quando o contrato de projetos mudar.
Não usar branch, commit, reset, checkout, stash ou comandos Git destrutivos. Rollback será por arquivo e por tarefa, usando os snapshots lidos antes de cada edição.
Escopo e arquitetura atual observada
- Next.js App Router com React 19 e TypeScript estrito.
app/page.tsxcarrega projetos no servidor porloadLiveFeaturedProjects()e repassa dados paraCinematicHome.filterLiveProjects()hoje transforma um snapshot ausente/expirado em lista vazia, ocultando todo projeto.app/layout.tsxenvolve todas as rotas em Lenis/GSAP, contexto de animação, transição de rota e overlays;PageIntrocria um portal modal que cobre a página por tempo fixo.- A home soma motion local, GSAP/ScrollTrigger redundante, cursores/overlays e loops contínuos de canvas.
GoogleAnalyticscarrega GA em produção sem consentimento e usa rewrites que camuflam script/coleta.next.config.tsmarca diretórios públicos com nomes estáveis comoimmutablee mantém CSP somente em relatório comunsafe-inlineeunsafe-eval.- Não existe workflow em
.github/workflows. pnpm-lock.yamlresolve Next16.2.9, contém overrides históricos e foi gerado com faixas, emborapackage.jsonnão fixe o conjunto de dependências.nixpacks.tomljá escolhe Node 22 e pnpm 9, mas instala com--no-frozen-lockfile.- Há componentes e testes sem consumidores de produção, incluindo dois caminhos de contato, biblioteca de demos/magicui e infraestrutura de animação não consumida.
Mapa de capacidades
| Módulo | Responsabilidade | Depende de |
|---|---|---|
| Projetos e saúde de demo | Sempre fornecer conteúdo; anexar estado observacional do link | JSON estático, snapshot opcional |
| Shell acessível | Entregar conteúdo/foco sem overlay bloqueante e sem scroll engine global | layout, CSS global |
| Movimento e performance | Preservar efeitos essenciais, remover duplicação e pausar trabalho invisível | hooks locais, Intersection Observer, Page Visibility |
| Dependências e deploy | Fixar runtime/dependências e produzir lock reproduzível | código morto já removido |
| Analytics e headers | Consentimento opt-in, revogação e carregamento direto de GA | layout, next/script, configuração de headers |
| CI | Instalação congelada e gates determinísticos | lock final e scripts do pacote |
| Documentação operacional | Descrever apenas comandos e comportamento realmente presentes | implementação concluída |
Direção de dependência: conteúdo estático → projeção com saúde → home; shell/CSS → componentes; código vivo → manifesto de dependências → lock/deploy/CI. Nenhum componente de UI consulta rede para decidir se conteúdo existe.
Decisões
1. Projeto é conteúdo; saúde é apenas estado do link
Substituir o contrato de “projetos vivos” por uma projeção que mantém todo projeto com featured !== false e anexa o estado da demo:
export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";
export type PortfolioProjectView = PortfolioProject & {
demoAvailability: DemoAvailability;
demoCheckedAt: string | null;
};
Regras:
featured === falsecontinua sendo a única regra de exclusão editorial.- Sem
demoUrl, o estado énonee não há CTA de demo. - Sem entrada de snapshot, o estado é
unknown; o card permanece e o URL configurado não é descartado. - Resposta final
200comContent-TypeHTML produzavailable. - Qualquer resultado explícito diferente produz
unavailable. unavailablerenderiza a evidência inteira e um controle não navegável com texto “Demo indisponível na última verificação”.unknownmantém o link e o anota como “Disponibilidade não verificada”.- Em desenvolvimento, snapshot ausente ou mais antigo que quinze minutos pode disparar uma atualização em memória; falha de rede retorna os projetos com o último estado conhecido/
unknown. - Em produção, render nunca chama demos externas. O snapshot versionado é observacional e conteúdo não depende de cron, validade temporal ou rede.
app/page.tsx deixa de usar ISR de cinco minutos para simular monitoramento. scripts/check-projects.mjs continua como comando manual de atualização atômica, não como requisito de disponibilidade do site.
2. Acessibilidade não depende de animação
- Remover
PageIntroe seu teste; não substituir por outro splash. - Manter o skip link e garantir que nenhum overlay o cubra.
- Colocar o reset de cor de links na camada base do Tailwind, permitindo que
text-white,text-slate-*e estados hover da 404 prevaleçam. - Em
prefers-reduced-motion: reduce, desativar smooth scroll e reduzir animações/transições CSS a uma atualização praticamente instantânea. - Fallbacks estáticos permanecem com o mesmo conteúdo e ações essenciais.
3. Um efeito por intenção, não camadas redundantes
A assinatura visual preservada é: paleta, fontes, hero “command center”, globo, starfield, notebook 3D quando elegível, cartões, divisores e tipografia de marquee.
Serão removidos:
- Lenis e ticker GSAP do layout global.
- Transição de rota global.
AnimationProvider, pois nenhum componente de produção consome seu contexto.ReelDirectoreCinematicSection, que repetem revelações já feitas por componentes locais.- Cursor customizado e seu
MutationObserver, que reaplica listeners sobre a árvore. - canvas de film grain; a textura passa a CSS estático.
- loops JS de marquee e divisores; tornam-se animações CSS/SVG sem estado React por frame.
GSAP/ScrollTrigger permanece somente na história do notebook, onde controla uma timeline scroll-driven real. Motion permanece nos componentes que já implementam feedback/reveal local. Nenhuma biblioteca nova será adicionada.
Loops restantes obedecem aos dois sinais:
const shouldRun = isIntersecting && !document.hidden && !shouldReduceMotion;
HeroStarfield já segue esse contrato e será preservado. NotebookStory combinará interseção e visibilitychange; o globo manterá flags separadas para interseção e visibilidade do documento para não reativar fora da viewport.
4. Hero terá somente a variante ativa
Hero mantém exclusivamente “command-center”. Remover pesos, seleção aleatória, cinco blocos editoriais não alcançáveis, layouts alternativos e branches de SideVisual. HeroScene perde o parâmetro variant, hoje ignorado. O conteúdo visível, CTAs, globo, telemetria, links sociais e fallback de reduced motion permanecem.
5. Remoção de código morto é um corte limpo
Excluir, após confirmar todos os callers na execução:
ContactForm,ContactInfoeusePortfolioData;Contacté o caminho ativo.CompanyLogose seus três arquivos de teste.- todos os demos na raiz de
components/ecomponents/magicui/, sem importadores. AnimationProvider, seu teste,app/lib/animations/config.ts,app/lib/animations/variants.tseapp/hooks/useScrollReveal.ts.FloatingElements,HomeButton,HorizontalScrolleCinematicText, sem importadores.- componentes de performance substituídos/removidos na decisão 3 e seus testes de implementação órfãos.
Não manter aliases, reexports ou shims.
6. Testes protegem comportamento observável
- Reescrever os testes de saúde para provar que todos os projetos editoriais permanecem e apenas o estado da demo muda.
- Preservar um teste de desenvolvimento que prova atualização em memória sem permitir fetch em produção.
- Cobrir o CTA disponível, indisponível e desconhecido de
Projectspor nome/estado acessível. - Remover PBTs de
CompanyLogosjunto do componente. - Remover PBTs do Footer que repetem um componente estático;
Footer.test.tsxjá cobre nomes acessíveis e segurança de links. - Substituir três PBTs de
Experiencepor testes determinísticos de filtro editorial e ordem cronológica; hierarquia de heading não precisa de geração aleatória. - Remover a asserção que fixa classes internas do Footer e a asserção da hero que inspeciona props serializadas de motion.
- Remover
fast-checkquando seu último consumidor desaparecer.
7. Runtime e dependências serão reproduzíveis
engines.node:22.x.packageManager:[email protected].- Next.js:
16.3.0, menor versão estável da mesma major que eliminou as três vulnerabilidades high mantidas por16.2.11e16.2.12. - Todas as dependências diretas e de desenvolvimento terão versão exata, sem
^,~ou comparadores. @types/nodeacompanha o runtime em22.20.1.@types/threepassa paradevDependencies.- Remover pacotes cujo último consumidor foi apagado:
@gsap/react,class-variance-authority,lenis,ogl,tailwindcss-animate,tw-animate-css,fast-check,@types/resize-observer-browsereresize-observer-polyfill. - Manter GSAP, cobe, Motion, React Three Fiber, Drei e Three porque ainda há consumidores ativos.
- Regenerar
pnpm-lock.yamlcom pnpm 9.15.9 e sem blocooverrides. - Corrigir advisories atualizando o pacote direto proprietário da cadeia; não usar
audit --force, exceções ou novos overrides. - Nixpacks usa
pnpm install --frozen-lockfile.
8. CI terá um caminho único
Criar .github/workflows/ci.yml para push e pull_request, em Ubuntu, Node 22 e pnpm 9.15.9. Ordem:
- checkout;
- instalação congelada com cache do pnpm;
pnpm lint;pnpm typecheck;pnpm test -- --runInBand;pnpm build;pnpm audit --prod --audit-level=high;pnpm audit --dev --audit-level=high.
Não adicionar Playwright, Cypress ou outro browser ao projeto. O smoke visual/interativo usa o Chromium disponível no harness durante a implementação; CI fica restrita aos gates aprovados.
9. Analytics exige opt-in e permite revogação
Aplicar consentimento a todos os visitantes, sem geolocalização de IP: é menor, previsível e não arrisca carregar GA antes de uma decisão quando a obrigação legal se aplica.
GoogleAnalyticscomeça sem<Script>e sem requisição a Google.- Persistir somente
"granted"ou"denied"emlocalStoragesob uma chave versionada. - Exibir ações “Aceitar analytics” e “Recusar” quando não há escolha.
- Manter um controle “Preferências de privacidade” acessível após a escolha.
- Somente
grantedmontahttps://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3Se a configuração de GA. - Consentimento de publicidade permanece negado; somente
analytics_storagepode ser concedido. - Revogação chama
gtag("consent", "update", ...)com estados negados, definewindow["ga-disable-G-QTT9QT0T3S"] = true, expira cookies_gae_ga_*visíveis no domínio atual e desmonta o loader. - Remover todos os rewrites de analytics/ad-block bypass e usar endpoints oficiais diretamente.
A ordem segue a orientação oficial do Google: default negado antes de coleta, update após escolha e persistência/revogação controladas pela aplicação.
10. CSP e cache mudam somente quando compatíveis
Não introduzir nonces nesta rodada. A documentação do Next informa que nonces exigem renderização dinâmica, desabilitam otimização estática/ISR e alteram cache/custo; isso conflita com a home estática e seria endurecimento desproporcional.
- Manter CSP em
Report-Only. - Adicionar
default-src 'self'eform-action 'self'. - Remover
unsafe-evalem produção; mantê-lo somente em desenvolvimento, onde o Next documenta a necessidade para debugging. - Manter
unsafe-inlinepor enquanto para compatibilidade observada com scripts/estilos do Next e JSON-LD inline. - Manter origens Google apenas porque GA opt-in ainda as usa.
- Remover regras
immutablepara/static,/fonts,/assetse/logos; esses nomes públicos não têm hash garantido. Assets gerados pelo Next em/_next/staticjá recebem cache apropriado do framework.
11. Documentação será derivada do estado final
- README troca badge Next 15 por 16.3.0, remove Lenis e bibliotecas apagadas, documenta Node 22/pnpm 9.15.9, scripts reais, arquitetura e validação.
docs/project-health-cron.mdserá substituído pordocs/project-health.md, descrevendo snapshot opcional, atualização manual/desenvolvimento e a garantia de cards sempre visíveis.- Deployment documentado no README usa somente
nixpacks.toml: Node 22, pnpm 9 e lock congelado. - Nenhum dado comercial, cliente, métrica, review, endereço ou claim novo será criado.
Não-objetivos
- Redesenhar hero, paleta, tipografia, navegação ou arquitetura de informação.
- Remover notebook 3D, React Three Fiber, Drei, Three, cobe, Motion ou GSAP usado pelo notebook.
- Criar backend, banco, CMP externo ou coleta analytics própria.
- Migrar para outro gerenciador de pacotes ou outra plataforma de deploy.
- Alterar conteúdo, metadata, JSON-LD, sitemap ou robots por pesquisa SEO/GEO externa. Essa integração terá spec e plano separados.
- Atualizar o snapshot do usuário em
data/project-health.json. - Importar ou consolidar o
globals.cssnão rastreado da raiz. - Endurecer CSP com nonce/hash sem uma prova separada de compatibilidade e impacto de renderização.
Riscos e mitigação
| Risco | Impacto | Mitigação |
|---|---|---|
| Saúde antiga desabilita uma demo já recuperada | Médio | Exibir timestamp/estado como última verificação e manter atualização manual/dev; nunca ocultar card |
| Alterar o tipo de projeto quebra notebook/home | Alto | Introduzir tipo por interseção compatível, migrar callers no mesmo checkpoint e rodar testes focados |
| Remoção de wrappers muda composição/hidratação | Alto | Remover um wrapper por corte, verificar foco, rotas e console no browser após o shell |
| Remoção de efeitos achata identidade | Médio | Preservar hero command center, globo, starfield, notebook, Motion local e CSS visual; comparar desktop/mobile |
| CSS reduced motion excessivamente amplo oculta conteúdo | Alto | Testar com emulação reduce; regras mudam duração, não display/opacity final |
| Revogação não interrompe GA já carregado | Alto | Atualizar consent mode para denied, ativar flag ga-disable, expirar cookies e testar ausência de novas requisições após revoke |
| CSP passa a bloquear runtime/GA | Alto | Permanecer Report-Only e remover apenas unsafe-eval em produção; observar console/network antes de enforcement futuro |
| Lock regenerado reintroduz advisory | Alto | Auditar prod/dev após frozen install e atualizar cadeia proprietária sem overrides/force |
| Exclusão alcança arquivo ainda importado | Alto | Buscar referências antes de cada lote e exigir tsc no checkpoint de corte |
| Alterações do usuário são sobrescritas | Alto | Tratar quatro arquivos listados como baseline; não tocar nos dois fora de escopo e usar edições ancoradas nos dois em escopo |
Rollback
Rollback é incremental, sem Git destrutivo:
- Antes de cada tarefa, ler e registrar os arquivos exatos daquele lote.
- Se o checkpoint falhar, interromper o lote seguinte.
- Restaurar somente os arquivos do lote com edições inversas baseadas no snapshot pré-tarefa.
- Para arquivos novos, removê-los somente se foram criados pelo lote e ainda não são consumidos.
- Para dependências, restaurar juntos
package.json,pnpm-lock.yaml,tailwind.config.jsenixpacks.toml; nunca restaurar apenas o lock. - Reexecutar o último checkpoint verde.
- Nunca incluir
data/project-health.json,globals.cssda raiz ou mudanças não relacionadas no rollback.
Critérios observáveis de aceite
- Home e
/casescontinuam exibindo cards independentemente de snapshot ausente, antigo ou com falha. - Demo explicitamente indisponível não navega e tem estado acessível; estado desconhecido não remove o URL nem o card.
- Produção não faz fetch de demos durante render; desenvolvimento pode atualizar em memória e falha de rede não esvazia conteúdo.
- Skip link recebe foco visível imediatamente, sem splash sobreposto.
- Links da 404 exibem as cores Tailwind pretendidas e foco visível.
- Reduced motion mostra todo conteúdo essencial sem scroll suave, loops ou transições longas.
- Loops restantes deixam de atualizar quando seção está fora da viewport ou
document.hiddené verdadeiro. - Hero mostra o mesmo conteúdo command-center sem infraestrutura de variantes mortas.
- Não há imports para os arquivos excluídos e TypeScript resolve o workspace sem erros.
package.jsoncontém versões exatas, Node22.x, pnpm9.15.9e Next16.3.0; lock não contémoverrides.- Instalação Nixpacks e local usa frozen lockfile com pnpm 9.
pnpm audit --prod --audit-level=highepnpm audit --dev --audit-level=highencerram com código zero, sem force, ignore ou override.- Antes de consentimento/rejeição não há request a
googletagmanager.com/google-analytics.comnem cookie_ga*criado pela página. - Aceite carrega GA diretamente; revogação nega consentimento, desabilita coleta futura e expira cookies acessíveis.
- Configuração não contém rewrites de analytics; CSP continua Report-Only compatível e cache immutable não cobre arquivos públicos sem hash.
- CI executa install congelado, Biome, tsc, Jest, build e auditorias prod/dev.
- README e documentação de project health descrevem comandos, versões e comportamento finais sem depender de cron.
globals.cssda raiz edata/project-health.jsonpermanecem inalterados.
Fontes técnicas
- Next.js, CSP: https://nextjs.org/docs/app/guides/content-security-policy
- Next.js,
next/script: https://nextjs.org/docs/app/api-reference/components/script - Google, consent mode: https://developers.google.com/tag-platform/security/guides/consent
- Google, API
gtag: https://developers.google.com/tag-platform/gtagjs/reference#consent - Nixpacks, provider Node: https://nixpacks.com/docs/providers/node
- pnpm, audit: https://pnpm.io/cli/audit
- pnpm Action para pnpm 10 e anteriores: https://github.com/pnpm/action-setup