feat: harden portfolio and add SEO foundations

This commit is contained in:
Evandro Fonseca Junior committed 2026-09-04 19:25:30 -03:00
1 parent b079afdd73
commit 62bcfbd8fa
121 files changed
+7769 -10469

No files matched your search

@@ -0,0 +1,903 @@
# Portfolio Hardening Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current checkout. Do not dispatch writing agents: one editor owns the dirty working tree. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Tornar conteúdo, acessibilidade, movimento, dependências, analytics, deploy e validação do portfólio previsíveis, com menos JavaScript e sem alterar sua identidade visual.
**Architecture:** Conteúdo de projetos continua estático e recebe apenas uma projeção observacional de saúde de demo; falha de monitoramento nunca remove card. O shell deixa de instalar scroll/animação global, mantendo Motion local e GSAP somente na história do notebook. Dependências exatas, lock congelado, consentimento opt-in e CI formam o limite operacional.
**Tech Stack:** Next.js 16.3.0 App Router, React 19.1.0, TypeScript 5.9.2, Tailwind CSS 4.1.12, Motion 12.23.12, GSAP 3.13.0, React Three Fiber 9.5.0, Jest 30.3.0, Biome 2.2.0, Node 22 e pnpm 9.15.9.
**Spec:** `docs/superpowers/specs/2026-09-04-portfolio-hardening-design.md`
## Global Constraints
- Trabalhar no checkout atual; não criar worktree porque mudanças não commitadas são parte do estado-alvo e haverá um único editor.
- Não executar branch, commit, reset, checkout, stash nem comando Git destrutivo.
- Preservar `data/project-health.json` e o `globals.css` não rastreado da raiz sem qualquer edição.
- Evoluir, sem restaurar, o conteúdo atual de `lib/portfolio-data.ts` e `lib/portfolio-data.load.test.ts`.
- Editar somente `app/globals.css`; nunca importar, mover ou apagar o `globals.css` da raiz.
- Manter preto/papel/verde ácido, fontes, hero command-center, globo, starfield, notebook, cartões e conteúdo factual existente.
- Não criar dados, métricas, clientes, reviews, endereços, preços ou claims.
- Não alterar metadata, JSON-LD, sitemap, robots ou conteúdo por SEO/GEO externo; essa integração terá plano separado.
- Não adicionar dependência; remover apenas as dependências que ficarem sem consumidores.
- Não usar suppressions do Biome, audit force, ignore de advisory, override transitivo, alias ou shim.
- Não adicionar Playwright/Cypress; o smoke browser usa o Chromium do harness.
- Antes de editar símbolo exportado, consultar referências via LSP; antes de excluir arquivo, repetir busca de importadores.
- Cada checkpoint deve ficar verde antes do grupo seguinte.
## File Map
### Conteúdo e saúde de demo
- Modify: `lib/project-health.ts` — classificar a última observação sem filtrar conteúdo.
- Modify: `lib/project-health.test.ts` — contrato de disponibilidade.
- Modify: `lib/portfolio-data.ts` — projetar todo featured com estado de demo e limitar refresh a desenvolvimento.
- Modify: `lib/portfolio-data.test.ts` — cards persistem em estados de saúde distintos.
- Modify: `lib/portfolio-data.load.test.ts` — produção sem rede; desenvolvimento atualiza sem esvaziar.
- Modify: `app/page.tsx` — usar o novo loader e remover ISR de monitoramento.
- Modify: `app/components/Projects.tsx` — CTA disponível, desconhecido e indisponível.
- Create: `app/components/Projects.test.tsx` — comportamento acessível do CTA.
### Shell, movimento e performance
- Modify: `app/layout.tsx` — remover wrappers globais; manter metadata/JSON-LD intactos.
- Modify: `app/components/cinematic/CinematicOverlays.tsx` — manter somente overlays leves da home.
- Delete: `app/components/cinematic/PageIntro.tsx`, `app/components/cinematic/PageIntro.test.tsx`.
- Delete: `app/providers/SmoothScrollProvider.tsx`.
- Delete: `app/providers/AnimationProvider.tsx`, `app/providers/AnimationProvider.test.tsx`.
- Delete: `app/lib/animations/config.ts`, `app/lib/animations/variants.ts`.
- Delete: `app/components/RouteTransition.tsx`.
- Modify: `app/globals.css` — camada base de links, reduced motion, grain/marquee/divisor CSS e tokens consolidados.
- Modify: `app/components/cinematic/CinematicHome.tsx` — retirar diretores/wrappers redundantes.
- Modify: `app/components/cinematic/CinematicHome.test.tsx` — testar ordem de conteúdo, não mocks removidos.
- Delete: `app/components/cinematic/CinematicSection.tsx`.
- Delete: `app/components/story/ReelDirector.tsx`.
- Delete: `app/hooks/useScrollReveal.ts`.
- Rewrite: `app/components/cinematic/FilmGrain.tsx` — elemento decorativo sem canvas/RAF.
- Rewrite: `app/components/cinematic/ScrollVelocityText.tsx` — marquee CSS sem hooks por frame.
- Rewrite: `app/components/cinematic/SectionDivider.tsx` — DOM/SVG estático e CSS.
- Delete: `app/components/cinematic/CustomCursor.tsx`.
- Modify: `app/components/cinematic/ControlRoomGlobe.tsx` — interseção e visibilidade separadas.
- Modify: `app/components/story/NotebookStory.tsx` — stage pausado em aba oculta.
- Modify: `app/components/story/NotebookStory.test.tsx` — transição de visibilidade.
- Modify: `app/components/Hero.tsx` — única variante ativa.
- Modify: `app/components/Hero.test.tsx`, `app/components/Hero.reducedMotion.test.tsx` — somente comportamento observável.
- Modify: `app/components/cinematic/HeroScene.tsx` — remover variante ignorada.
### Código morto e testes sem valor
- Delete: `app/components/ContactForm.tsx`, `app/components/ContactInfo.tsx`, `lib/usePortfolioData.ts`.
- Delete: `app/components/CompanyLogos.tsx` e seus três testes.
- Delete: `components/DotGrid.tsx`, `components/TextType.tsx`, `components/FaultyTerminal.tsx`, `components/Squares.tsx`, `components/LogoLoop.tsx`, `components/RotatingText.tsx`.
- Delete: todos os nove arquivos em `components/magicui/` e `components.json`.
- Delete: `app/components/cinematic/FloatingElements.tsx`, `HomeButton.tsx`, `HorizontalScroll.tsx`, `CinematicText.tsx`.
- Delete: os três PBTs do Footer e os três PBTs de Experience.
- Create: `app/components/Experience.test.tsx` — filtro featured e ordem.
- Modify: `app/components/Footer.test.tsx` — remover asserção de classes internas.
### Dependências, segurança, CI e documentação
- Modify: `package.json`, `pnpm-lock.yaml`, `tailwind.config.js`, `nixpacks.toml`, `jest.setup.ts`.
- Create: `.github/workflows/ci.yml`.
- Modify: `app/components/GoogleAnalytics.tsx`.
- Create: `app/components/GoogleAnalytics.test.tsx`.
- Modify: `next.config.ts`.
- Modify: `README.md`.
- Move/replace: `docs/project-health-cron.md` → `docs/project-health.md`.
## Sequência e dependências
```text
1 saúde pura
└─ 2 loader
└─ 3 apresentação de projetos
4 shell/foco
└─ 5 home sem GSAP redundante
└─ 6 efeitos sem loops
└─ 7 pause de renderers
8 hero única
9 código morto e testes
└─ 10 dependências/lock/deploy
└─ 11 analytics/headers
└─ 12 CI/docs/verificação final
```
## Tabela de conflitos de arquivos e interfaces
| Tarefas | Arquivo/interface compartilhada | Tipo de conflito | Regra de serialização |
| --- | --- | --- | --- |
| 1, 2 | `ProjectHealthSnapshot`, classificação de demo | Tipo e semântica | Concluir 1 antes de alterar loader |
| 2, 3 | `PortfolioProjectView`, `loadFeaturedProjects()` | Producer/consumer | Fixar assinatura em 2; migrar UI em 3 |
| 3, 5 | props `projects` de `CinematicHome`/`NotebookStory` | Tipo por interseção | Provar compatibilidade em 3 antes de simplificar home |
| 4, 6, 11 | `app/layout.tsx`, `CinematicOverlays` | Shell e ordem de scripts | Executar 4 → 6 → 11; reler antes de cada edição |
| 4, 6 | `app/globals.css` | Base, reduced motion e efeitos | Aplicar base/reduced em 4; efeitos/tokens em 6 |
| 5, 6 | `CinematicHome.tsx` | Composição e componentes substituídos | Remover wrappers em 5; trocar efeitos em 6 |
| 6, 7 | política de loops/visibilidade | Contrato de execução | Eliminar loops evitáveis antes de instrumentar os restantes |
| 4, 8 | `Hero` sob shell sem wrappers | Hidratação e teste visual | Estabilizar shell antes de podar hero |
| 5, 9, 10 | consumidores de GSAP/dependências | Grafo de imports | Apagar consumidores em 5/9; remover pacotes somente em 10 |
| 9, 10 | `fast-check`, CVA, OGL, Lenis e plugins Tailwind | Último consumidor | Confirmar zero importadores em 9; podar manifesto em 10 |
| 10, 11 | `package.json`, lock, `next.config.ts` | Next patch e build | Congelar toolchain antes de validar analytics/headers |
| 4, 11 | `app/layout.tsx` e `GoogleAnalytics` | Montagem global | Layout limpo em 4; consentimento em 11 sem reintroduzir wrapper |
| 10, 12 | scripts, lock e CI | Comandos contratuais | Workflow somente após comandos locais verdes |
| 2, 12 | docs de project health | Semântica pública | Documentar apenas o comportamento já comprovado |
---
### Task 1: Trocar filtro de projetos por classificação de demo
**Files:**
- Modify: `lib/project-health.ts`
- Modify: `lib/project-health.test.ts`
**Interfaces:**
- Consumes: `ProjectHealthCheck | undefined`.
- Produces:
```ts
export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";
export function getDemoAvailability(
demoUrl: string | null | undefined,
check: ProjectHealthCheck | undefined,
): DemoAvailability;
```
- [ ] **Step 1: Substituir testes de filtragem por testes de classificação**
Criar casos explícitos no teste existente:
```ts
expect(getDemoAvailability(null, undefined)).toBe("none");
expect(getDemoAvailability("https://demo.example", undefined)).toBe("unknown");
expect(getDemoAvailability("https://demo.example", snapshot.projects.online)).toBe("available");
expect(getDemoAvailability("https://demo.example", snapshot.projects.redirect)).toBe("unavailable");
expect(getDemoAvailability("https://demo.example", snapshot.projects.stale)).toBe("available");
```
A última expectativa registra que expiração governa refresh em desenvolvimento, não a existência/estado da última observação em produção.
- [ ] **Step 2: Executar o teste vermelho**
Run:
```bash
pnpm exec jest lib/project-health.test.ts --runInBand
```
Expected: falha porque `getDemoAvailability` ainda não existe ou o filtro antigo retorna lista vazia.
- [ ] **Step 3: Implementar a função pura e remover `filterLiveProjects`/`isCurrentLiveCheck`**
Implementação mínima:
```ts
export function getDemoAvailability(
demoUrl: string | null | undefined,
check: ProjectHealthCheck | undefined,
): DemoAvailability {
if (!demoUrl) return "none";
if (!check) return "unknown";
return check.status === 200 &&
check.contentType?.toLowerCase().startsWith("text/html")
? "available"
: "unavailable";
}
```
Manter `PROJECT_HEALTH_MAX_AGE_MS` e `createProjectHealthSnapshot`; expor um helper de frescor somente para o loader de desenvolvimento.
- [ ] **Step 4: Confirmar o contrato verde**
Run o mesmo Jest. Expected: `PASS`; nenhuma chamada de rede neste teste.
---
### Task 2: Fazer o loader sempre retornar projetos editoriais
**Files:**
- Modify: `lib/portfolio-data.ts`
- Modify: `lib/portfolio-data.test.ts`
- Modify: `lib/portfolio-data.load.test.ts`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `getDemoAvailability`, `ProjectHealthSnapshot`, `PROJECT_HEALTH_MAX_AGE_MS`.
- Produces:
```ts
export type PortfolioProjectView = PortfolioProject & {
demoAvailability: DemoAvailability;
demoCheckedAt: string | null;
};
export function getFeaturedProjects(
snapshot: ProjectHealthSnapshot | null,
): PortfolioProjectView[];
export async function loadFeaturedProjects(): Promise<PortfolioProjectView[]>;
```
- [ ] **Step 1: Atualizar o teste puro**
Fixture deve provar três invariantes em uma única lista: featured permanece, `featured: false` sai, e estado de demo não filtra card.
```ts
const result = getFeaturedProjects(snapshot);
expect(result.map(({ id }) => id)).toEqual(
portfolio.projects.filter(({ featured }) => featured !== false).map(({ id }) => id),
);
expect(result.find(({ id }) => id === "sev-modaintima")?.demoAvailability).toBe("available");
expect(result.find(({ id }) => id === "twins-lab")?.demoAvailability).toBe("unknown");
```
- [ ] **Step 2: Atualizar o teste do loader sem apagar o baseline do usuário**
Produção:
```ts
await expect(loadFeaturedProjects()).resolves.toEqual(
expect.arrayContaining([expect.objectContaining({ id: "sev-modaintima" })]),
);
expect(createProjectHealthSnapshot).not.toHaveBeenCalled();
```
Desenvolvimento com snapshot antigo:
```ts
expect(createProjectHealthSnapshot).toHaveBeenCalledTimes(1);
expect(result).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "sev-modaintima", demoAvailability: "available" }),
]));
```
Adicionar falha de refresh: a promise rejeitada não rejeita o loader nem esvazia a lista.
- [ ] **Step 3: Rodar ambos os testes e observar a quebra do contrato antigo**
```bash
pnpm exec jest lib/project-health.test.ts lib/portfolio-data.test.ts lib/portfolio-data.load.test.ts --runInBand
```
Expected: falha nas expectativas “sempre retorna featured”.
- [ ] **Step 4: Implementar projeção e loader**
`getFeaturedProjects` faz um único `filter(featured !== false)` e um `map`; `loadFeaturedProjects` lê snapshot uma vez. Em produção retorna a projeção imediatamente. Em desenvolvimento, atualiza somente se snapshot ausente/antigo, envolve o refresh em `try/catch` e usa snapshot anterior em falha.
- [ ] **Step 5: Migrar a home**
Em `app/page.tsx`:
```tsx
import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data";
export default async function Home() {
return (
<CinematicHome
projects={await loadFeaturedProjects()}
experiences={portfolio.experiences}
skillCategories={portfolio.skills.technical}
/>
);
}
```
Remover `revalidate = 300`; não adicionar outro cron/cache para saúde.
- [ ] **Step 6: Checkpoint do loader**
Run o comando do passo 3. Expected: todos passam; mock de fetch não é chamado em produção; rejeição dev ainda retorna cards.
---
### Task 3: Apresentar disponibilidade sem remover evidência
**Files:**
- Modify: `app/components/Projects.tsx`
- Create: `app/components/Projects.test.tsx`
- Modify: `app/components/story/notebookProjects.ts`
- Modify: `app/components/story/notebookProjects.test.ts`
**Interfaces:**
- Consumes: `PortfolioProjectView[]`.
- Produces: card sempre visível e CTA de demo acessível por estado.
- [ ] **Step 1: Escrever testes de UI por papel e nome acessível**
Cobrir três cards:
```tsx
expect(screen.getByRole("heading", { name: "Demo disponível" })).toBeVisible();
expect(screen.getByRole("link", { name: "Abrir projeto" })).toHaveAttribute(
"href",
"https://available.example",
);
expect(screen.getByText("Demo indisponível na última verificação")).toHaveAttribute(
"aria-disabled",
"true",
);
expect(screen.getByRole("link", { name: /Abrir projeto.*disponibilidade não verificada/i }))
.toHaveAttribute("href", "https://unknown.example");
```
O teste não inspeciona classes, props de motion ou contagem de wrappers.
- [ ] **Step 2: Rodar teste vermelho**
```bash
pnpm exec jest app/components/Projects.test.tsx --runInBand
```
Expected: falha porque `Projects` ainda trata qualquer `demoUrl` como navegável e não anota estado.
- [ ] **Step 3: Implementar CTA por estado**
- `available`: link atual.
- `unavailable`: `span` visualmente equivalente, `aria-disabled="true"`, sem `href`, `target` ou click.
- `unknown`: link configurado, com texto auxiliar acessível “Disponibilidade não verificada”.
- `none`: sem CTA.
Não esconder `article`, título, descrição, stack, GitHub ou status.
- [ ] **Step 4: Adaptar o seletor do notebook sem torná-lo dependente de saúde**
`selectNotebookProjects` continua usando screenshot local + `demoUrl`; aceita `PortfolioProjectView[]` estruturalmente e não filtra por `demoAvailability`.
- [ ] **Step 5: Checkpoint da apresentação**
```bash
pnpm exec jest app/components/Projects.test.tsx app/components/story/notebookProjects.test.ts lib/portfolio-data.test.ts --runInBand
```
Expected: todos passam e o fixture indisponível continua presente.
---
### Task 4: Remover bloqueadores globais e corrigir foco/reduced motion
**Files:**
- Modify: `app/layout.tsx`
- Modify: `app/components/cinematic/CinematicOverlays.tsx`
- Modify: `app/globals.css`
- Delete: `app/components/cinematic/PageIntro.tsx`
- Delete: `app/components/cinematic/PageIntro.test.tsx`
- Delete: `app/providers/SmoothScrollProvider.tsx`
- Delete: `app/providers/AnimationProvider.tsx`
- Delete: `app/providers/AnimationProvider.test.tsx`
- Delete: `app/lib/animations/config.ts`
- Delete: `app/lib/animations/variants.ts`
- Delete: `app/components/RouteTransition.tsx`
**Interfaces:**
- Produces: layout server-first sem splash, Lenis, contexto ou transição global.
- [ ] **Step 1: Repetir referências e remover `PageIntro` do overlay**
`CinematicOverlays` não importa nem renderiza o portal. Excluir componente e teste no mesmo passo.
- [ ] **Step 2: Achatar o body do layout**
Preservar JSON-LD, skip link, Header, overlays e analytics na ordem abaixo:
```tsx
<body>
<script type="application/ld+json">{safeJsonLd(jsonLd)}</script>
<a className="skip-link" href="#main-content">Pular para o conteúdo</a>
<Header />
<CinematicOverlays />
<div id="main-content" tabIndex={-1}>{children}</div>
<GoogleAnalytics />
</body>
```
Não tocar no objeto `metadata` nem no grafo JSON-LD.
- [ ] **Step 3: Excluir infraestrutura sem consumidores**
Apagar os providers, config/variants e `RouteTransition`; repetir busca pelos nomes e exigir zero resultado em código vivo.
- [ ] **Step 4: Corrigir cascata de links**
Mover `a { color: inherit; }` para dentro de `@layer base`, junto do reset, para as utilities da 404 prevalecerem.
- [ ] **Step 5: Adicionar fallback reduced motion global**
```css
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
```
Não usar `display: none` para conteúdo.
- [ ] **Step 6: Checkpoint de shell no browser**
Iniciar a app pelo processo supervisionado, abrir `/`, focar o primeiro controle com `Tab` e confirmar que o skip link aparece imediatamente e move foco para `#main-content`. Abrir uma rota inexistente e confirmar visualmente: CTA primário branco sobre azul; CTA secundário slate e hover branco. Emular reduced motion e confirmar conteúdo completo sem splash/smooth scroll. Console sem erro de provider/hidratação.
---
### Task 5: Retirar GSAP redundante da home
**Files:**
- Modify: `app/components/cinematic/CinematicHome.tsx`
- Modify: `app/components/cinematic/CinematicHome.test.tsx`
- Delete: `app/components/cinematic/CinematicSection.tsx`
- Delete: `app/components/story/ReelDirector.tsx`
- Delete: `app/hooks/useScrollReveal.ts`
**Interfaces:**
- Produces: mesma ordem sem wrapper de revelação ou scanner global.
- [ ] **Step 1: Atualizar teste de integração**
Manter somente o contrato útil: exatamente uma história de notebook e `About` depois dela. Remover mocks/asserções de `ReelDirector` e `CinematicSection`.
- [ ] **Step 2: Simplificar composição**
Remover `ReelDirector` e renderizar `About`, `Skills`, `Experience`, `Projects`, `LighthouseMetrics`, `WorkModalities` e `Contact` diretamente. Os próprios componentes mantêm Motion/reduced motion.
- [ ] **Step 3: Excluir os três módulos sem consumidores**
Apagar `CinematicSection`, `ReelDirector` e `useScrollReveal`. GSAP deve permanecer importado somente por `NotebookStory` e nenhum caminho morto.
- [ ] **Step 4: Checkpoint de ordem**
```bash
pnpm exec jest app/components/cinematic/CinematicHome.test.tsx app/components/story/NotebookStory.test.tsx --runInBand
```
Expected: `PASS`; home mantém hero → notebook → about → demais seções.
---
### Task 6: Trocar efeitos contínuos evitáveis por CSS/SVG
**Files:**
- Rewrite: `app/components/cinematic/FilmGrain.tsx`
- Rewrite: `app/components/cinematic/ScrollVelocityText.tsx`
- Rewrite: `app/components/cinematic/SectionDivider.tsx`
- Modify: `app/components/cinematic/CinematicOverlays.tsx`
- Modify: `app/components/cinematic/CinematicHome.tsx`
- Modify: `app/globals.css`
- Delete: `app/components/cinematic/CustomCursor.tsx`
**Interfaces:**
- `ScrollVelocityText` passa a consumir `direction?: "left" | "right"` no lugar de `baseVelocity`.
- `SectionDivider` mantém `variant` para não espalhar branches no caller.
- [ ] **Step 1: Remover cursor customizado**
Excluir import/render dinâmico e o arquivo. O cursor nativo volta a operar; retirar qualquer regra global `cursor: none` junto com o componente.
- [ ] **Step 2: Fazer grain sem cliente/RAF**
`FilmGrain` retorna somente:
```tsx
export default function FilmGrain() {
return <div aria-hidden="true" className="film-grain" />;
}
```
CSS usa textura estática/repeating gradients, `pointer-events: none`, posição fixa e a opacidade atual. Reduced motion/coarse pointer pode ocultar apenas esse ornamento.
- [ ] **Step 3: Fazer marquee em CSS**
Remover `useAnimationFrame`, `useScroll`, springs, resize state e Motion. Renderizar cópias suficientes em dois trilhos sem estado; `direction` seleciona `animate-marquee` ou `animate-marquee-reverse`. Migrar três callers de `baseVelocity` para direção explícita.
- [ ] **Step 4: Fazer divisor sem canvas**
- `gradient`: duas linhas CSS; animação existente.
- `pulse`: cinco pontos CSS.
- `waveform`: SVG estático com três paths, `aria-hidden="true"`.
Nenhum branch chama `requestAnimationFrame`.
- [ ] **Step 5: Consolidar tokens tocados**
Usar `--slash: #ccff00`, `--ink` e `--paper` como fonte única nas novas regras; remover apenas keyframes/classes que ficaram sem consumidores. Não reformatar o stylesheet nem copiar o arquivo da raiz.
- [ ] **Step 6: Checkpoint visual/performance**
No browser, verificar desktop e mobile: marquee legível, divisores presentes, grain sutil, cursor nativo, nenhum layout shift. Com Performance/console, confirmar ausência de RAF originado em `FilmGrain`, `ScrollVelocityText` e `SectionDivider`.
---
### Task 7: Pausar renderers restantes fora de uso
**Files:**
- Modify: `app/components/story/NotebookStory.tsx`
- Modify: `app/components/story/NotebookStory.test.tsx`
- Modify: `app/components/cinematic/ControlRoomGlobe.tsx`
**Interfaces:**
- Produces: `isVisible = sectionIntersects && !document.hidden` para o Canvas do notebook.
- Globo mantém `intersectsRef` separado de `document.hidden`.
- [ ] **Step 1: Escrever teste de Page Visibility para notebook**
Capturar a prop `isVisible` do mock dinâmico. Após `Object.defineProperty(document, "hidden", { value: true, configurable: true })` e `visibilitychange`, esperar `false`; ao voltar a `false`, o valor depende novamente da interseção.
- [ ] **Step 2: Implementar estado combinado no notebook**
Não duplicar timeline. Adicionar listener `visibilitychange`, limpar no unmount e passar a conjunção ao `NotebookStage3D`.
- [ ] **Step 3: Corrigir o globo**
O callback do Intersection Observer altera apenas `intersectsRef`. O `onRender` retorna cedo quando `!intersectsRef.current || document.hidden`; `visibilitychange` não sobrescreve estado de interseção.
- [ ] **Step 4: Checkpoint de pausa**
```bash
pnpm exec jest app/components/story/NotebookStory.test.tsx app/components/story/NotebookStage3D.test.tsx --runInBand
```
No browser, rolar notebook/globo para fora, ocultar/mostrar aba e verificar que não há salto nem erro WebGL ao retornar.
---
### Task 8: Reduzir Hero à variante ativa
**Files:**
- Modify: `app/components/Hero.tsx`
- Modify: `app/components/Hero.test.tsx`
- Modify: `app/components/Hero.reducedMotion.test.tsx`
- Modify: `app/components/cinematic/HeroScene.tsx`
**Interfaces:**
- Produces: Hero sem prop/seleção de variante; conteúdo command-center é constante.
- [ ] **Step 1: Ajustar testes para o contrato visível**
Remover spy de `Math.random`, mock/asserção de `variant` e inspeção `data-initial`/`data-animate`. Manter heading, CTA WhatsApp, link de cases, CV, links sociais e ausência de 3D em reduced motion.
- [ ] **Step 2: Podar o modelo de variantes**
Trocar `HERO_VARIANTS[0]` por `HERO_CONTENT` sem `weight`, `scene` e `layout`. Remover cinco objetos inalcançáveis, `LAYOUT_CLASSES`, layouts alternativos e todos os branches de `SideVisual`; manter diretamente starfield, globo e telemetria.
- [ ] **Step 3: Simplificar `HeroScene`**
Remover união `HeroSceneVariant`, prop `variant` e `void variant`; caller passa somente `isVisible`.
- [ ] **Step 4: Checkpoint da hero**
```bash
pnpm exec jest app/components/Hero.test.tsx app/components/Hero.reducedMotion.test.tsx --runInBand
```
Expected: conteúdo e ações passam; nenhum teste depende de branch morto.
---
### Task 9: Excluir código morto e podar testes de implementação
**Files:**
- Delete: grupos listados em “Código morto e testes sem valor”.
- Create: `app/components/Experience.test.tsx`.
- Modify: `app/components/Footer.test.tsx`.
**Interfaces:**
- Consumes: `Contact` continua como formulário ativo; `Footer` e `Experience` mantêm APIs atuais.
- Produces: zero importadores órfãos e testes comportamentais pequenos.
- [ ] **Step 1: Excluir caminho antigo de contato**
Confirmar que somente os próprios arquivos referenciam `ContactForm`, `ContactInfo` e `usePortfolioData`; apagar os três. Não alterar `Contact.tsx`.
- [ ] **Step 2: Excluir CompanyLogos e testes**
Apagar componente, `CompanyLogos.test.tsx`, `CompanyLogos.altText.pbt.test.tsx` e `CompanyLogos.reducedMotion.pbt.test.tsx`. Não apagar logos genéricos usados por `Logo`.
- [ ] **Step 3: Excluir biblioteca demo/magicui**
Apagar os seis componentes da raiz de `components/`, os nove de `components/magicui/` e `components.json`. Confirmar que `@/lib/utils` continua consumido por `ControlRoomGlobe` e `TelemetryStrip`.
- [ ] **Step 4: Excluir cinemáticos sem importadores**
Apagar `FloatingElements`, `HomeButton`, `HorizontalScroll` e `CinematicText`.
- [ ] **Step 5: Substituir PBTs por dois testes de Experience**
Excluir os três `Experience.*.pbt.test.tsx`. Novo teste usa fixture fixa e verifica:
```ts
expect(screen.queryByRole("heading", { name: "Oculta" })).not.toBeInTheDocument();
expect(screen.getAllByRole("article").map((article) =>
within(article).getByRole("heading", { level: 3 }).textContent,
)).toEqual(["Mais recente", "Mais antiga"]);
```
- [ ] **Step 6: Remover PBTs redundantes do Footer**
Excluir os três arquivos `Footer.*.pbt.test.tsx`. Em `Footer.test.tsx`, manter contato, navegação, nomes acessíveis, `target`/`rel` e ano; remover somente teste que fixa `.ds-grid-2`, quantidade de cards e classes.
- [ ] **Step 7: Checkpoint de corte**
```bash
pnpm exec tsc --noEmit
pnpm exec jest app/components/Experience.test.tsx app/components/Footer.test.tsx --runInBand
```
Expected: TypeScript sem import quebrado; testes comportamentais passam.
---
### Task 10: Fixar dependências, runtime e deploy
**Files:**
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`
- Modify: `tailwind.config.js`
- Modify: `nixpacks.toml`
- Modify: `jest.setup.ts`
**Interfaces:**
- Produces: Node `22.x`, pnpm `9.15.9`, Next `16.3.0`, scripts `typecheck` e versões exatas.
- [ ] **Step 1: Fixar manifesto**
Adicionar:
```json
"engines": { "node": "22.x" },
"packageManager": "[email protected]"
```
Adicionar script:
```json
"typecheck": "tsc --noEmit"
```
Fixar às versões resolvidas do spec; mover `@types/three` para dev e `@types/node` para `22.20.1`.
- [ ] **Step 2: Remover dependências sem consumidores**
Remover do manifesto: `@gsap/react`, `class-variance-authority`, `lenis`, `ogl`, `tailwindcss-animate`, `tw-animate-css`, `fast-check`, `@types/resize-observer-browser`, `resize-observer-polyfill`.
Manter `@react-three/drei`, `@react-three/fiber`, `three`, `cobe`, `gsap`, `motion`, `clsx`, `tailwind-merge`, Jest e Testing Library.
- [ ] **Step 3: Retirar plugin e mock órfãos**
Em `tailwind.config.js`, deixar `plugins: []`. Em `jest.setup.ts`, remover import de `./__mocks__/resizeObserver` somente se o mock nativo definido no arquivo cobre todos os testes; apagar `__mocks__/resizeObserver.js` se ficar sem importador.
- [ ] **Step 4: Regenerar lock com a versão fixada**
Ativar pnpm 9.15.9 via Corepack e executar:
```bash
corepack prepare [email protected] --activate
pnpm install --lockfile-only
pnpm install --frozen-lockfile
```
Expected: `pnpm-lock.yaml` sem seção `overrides`; Next resolve `16.3.0`; manifesto e lock concordam.
- [ ] **Step 5: Corrigir cadeias auditadas sem force/override**
Executar as auditorias separadas. Se uma cadeia direta ainda resolver versão vulnerável, atualizar o pacote direto que a introduz para a menor versão compatível corrigida, fixar essa versão e repetir os três comandos do passo 4.
```bash
pnpm audit --prod --audit-level=high
pnpm audit --dev --audit-level=high
```
Expected: ambos código zero. É proibido usar `--force`, ignore ou override para obter verde.
- [ ] **Step 6: Congelar Nixpacks**
Manter `nodejs_22` e `pnpm-9_x`; trocar somente install por:
```toml
[phases.install]
cmds = ["pnpm install --frozen-lockfile"]
```
- [ ] **Step 7: Checkpoint de toolchain**
```bash
pnpm install --frozen-lockfile
pnpm typecheck
pnpm lint
```
Expected: instalação não altera lock, TypeScript e Biome encerram em zero. Corrigir diagnósticos nos arquivos responsáveis; não alterar `biome.json` para desligar regras.
---
### Task 11: Implementar consentimento e corrigir headers/cache
**Files:**
- Modify: `app/components/GoogleAnalytics.tsx`
- Create: `app/components/GoogleAnalytics.test.tsx`
- Modify: `next.config.ts`
**Interfaces:**
- Storage key: `evandro.analytics-consent.v1`.
- Values persistidos: `granted | denied`.
- GA ID permanece `G-QTT9QT0T3S`.
- [ ] **Step 1: Escrever testes de consentimento**
Testes devem provar:
```ts
expect(screen.queryByTestId("google-analytics-script")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Aceitar analytics" })).toBeVisible();
await user.click(screen.getByRole("button", { name: "Aceitar analytics" }));
expect(localStorage.getItem("evandro.analytics-consent.v1")).toBe("granted");
expect(screen.getByTestId("google-analytics-script")).toHaveAttribute(
"src",
"https://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3S",
);
```
Adicionar recusa sem script e revogação que grava `denied`, define `ga-disable-G-QTT9QT0T3S` e expira cookies `_ga`/`_ga_*`.
- [ ] **Step 2: Rodar teste vermelho**
```bash
pnpm exec jest app/components/GoogleAnalytics.test.tsx --runInBand
```
Expected: falha porque loader atual monta GA sem decisão e não oferece controles.
- [ ] **Step 3: Implementar UI e loader no arquivo existente**
Inicializar estado após mount lendo somente a chave versionada. Sem decisão, renderizar um diálogo não modal acessível com descrição curta e aceitar/recusar. Após escolha, manter botão de preferências. Montar `next/script` direto apenas em `granted`; advertising consent fica sempre denied.
- [ ] **Step 4: Implementar revogação completa alcançável**
Ao revogar:
```ts
window["ga-disable-G-QTT9QT0T3S"] = true;
window.gtag?.("consent", "update", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
});
```
Expirar somente cookies cujos nomes começam com `_ga`, para path `/` e domínio atual quando aplicável. Não apagar storage/cookies alheios.
- [ ] **Step 5: Remover rewrites e cache incorreto**
Excluir `rewrites()` inteiro de `next.config.ts`. Remover headers customizados `immutable` de `/static`, `/fonts`, `/assets`, `/logos`.
- [ ] **Step 6: Ajustar CSP com risco controlado**
Manter `Content-Security-Policy-Report-Only`. Incluir `default-src 'self'` e `form-action 'self'`; construir `script-src` com `unsafe-eval` somente em desenvolvimento. Manter `unsafe-inline` e origens oficiais do Google. Não criar nonce/proxy nesta rodada.
- [ ] **Step 7: Checkpoint automatizado e browser**
```bash
pnpm exec jest app/components/GoogleAnalytics.test.tsx --runInBand
pnpm typecheck
```
No browser de produção local: limpar a chave/cookies; confirmar zero request Google antes de escolher; recusar e recarregar, ainda zero; aceitar e observar um único carregamento direto; revogar e confirmar flag/consent denied e cookies acessíveis removidos. Conferir console sem violações que quebrem a app.
---
### Task 12: Criar CI, atualizar documentação e fechar a iniciativa
**Files:**
- Create: `.github/workflows/ci.yml`
- Modify: `README.md`
- Move/replace: `docs/project-health-cron.md` → `docs/project-health.md`
**Interfaces:**
- CI usa scripts reais de `package.json` e lock final.
- Docs descrevem somente o comportamento verificado nas tarefas 1–11.
- [ ] **Step 1: Criar workflow único**
Conteúdo operacional:
```yaml
name: CI
on:
push:
pull_request:
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test -- --runInBand
- run: pnpm build
- run: pnpm audit --prod --audit-level=high
- run: pnpm audit --dev --audit-level=high
```
`pnpm/action-setup` lê `packageManager`; não duplicar versão no workflow.
- [ ] **Step 2: Reescrever README com fatos do repositório**
Atualizar badge/stack para Next 16.3.0; remover Lenis, demos apagados e PBTs; registrar Node 22, pnpm 9.15.9, quick start, todos os scripts existentes, App Router, conteúdo estático, notebook 3D progressivo, consentimento, CI e Nixpacks frozen. Não criar seção de resultados comerciais nem promessas de performance não medidas.
- [ ] **Step 3: Substituir documentação de cron**
Remover `docs/project-health-cron.md` e criar `docs/project-health.md` com:
- cards derivados de `featured`, nunca de saúde;
- estados `available`, `unavailable`, `unknown`, `none`;
- produção sem fetch externo durante render;
- atualização dev em memória quando snapshot está antigo;
- `pnpm check:projects` como atualização manual atômica opcional;
- `data/project-health.json` como última observação versionada, não requisito de conteúdo.
Não editar o snapshot ao documentá-lo.
- [ ] **Step 4: Rodar gate completo uma única vez**
```bash
pnpm install --frozen-lockfile
pnpm lint
pnpm typecheck
pnpm test -- --runInBand
pnpm build
pnpm audit --prod --audit-level=high
pnpm audit --dev --audit-level=high
```
Expected: todos encerram com código zero; install não modifica lock; build lista as rotas atuais.
- [ ] **Step 5: Smoke browser final**
Exercitar `/`, `/cases`, `/servicos`, `/curriculo` e uma 404 em desktop e viewport móvel. Confirmar projetos, foco, menu, notebook/fallback, formulário/WhatsApp, reduced motion, consentimento e ausência de erro de console. Não instalar runner de browser.
- [ ] **Step 6: Conferir arquivos protegidos e corte limpo**
Por leitura/comparação com snapshots iniciais, confirmar:
- `data/project-health.json` inalterado;
- `globals.css` da raiz inalterado;
- alterações em `lib/portfolio-data.ts` e seu teste preservam o refresh dev existente enquanto removem fail-closed;
- nenhum import aponta para arquivo excluído;
- nenhum artifact temporário, log, relatório ou cache foi criado no repo.
## Auto-revisão de cobertura do plano
| Requisito aprovado | Cobertura |
| --- | --- |
| Cards sempre visíveis; health não remove; dev atualiza; prod sem cron | Tasks 1–3 e 12 |
| Next 16.3.0, advisories, sem force/overrides, Node 22, pnpm 9, frozen lock | Task 10 |
| Remover PageIntro, corrigir 404, reduced motion, pausar loops | Tasks 4, 6 e 7 |
| Código morto, Hero única, PBTs, deps órfãs, Biome verde | Tasks 5, 8, 9 e 10 |
| Tirar Lenis/GSAP global, reduzir JS/long tasks, tokens/cache | Tasks 4–6, 10 e 11 |
| CI com install/lint/tsc/test/build/audit; sem browser pesado | Task 12 |
| Sem bypass de ad blocker; consentimento/revogação; CSP compatível | Task 11 |
| README/deploy/project-health grounded | Task 12 |
| Preservar identidade e não inventar claims | Restrições globais e smoke final |
| Não integrar SEO externo nesta rodada | Restrições globais e não-objetivo do spec |
| Preservar mudanças do usuário e não usar worktree/commits | Restrições globais, rollback do spec e Task 12 |
A auto-revisão confirmou que todas as interfaces, ordens, comandos, estados e critérios necessários estão definidos. Não há passo de commit. A execução termina somente após o gate completo e o smoke real.
@@ -0,0 +1,765 @@
# SEO/GEO Growth Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current checkout. SEO starts only after the portfolio-hardening plan is fully complete and green. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Entregar SEO/GEO factual por rota, com entidades corretas, prova técnica em três cases, navegação recíproca, consentimento documentado e operação mensurável, sem alterar a direção visual.
**Architecture:** `lib/site-content.ts` continua sendo a fonte editorial; três cases ganham afirmações com fontes versionadas e serviços ganham resposta direta. `lib/seo.ts` centraliza IDs e builders mínimos de JSON-LD, enquanto metadata e imagens Open Graph permanecem colocalizadas nas rotas do App Router. O consentimento final do hardening é documentado em `/privacidade`; ações de Search Console, Bing e Cloudflare ficam em um runbook, sem automação nem alegação de execução.
**Tech Stack:** Next.js 16.3.0 App Router, React 19.1.0, TypeScript 5.9.2, Tailwind CSS 4.1.12, Jest 30.3.0, Testing Library, Biome 2.2.0, Node 22 e pnpm 9.15.9; nenhuma dependência nova.
**Spec:** `docs/superpowers/specs/2026-09-04-seo-geo-growth-design.md`
## Global Constraints
- O gate final de `docs/superpowers/plans/2026-09-04-portfolio-hardening.md` deve estar concluído antes da Task 1.
- Trabalhar sobre as interfaces finais do hardening; reler cada arquivo compartilhado antes de editá-lo.
- Não criar branch, commit, stash, reset, checkout ou worktree.
- Não adicionar dependência, CMS, backend, formulário, tracker, cidade-clone ou camada de compatibilidade.
- Não criar `llms.txt`, `FAQPage`, meta `keywords`, `ProfessionalService` ou `LocalBusiness` não comprovado.
- Não publicar métrica, review, cliente, endereço, coordenada, preço, resultado comercial ou presença física não verificados.
- Manter hero command-center, globo, starfield, notebook, paleta preto/papel/verde ácido, tipografia, grid, cartões, componentes e tokens `ds-*`.
- CTAs de contratação usam `/curriculo`; CTAs de consultoria usam `/#contact`; nenhum modal/layout novo.
- Base regional autorizada: “Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.”
- `data/project-health.json` e o `globals.css` não rastreado da raiz permanecem intocados.
- Testes validam comportamento, relações e dados observáveis; não fixam classes, wrappers ou texto irrelevante.
- Ações externas permanecem desmarcadas e documentadas; nenhuma conta/painel é alterado.
## Gate predecessor — obrigatório, fora da contagem de tarefas
- [ ] Ler o relatório final do hardening e confirmar que Tasks 1–12 terminaram.
- [ ] Confirmar que o manifesto final contém Node 22, pnpm 9.15.9, Next 16.3.0 e `typecheck`.
- [ ] Confirmar o último gate verde registrado:
```bash
pnpm install --frozen-lockfile
pnpm lint
pnpm typecheck
pnpm test -- --runInBand
pnpm build
pnpm audit --prod --audit-level=high
pnpm audit --dev --audit-level=high
```
- [ ] Se qualquer prova estiver ausente ou vermelha, encerrar esta execução antes de editar produção. Corrigir o predecessor no plano de hardening, não neste plano.
## Mapa de arquivos
### Contratos e fatos
- Modify: `lib/site-content.ts` — resposta de serviços, intenção de CTA, detalhes/fontes de três cases e relação inversa.
- Create: `lib/site-content.test.ts` — integridade das fontes e relações.
- Modify: `public/portfolio-data.json` — remover afirmações divergentes dos três registros escolhidos.
- Modify: `lib/portfolio-data.test.ts` — preservar o contrato final de projeto/health e verificar apenas fatos públicos essenciais.
### Entidades e frescor
- Create: `lib/seo.ts` — IDs e builders JSON-LD mínimos.
- Create: `lib/seo.test.ts` — relações entre entidades, breadcrumbs e sitemap determinístico.
- Modify: `app/layout.tsx`, `app/curriculo/page.tsx`, `app/sitemap.ts`.
### Serviços e cases
- Modify: `app/servicos/page.tsx`, `app/servicos/[slug]/page.tsx`.
- Create: `app/servicos/[slug]/page.test.tsx`.
- Modify: `app/cases/page.tsx`, `app/cases/[slug]/page.tsx`.
- Create: `app/cases/[slug]/page.test.tsx`.
### Open Graph
- Create: `lib/og-card.tsx`.
- Modify: `app/opengraph-image.tsx`.
- Create: `app/servicos/opengraph-image.tsx`, `app/servicos/[slug]/opengraph-image.tsx`.
- Create: `app/cases/opengraph-image.tsx`, `app/cases/[slug]/opengraph-image.tsx`.
- Create: `app/curriculo/opengraph-image.tsx`, `app/privacidade/opengraph-image.tsx`.
### Privacidade e operação
- Create: `app/privacidade/page.tsx`, `app/privacidade/page.test.tsx`.
- Modify: `app/components/GoogleAnalytics.tsx`, `app/components/GoogleAnalytics.test.tsx`.
- Modify: `app/components/Footer.tsx`, `app/components/Footer.test.tsx`.
- Create: `docs/seo-geo-operations.md`.
## Dependências e paralelização
```text
Hardening completo
├─ Task 1: contratos/fatos
│ ├─ Task 3: serviços
│ └─ Task 4: cases
└─ Task 2: entidades/frescor
├─ Task 3: serviços
├─ Task 4: cases
└─ Task 7: privacidade
Task 3 + Task 4
└─ Task 5: OG dinâmico/template
├─ Task 6: OG de hubs/currículo
└─ Task 7: OG/privacidade
Tasks 1–7
└─ Task 8: runbook e gate final
```
Após Tasks 1 e 2, Tasks 3 e 4 são slices independentes e podem ser executadas em paralelo somente se editores distintos não tocarem arquivos compartilhados. Task 5 fecha o contrato visual de imagem; Tasks 6 e 7 podem seguir em paralelo. Task 8 é serial.
## Matriz de conflitos
| Tarefas | Arquivo/interface | Conflito | Serialização |
| --- | --- | --- | --- |
| Hardening 4, SEO 2 | `app/layout.tsx` | Shell versus metadata/JSON-LD | Hardening completo; SEO relê e preserva o body final. |
| Hardening 11, SEO 7 | `GoogleAnalytics` e teste | Consentimento producer/consumer | SEO consome chave/estados finais e só acrescenta link/cópia. |
| Hardening 2–3, SEO 1 | JSON de portfólio e tipos finais | Health versus fatos editoriais | Não alterar loader/tipos de health; mudar somente três registros. |
| 1, 3, 4 | `lib/site-content.ts` | Contrato compartilhado | Task 1 fixa tudo; Tasks 3–4 não editam o arquivo. |
| 2, 3, 4, 7 | `lib/seo.ts` | Builders consumidos por rotas | Task 2 é provider; demais somente importam. |
| 2, 7 | `app/sitemap.ts`, `lib/seo.test.ts` | Privacidade aumenta 16→17 URLs | Task 2 remove frescor; Task 7 relê, adiciona rota e atualiza a expectativa. |
| 3, 5 | Conteúdo de serviço usado por OG | Copy/slug | Serviço fecha antes da imagem dinâmica. |
| 4, 5 | Conteúdo de case usado por OG | Copy/slug | Case fecha antes da imagem dinâmica. |
| 5, 6, 7 | `lib/og-card.tsx` | Template de imagem | Task 5 possui o template; Tasks 6–7 só fornecem props. |
| Hardening 9, SEO 7 | `Footer.tsx` e teste | Podas de teste versus link novo | SEO edita somente após o teste final do Footer existir. |
| 7, 8 | Privacidade/medição | Fato implementado versus documentação | Runbook descreve apenas o comportamento comprovado na Task 7. |
---
### Task 1: Fechar contrato factual, fontes e relações
**Files:**
- Modify: `lib/site-content.ts`
- Create: `lib/site-content.test.ts`
- Modify: `public/portfolio-data.json`
- Modify: `lib/portfolio-data.test.ts`
**Interfaces:**
- Consumes: `PortfolioProjectView` e projeção de health finais, sem alterá-los.
- Produces: `intentCtas`, `CtaIntent`, `EvidenceSource`, `SourcedStatement`, `CaseStudyDetail`, `ServicePageContent.answer`, `ServicePageContent.ctaIntent`, `CaseStudyContent.detail`, `getRelatedServices(caseId)`.
- [ ] **Step 1: Confirmar referências antes de mudar exports**
Usar LSP em `ServicePageContent`, `CaseStudyContent`, `caseStudies`, `servicePages` e `getRelatedCases`. Registrar os callers; não renomear símbolos existentes.
- [ ] **Step 2: Escrever teste vermelho de integridade editorial**
Criar `lib/site-content.test.ts` com invariantes de consumidor:
O teste deve fechar invariantes estáveis, sem snapshot nem copy completa:
- o conjunto de cases com `detail`, ordenado apenas no teste, é exatamente `as-servicos`, `nutriscan` e `twins-lab`;
- IDs de `sources` são únicos dentro de cada case;
- toda `EvidenceSource.url` corresponde a `https://github.com/<owner>/<repo>/blob/<SHA de 40 hex>/<path>`;
- `answer`, `challenge`, cada item de `implementation` e os limites comparativos têm pelo menos um `sourceId`;
- o único limite sem fonte é o limite puramente editorial de AS Serviços, explicitamente `sourceIds: []`;
- todo ID presente resolve dentro de `detail.sources`;
- o boundary do NutriScan inclui `nutri-readme`;
- `getRelatedServices("twins-lab")` produz o conjunto exato `consultoria-nextjs`, `integracao-hardware-software-iot` e `arquitetura-de-software`, rejeitando extras;
- todos os serviços têm `answer`, `ctaIntent: "consulting"` e `ctaHref: "/#contact"`.
- [ ] **Step 3: Rodar o teste vermelho**
```bash
pnpm exec jest lib/site-content.test.ts --runInBand
```
Expected: falha por interfaces/campos/helper ainda ausentes.
- [ ] **Step 4: Implementar os tipos e os três detalhes aprovados**
Copiar os textos e URLs SHA-pinned exatamente da seção “Cases escolhidos e fonte de cada campo” do spec. IDs de fonte locais e estáveis:
```ts
"as-readme" | "as-package" | "as-engine" | "as-canvas"
"nutri-search" | "nutri-barcode" | "nutri-wrapper" | "nutri-package" | "nutri-prisma" | "nutri-readme"
"twins-package" | "twins-stream" | "twins-simulator" | "twins-readme"
```
Ruling de proveniência: não criar fonte fictícia ou circular para política editorial. O boundary puramente editorial de AS usa `sourceIds: []`; afirmações técnicas/factuais e boundaries comparativos exigem fonte externa, e qualquer ID presente precisa resolver.
`getRelatedServices` filtra `servicePages` e não cria mapa duplicado.
Para cada serviço, `answer` resume oferta, entregável e limite usando apenas conteúdo já presente; substituir enumeração de cidades pela frase regional autorizada onde houver. `ctaIntent` é sempre `consulting`; rótulos usam “Solicitar consultoria”, “Conversar sobre o projeto” ou “Solicitar diagnóstico” conforme o serviço, todos com `/#contact`.
- [ ] **Step 5: Corrigir os três registros no JSON sem tocar health**
Em AS Serviços, manter fatos confirmados pelo código. Em NutriScan, remover versão fixa, PWA, MongoDB/histórico, scanner, produção e resultados de saúde não cruzados; manter busca por nome/código, cache JSON e Open Food Facts. Em Twins Lab, trocar WebSockets por SSE, remover OpenRouter/sub-segundo/produção/análise preditiva como fato atual; manter autenticação, simulador, MongoDB e stream a cada dois segundos.
Não alterar `featured`, `demoUrl`, `githubUrl`, `id` nem campos `demoAvailability` produzidos pelo loader final.
- [ ] **Step 6: Ajustar o teste final de portfólio somente para o contrato factual**
Serializar com `JSON.stringify` o registro público inteiro de cada um dos três cases e rejeitar termos específicos já removidos em `subtitle`, `description`, `longDescription`, `technologies`, `features`, `status` e `badges`. Manter apenas asserts positivos curtos para `Open Food Facts`, `SSE` e `Three.js`; não usar snapshot, frase completa, classe ou métrica interna.
- [ ] **Step 7: Checkpoint da fonte editorial**
```bash
pnpm exec jest lib/site-content.test.ts lib/portfolio-data.test.ts --runInBand
pnpm exec biome check lib/site-content.ts lib/site-content.test.ts public/portfolio-data.json lib/portfolio-data.test.ts
pnpm typecheck
```
Expected: zero; o contrato de proveniência distingue fonte externa de limite editorial, os três cases têm IDs/URLs válidos e todos os serviços têm resposta/CTA; health final permanece verde.
---
### Task 2: Tornar entidades mínimas e frescor determinístico
**Files:**
- Create: `lib/seo.ts`
- Create: `lib/seo.test.ts`
- Modify: `app/layout.tsx`
- Modify: `app/curriculo/page.tsx`
- Modify: `app/sitemap.ts`
**Interfaces:**
- Produces: `SEO_IDS`, `buildWebSiteJsonLd()`, `buildPersonJsonLd()`, `buildProfilePageJsonLd()`, `buildServiceJsonLd(service)`, `buildBreadcrumbList(pageUrl, items)`.
- Does not own: visual body, consent, OG renderer ou conteúdo de cases.
- [ ] **Step 1: Escrever testes vermelhos de entidade e frescor**
`lib/seo.test.ts` importa os builders e `sitemap`:
```ts
it("liga site, perfil e serviço à mesma pessoa", () => {
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
expect(buildProfilePageJsonLd().mainEntity).toEqual({ "@id": SEO_IDS.person });
expect(buildServiceJsonLd(servicePages[0]).provider).toEqual({
"@id": SEO_IDS.person,
});
});
it("não publica localização ou oferta não comprovada", () => {
const serialized = JSON.stringify([
buildPersonJsonLd(),
buildServiceJsonLd(servicePages[0]),
]);
expect(serialized).not.toMatch(/ProfessionalService|LocalBusiness|GeoCoordinates|PostalAddress|priceRange|OfferCatalog/);
});
it("gera breadcrumb canônico e contíguo", () => {
const breadcrumb = buildBreadcrumbList(`${siteUrl}/servicos/x`, [
["Início", siteUrl],
["Serviços", `${siteUrl}/servicos`],
["X", `${siteUrl}/servicos/x`],
]);
expect(breadcrumb.itemListElement.map(({ position }) => position)).toEqual([1, 2, 3]);
});
it("mantém sitemap determinístico e sem frescor fabricado", () => {
expect(sitemap()).toEqual(sitemap());
expect(sitemap()).toHaveLength(16);
expect(sitemap().every((entry) => !("lastModified" in entry))).toBe(true);
});
```
- [ ] **Step 2: Rodar o teste vermelho**
```bash
pnpm exec jest lib/seo.test.ts --runInBand
```
Expected: falha porque builders não existem e sitemap contém `new Date()`.
- [ ] **Step 3: Implementar builders mínimos**
`SEO_IDS` usa os valores do spec. `buildPersonJsonLd()` publica nome, URL, `/edev.jpg`, GitHub, LinkedIn e `homeLocation` nomeado Garuva; não publica telefone, Instagram divergente, endereço ou geo. `buildWebSiteJsonLd()` publica `publisher` por `@id`. `buildProfilePageJsonLd()` usa `isPartOf`/`mainEntity` por `@id`, sem data. `buildServiceJsonLd()` usa nome/description/URL/provider e `areaServed` somente Garuva e Norte de Santa Catarina.
`buildBreadcrumbList` recebe tuplas readonly `[name, url]`, atribui posições a partir de 1 e define `@id` como `${pageUrl}#breadcrumb`.
- [ ] **Step 4: Migrar layout e currículo sem tocar o shell final**
Em `app/layout.tsx`, remover `keywords`; substituir o grafo pelo resultado de `buildWebSiteJsonLd()` e `buildPersonJsonLd()`. Preservar body exatamente como entregue pelo hardening.
Em `/curriculo`, usar `buildProfilePageJsonLd()` e `buildBreadcrumbList`; remover `new Date().toISOString()`. Metadata usa canonical absoluto, descrição factual e caminho de contratação. A primeira cópia visível usa a frase regional autorizada sem prometer disponibilidade.
- [ ] **Step 5: Remover hints temporais sem fonte**
`app/sitemap.ts` retorna apenas `{ url }` para as 16 rotas existentes. Remover `new Date`, `lastModified`, `changeFrequency` e `priority`.
- [ ] **Step 6: Checkpoint de entidade/frescor**
```bash
pnpm exec jest lib/seo.test.ts --runInBand
pnpm exec biome check lib/seo.ts lib/seo.test.ts app/layout.tsx app/curriculo/page.tsx app/sitemap.ts
pnpm typecheck
pnpm build
```
Expected: zero; build preserva as rotas estáticas; o teste não encontra tipos/propriedades proibidos.
---
### Task 3: Entregar a slice completa de serviços
**Files:**
- Modify: `app/servicos/page.tsx`
- Modify: `app/servicos/[slug]/page.tsx`
- Create: `app/servicos/[slug]/page.test.tsx`
**Interfaces:**
- Consumes: conteúdo fechado na Task 1 e builders da Task 2.
- Produces: resposta factual cedo, `Service`/breadcrumb corretos, CTA consultoria e links internos serviço→case.
- [ ] **Step 1: Escrever teste vermelho de um serviço representativo**
Mockar somente `next/navigation`, `HeroBackground` e `MagneticButton`; renderizar o JSX retornado pela página de `consultoria-nextjs`:
```tsx
const ui = await ServicePage({
params: Promise.resolve({ slug: "consultoria-nextjs" }),
});
render(ui);
expect(screen.getByRole("heading", { level: 1 })).toBeVisible();
expect(screen.getByText(getServicePage("consultoria-nextjs")!.answer)).toBeVisible();
expect(screen.getByRole("link", { name: /solicitar (consultoria|diagnóstico)/i }))
.toHaveAttribute("href", "/#contact");
expect(screen.getByRole("link", { name: /ler case:/i }))
.toHaveAttribute("href", expect.stringMatching(/^\/cases\//));
```
Ler o script JSON-LD pelo seletor e provar `Service.provider.@id === SEO_IDS.person`, ausência de `ProfessionalService`/`FAQPage` e breadcrumb 1–3.
- [ ] **Step 2: Rodar o teste vermelho**
```bash
pnpm exec jest 'app/servicos/[slug]/page.test.tsx' --runInBand
```
Expected: falha por `answer`, provider por `@id` e link interno ainda ausentes.
- [ ] **Step 3: Migrar a rota dinâmica inteira**
- `generateMetadata` mantém title/description/canonical/OG/Twitter próprios e não usa keywords.
- JSON-LD usa `buildServiceJsonLd(service)` e `buildBreadcrumbList`.
- Hero mostra `service.answer` imediatamente após H1; `intro` pode continuar como explicação seguinte se não repetir.
- CTA primário usa rótulo factual de `service.ctaLabel` e `/#contact`.
- Cada case relacionado recebe `<Link href={/cases/id}>Ler case: título</Link>`; demo/código permanecem externos.
- FAQ visível continua como conteúdo comum, sem JSON-LD de FAQ.
- [ ] **Step 4: Corrigir hub de serviços**
Usar `buildBreadcrumbList`; preservar CollectionPage/ItemList apenas com itens visíveis. Ajustar a descrição inicial para resposta factual e manter cartões/estilos. Hub aponta para serviços, não para formulário externo.
- [ ] **Step 5: Checkpoint de serviços**
```bash
pnpm exec jest 'app/servicos/[slug]/page.test.tsx' lib/site-content.test.ts --runInBand
pnpm exec biome check app/servicos/page.tsx 'app/servicos/[slug]/page.tsx' 'app/servicos/[slug]/page.test.tsx'
pnpm typecheck
pnpm build
```
Expected: zero; as seis páginas continuam estáticas e cada case relacionado tem link interno.
---
### Task 4: Entregar a slice completa dos três cases aprofundados
**Files:**
- Modify: `app/cases/page.tsx`
- Modify: `app/cases/[slug]/page.tsx`
- Create: `app/cases/[slug]/page.test.tsx`
**Interfaces:**
- Consumes: `CaseStudyDetail`, `getRelatedServices`, builders e IDs.
- Produces: conteúdo aprofundado com fonte, links case→serviço e CTAs separados.
- [ ] **Step 1: Escrever teste vermelho da página aprofundada e fallback**
Renderizar `as-servicos` e `gcc-installer` para provar os dois caminhos:
```tsx
const detailed = await CasePage({
params: Promise.resolve({ slug: "as-servicos" }),
});
render(detailed);
expect(screen.getByText(caseStudies.find(({ id }) => id === "as-servicos")!.detail!.answer.text))
.toBeVisible();
expect(screen.getByRole("heading", { name: "Decisões de implementação" })).toBeVisible();
expect(screen.getAllByRole("link", { name: /fonte:/i }).length).toBeGreaterThan(0);
expect(screen.getByRole("link", { name: "Avaliar perfil para contratação" }))
.toHaveAttribute("href", "/curriculo");
expect(screen.getByRole("link", { name: "Solicitar consultoria técnica" }))
.toHaveAttribute("href", "/#contact");
```
Após cleanup, renderizar `gcc-installer` e confirmar que summary/stack/links existentes aparecem sem headings vazios de aprofundamento.
- [ ] **Step 2: Rodar o teste vermelho**
```bash
pnpm exec jest 'app/cases/[slug]/page.test.tsx' --runInBand
```
Expected: falha por seções/fontes/links de serviço/CTA consultoria ainda ausentes.
- [ ] **Step 3: Renderizar aprofundamento sem inferência**
Para `detail` presente, a ordem é: resposta no hero → “Desafio verificável” → “Decisões de implementação” → “Limites desta evidência” → “Fontes”. Cada fonte é link externo com label e SHA. Para `detail` ausente, não renderizar essas seções.
Substituir o par genérico currículo/serviços por:
```tsx
<Link href={intentCtas.hiring.href}>{intentCtas.hiring.label}</Link>
<Link href={intentCtas.consulting.href}>
{intentCtas.consulting.label}
</Link>
```
Listar `getRelatedServices(caseStudy.id)` com links `/servicos/{slug}` e nomes descritivos.
- [ ] **Step 4: Corrigir metadata/schema e hub**
- Metadata de cada case usa summary factual, canonical, OG/Twitter próprios e sem keywords.
- Remover `Article` incompleto. Manter breadcrumb por builder; nenhuma data/imagem/autoria editorial é inventada.
- Hub usa breadcrumb builder e preserva ItemList consistente com cartões visíveis.
- Hub separa CTA “Avaliar perfil para contratação” (`/curriculo`) e “Solicitar consultoria técnica” (`/#contact`) usando classes atuais.
- [ ] **Step 5: Checkpoint dos cases**
```bash
pnpm exec jest 'app/cases/[slug]/page.test.tsx' lib/site-content.test.ts lib/portfolio-data.test.ts --runInBand
pnpm exec biome check app/cases/page.tsx 'app/cases/[slug]/page.tsx' 'app/cases/[slug]/page.test.tsx'
pnpm typecheck
pnpm build
```
Expected: zero; três cases aprofundados, três no fallback simples, nenhum resultado comercial alegado.
---
### Task 5: Criar template e OG dinâmico de serviço/case
**Files:**
- Create: `lib/og-card.tsx`
- Modify: `app/opengraph-image.tsx`
- Create: `app/servicos/[slug]/opengraph-image.tsx`
- Create: `app/cases/[slug]/opengraph-image.tsx`
**Interfaces:**
- Produces: `OG_SIZE`, `OG_CONTENT_TYPE`, `OgCard({ eyebrow, title, description })`.
- Consumes: slugs e conteúdo fechados nas Tasks 1, 3 e 4.
- [ ] **Step 1: Implementar componente visual puro**
```tsx
export const OG_SIZE = { width: 1200, height: 630 } as const;
export const OG_CONTENT_TYPE = "image/png";
export function OgCard({
eyebrow,
title,
description,
}: {
eyebrow: string;
title: string;
description: string;
}) {
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "#050505",
color: "#f4f1e8",
padding: 72,
fontFamily: "Arial, sans-serif",
}}
>
<div
style={{
display: "flex",
color: "#ccff00",
fontSize: 24,
letterSpacing: "0.16em",
textTransform: "uppercase",
}}
>
{eyebrow}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
<div style={{ display: "flex", fontSize: 64, fontWeight: 700 }}>
{title}
</div>
<div
style={{
display: "flex",
maxWidth: 980,
color: "#c7c3ba",
fontSize: 28,
lineHeight: 1.35,
}}
>
{description}
</div>
</div>
<div
style={{
display: "flex",
borderTop: "1px solid #343434",
paddingTop: 24,
fontSize: 20,
letterSpacing: "0.12em",
}}
>
EVANDRO.DEV.BR / ENGENHARIA DE SOFTWARE
</div>
</div>
);
}
```
Usar estilos inline aceitos por `ImageResponse`; nenhuma imagem gerada/foto/claim novo. Migrar a imagem raiz para `OgCard` com copy de marca.
- [ ] **Step 2: Criar imagem de serviço por params**
Exportar `generateImageMetadata({ params })` com um item contendo `id`, `size: OG_SIZE`, `contentType` e alt `“{navLabel} — Evandro Fonseca Junior”`. A função default recebe `params` e `id` como promises no Next 16, resolve serviço e retorna `new ImageResponse(<OgCard eyebrow={service.eyebrow} title={service.headline} description={service.answer} />, OG_SIZE)`. Slug inválido usa `notFound()`.
- [ ] **Step 3: Criar imagem de case por params**
Mesma interface, com alt `“Case {title} — Evandro Fonseca Junior”`, eyebrow “Case de engenharia de software”, title e summary factuais. Slug inválido usa `notFound()`.
- [ ] **Step 4: Verificar build e respostas reais**
```bash
pnpm exec biome check lib/og-card.tsx app/opengraph-image.tsx 'app/servicos/[slug]/opengraph-image.tsx' 'app/cases/[slug]/opengraph-image.tsx'
pnpm typecheck
pnpm build
```
Iniciar o build por processo supervisionado:
```bash
pnpm start
```
No browser/rede, abrir uma página de serviço e AS Serviços. Obter as URLs `og:image` do head e confirmar para cada resposta: status `200`, `content-type: image/png`, imagem 1200×630 e texto/alt correspondente à rota. Encerrar o processo supervisionado.
---
### Task 6: Completar OG de hubs e currículo
**Files:**
- Create: `app/servicos/opengraph-image.tsx`
- Create: `app/cases/opengraph-image.tsx`
- Create: `app/curriculo/opengraph-image.tsx`
**Interfaces:**
- Consumes: `OgCard`, `OG_SIZE`, `OG_CONTENT_TYPE`.
- Produces: imagem específica para cada segmento estático.
- [ ] **Step 1: Criar as três imagens estáticas**
Cada arquivo exporta `alt`, `size`, `contentType` e retorna `ImageResponse`:
| Segmento | Eyebrow | Title | Descrição curta |
| --- | --- | --- | --- |
| `/servicos` | Serviços | Engenharia de software e consultoria técnica | Base em Garuva, Norte de Santa Catarina e remoto. |
| `/cases` | Trabalho verificável | Cases de software, web e sistemas conectados | Decisões, implementação, limites e fontes. |
| `/curriculo` | Perfil profissional | Evandro Fonseca Junior | Engenharia da computação e desenvolvimento de software. |
- [ ] **Step 2: Verificar metadata herdada por segmento**
```bash
pnpm exec biome check app/servicos/opengraph-image.tsx app/cases/opengraph-image.tsx app/curriculo/opengraph-image.tsx
pnpm typecheck
pnpm build
```
No build iniciado com `pnpm start`, abrir `/servicos`, `/cases` e `/curriculo`, conferir uma `og:image` distinta por segmento, `200 image/png`, 1200×630 e alt específico. Encerrar o processo.
---
### Task 7: Publicar privacidade coerente e integrar consentimento
**Files:**
- Create: `app/privacidade/page.tsx`
- Create: `app/privacidade/page.test.tsx`
- Create: `app/privacidade/opengraph-image.tsx`
- Modify: `app/components/GoogleAnalytics.tsx`
- Modify: `app/components/GoogleAnalytics.test.tsx`
- Modify: `app/components/Footer.tsx`
- Modify: `app/components/Footer.test.tsx`
- Modify: `app/sitemap.ts`
- Modify: `lib/seo.test.ts`
**Interfaces:**
- Consumes: consentimento final `evandro.analytics-consent.v1`, `granted | denied`, revogação e `OgCard`.
- Produces: `/privacidade`, links crawlable e sitemap de 17 URLs.
- [ ] **Step 1: Estender testes de consentimento e Footer**
No teste final de `GoogleAnalytics`, adicionar:
```tsx
expect(screen.getByRole("link", { name: "Ler política de privacidade" }))
.toHaveAttribute("href", "/privacidade");
```
A asserção roda no estado sem escolha e na tela de preferências. No teste do Footer:
```tsx
expect(screen.getByRole("link", { name: "Privacidade" }))
.toHaveAttribute("href", "/privacidade");
```
Não alterar as provas existentes de zero request antes de consentimento, aceite, recusa e revogação.
- [ ] **Step 2: Escrever teste vermelho da página**
Renderizar `PrivacyPage` e provar conteúdo observável:
```tsx
expect(screen.getByRole("heading", { level: 1, name: /privacidade/i })).toBeVisible();
expect(screen.getByText(/evandro\.analytics-consent\.v1/)).toBeVisible();
expect(screen.getByText(/WhatsApp/)).toBeVisible();
expect(screen.getByText(/nenhuma tag do Google é carregada/i)).toBeVisible();
expect(screen.getByText(/2026-09-04/)).toBeVisible();
```
Ler o JSON-LD e confirmar breadcrumb Início→Privacidade; não adicionar entidade de negócio.
- [ ] **Step 3: Rodar testes vermelhos**
```bash
pnpm exec jest app/components/GoogleAnalytics.test.tsx app/components/Footer.test.tsx app/privacidade/page.test.tsx --runInBand
```
Expected: falha pelos links/rota ainda ausentes; os testes de consentimento anteriores continuam compilando.
- [ ] **Step 4: Implementar a página com o conteúdo fechado no spec**
Usar `main`, `ds-section`, `ds-section-shell`, headings, cards e links existentes. Incluir metadata title/description/canonical e breadcrumb. Não criar banner/CMP novo. Não afirmar retenção, venda, certificação legal ou integração não presente.
- [ ] **Step 5: Integrar links sem redesenhar**
Adicionar link “Ler política de privacidade” ao diálogo/preferências já existentes em `GoogleAnalytics`. Adicionar “Privacidade” a `quickLinks` do Footer. Preservar todos os nomes acessíveis e estados do hardening.
- [ ] **Step 6: Adicionar OG e sitemap**
`app/privacidade/opengraph-image.tsx` usa `OgCard` com título “Privacidade e consentimento”. Adicionar `${siteUrl}/privacidade` ao sitemap, ainda sem datas/hints. Atualizar `lib/seo.test.ts` para 17 URLs e presença única da rota.
- [ ] **Step 7: Checkpoint automatizado e browser**
```bash
pnpm exec jest app/components/GoogleAnalytics.test.tsx app/components/Footer.test.tsx app/privacidade/page.test.tsx lib/seo.test.ts --runInBand
pnpm exec biome check app/privacidade app/components/GoogleAnalytics.tsx app/components/GoogleAnalytics.test.tsx app/components/Footer.tsx app/components/Footer.test.tsx app/sitemap.ts lib/seo.test.ts
pnpm typecheck
pnpm build
```
No browser de produção local: limpar consentimento/cookies; abrir `/privacidade`; navegar pelo link do Footer; voltar, abrir preferências, seguir o link de política; confirmar zero request Google antes de escolha e que revogação continua funcionando. Conferir foco, mobile e reduced motion sem alteração visual estrutural.
---
### Task 8: Documentar ações externas e fechar com prova integral
**Files:**
- Create: `docs/seo-geo-operations.md`
- Verify only: todos os arquivos das Tasks 1–7
**Interfaces:**
- Consumes: 17 URLs finais, consentimento final, sitemap e fontes do spec.
- Produces: runbook externo declarativo e evidência final.
- [ ] **Step 1: Criar runbook de Search Console**
Registrar ações desmarcadas com evidência exigida:
- verificação de propriedade;
- submissão de `https://evandro.dev.br/sitemap.xml`;
- inspeção das 17 URLs e canônica escolhida;
- baseline de 90 dias por consulta/página/dispositivo/localidade;
- relatório de IA generativa e configuração de inclusão, se disponíveis na conta.
Escrever explicitamente “Estado: não executado neste repositório” no cabeçalho.
- [ ] **Step 2: Registrar Bing, Cloudflare e medição**
Bing: propriedade, sitemap, inspeção, AI Performance; IndexNow permanece excluído até haver publicação frequente. Cloudflare: comparar resposta pública e `robots.ts`, Managed Robots, AI Crawl Control, WAF e logs `200/403/429`; separar crawlers de busca/citação de treino. Medição: p75 LCP/INP/CLS por template, consultas, landing, caminhos case→serviço→contato e conversão.
Nenhuma caixa é marcada, nenhum valor é preenchido com estimativa e nenhuma credencial/comando de painel é incluído.
- [ ] **Step 3: Auto-revisar contra os 57 itens-fonte**
Usar a matriz do spec e confirmar por grupo: Google 13; Bing/Web Vitals 4; Schema 6; Next 7; GEO/crawlers 7; mercado 20. Verificar que nenhuma implementação adicionou city page, conteúdo escalado, `llms.txt`, FAQ schema, keyword metadata, entidade local não comprovada, claim ou data artificial.
Executar varredura textual focada:
```bash
pnpm exec biome check app lib docs/seo-geo-operations.md
```
Usar a ferramenta de busca do harness em `app`, `lib`, `public/portfolio-data.json` e no runbook para:
```text
ProfessionalService|LocalBusiness|FAQPage|keywords:|llms\.txt|priceRange|GeoCoordinates|PostalAddress
```
Expected: nenhum resultado em produção. Menções históricas permanecem apenas no spec/pesquisa como proibições documentadas.
- [ ] **Step 4: Rodar gate completo uma vez**
```bash
pnpm install --frozen-lockfile
pnpm lint
pnpm typecheck
pnpm test -- --runInBand
pnpm build
pnpm audit --prod --audit-level=high
pnpm audit --dev --audit-level=high
```
Expected: todos código zero; install não altera lock; build lista 17 rotas indexáveis e os endpoints de imagem esperados.
- [ ] **Step 5: Smoke real em desktop/mobile/reduced motion**
Com `pnpm start` em processo supervisionado, usar navegador real:
1. abrir `/`, `/servicos`, os seis serviços, `/cases`, os seis cases, `/curriculo` e `/privacidade`;
2. verificar title/description/canonical/OG e parsear todo `script[type="application/ld+json"]` com `JSON.parse`;
3. confirmar resposta factual perto do H1, links recíprocos e destinos de CTA;
4. abrir as sete classes de OG (root, hubs, dinâmicos, currículo, privacidade) e validar `200 image/png`, 1200×630;
5. testar menu/Footer, consentimento, foco, 404, mobile e reduced motion;
6. confirmar console sem erro/hidratação e nenhuma request Google antes de consentimento.
Encerrar o processo supervisionado.
- [ ] **Step 6: Conferir corte limpo**
Confirmar por leitura:
- três cases aprofundados e somente eles;
- cada afirmação técnica/factual resolve para fonte SHA-pinned; somente o limite editorial de AS usa `sourceIds: []`;
- 17 URLs únicas no sitemap, sem datas artificiais;
- nenhum loader/health/interface do hardening foi alterado;
- `data/project-health.json`, `globals.css` raiz, lock e dependências estão inalterados por SEO;
- runbook não alega execução externa;
- direção visual existente está preservada.
## Critérios de saída por slice
| Slice | Observável independente |
| --- | --- |
| 1. Fatos | Dados compilam; três cases têm proveniência válida com exceção editorial explícita; relações inversas derivam da lista existente. |
| 2. Entidades/frescor | IDs são únicos/referenciados; sitemap determinístico; zero tipos/propriedades proibidos. |
| 3. Serviços | Seis rotas com resposta cedo, Service/breadcrumb, CTA consultoria e link para case. |
| 4. Cases | Três aprofundados com fonte, três sem preenchimento genérico, links para serviços e dois CTAs distintos. |
| 5. OG dinâmico | Home, serviço e case retornam imagem correta por conteúdo/slug. |
| 6. OG estático | Hubs e currículo não herdam card genérico. |
| 7. Privacidade | Rota, consentimento, Footer, sitemap e OG concordam; coleta opt-in continua comprovada. |
| 8. Operação | Runbook declarativo, 57 fontes reconciliadas e gate/smoke completos verdes. |
## Auto-revisão do plano
- **Cobertura:** todos os requisitos do spec aparecem em uma task e em critério observável.
- **Tamanho:** oito tarefas; nenhuma cria subsistema, dependência ou página por cidade.
- **Interfaces:** Tasks 1–2 são providers; 3–4 consumidores independentes; 5 fornece OG; 6–7 consomem; 8 integra.
- **Conflitos:** todos os arquivos compartilhados com hardening ou entre tasks têm ordem explícita.
- **Fontes:** AS Serviços, NutriScan e Twins Lab têm fonte SHA-pinned por novo campo e limites editoriais expressos.
- **Verificação:** cada slice tem comando real; mudanças de rota/OG/consentimento têm smoke no produto real.
- **Restrições:** sem commit, dependência, redesign, city-clone, schema proibido, dado inventado ou ação externa alegada.
- **57 fontes:** a revisão segue a contagem 13 + 4 + 6 + 7 + 7 + 20 e preserva as conclusões da pesquisa.
A execução termina apenas após a Task 8. Este documento não autoriza nenhuma alteração de produção antes do hardening completo.
@@ -0,0 +1,267 @@
# 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.json` modificado pelo usuário: não regenerar nem reverter nesta iniciativa.
- `lib/portfolio-data.ts` modificado pelo usuário: evoluir a partir do conteúdo atual, preservando a atualização apenas em desenvolvimento.
- `globals.css` na raiz, não rastreado: não editar, mover, importar ou apagar. O stylesheet ativo é `app/globals.css`.
- `lib/portfolio-data.load.test.ts` nã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.tsx` carrega projetos no servidor por `loadLiveFeaturedProjects()` e repassa dados para `CinematicHome`.
- `filterLiveProjects()` hoje transforma um snapshot ausente/expirado em lista vazia, ocultando todo projeto.
- `app/layout.tsx` envolve todas as rotas em Lenis/GSAP, contexto de animação, transição de rota e overlays; `PageIntro` cria 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.
- `GoogleAnalytics` carrega GA em produção sem consentimento e usa rewrites que camuflam script/coleta.
- `next.config.ts` marca diretórios públicos com nomes estáveis como `immutable` e mantém CSP somente em relatório com `unsafe-inline` e `unsafe-eval`.
- Não existe workflow em `.github/workflows`.
- `pnpm-lock.yaml` resolve Next `16.2.9`, contém overrides históricos e foi gerado com faixas, embora `package.json` não fixe o conjunto de dependências.
- `nixpacks.toml` já 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:
```ts
export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";
export type PortfolioProjectView = PortfolioProject & {
demoAvailability: DemoAvailability;
demoCheckedAt: string | null;
};
```
Regras:
1. `featured === false` continua sendo a única regra de exclusão editorial.
2. Sem `demoUrl`, o estado é `none` e não há CTA de demo.
3. Sem entrada de snapshot, o estado é `unknown`; o card permanece e o URL configurado não é descartado.
4. Resposta final `200` com `Content-Type` HTML produz `available`.
5. Qualquer resultado explícito diferente produz `unavailable`.
6. `unavailable` renderiza a evidência inteira e um controle não navegável com texto “Demo indisponível na última verificação”.
7. `unknown` mantém o link e o anota como “Disponibilidade não verificada”.
8. 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`.
9. 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 `PageIntro` e 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.
- `ReelDirector` e `CinematicSection`, 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:
```ts
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`, `ContactInfo` e `usePortfolioData`; `Contact` é o caminho ativo.
- `CompanyLogos` e seus três arquivos de teste.
- todos os demos na raiz de `components/` e `components/magicui/`, sem importadores.
- `AnimationProvider`, seu teste, `app/lib/animations/config.ts`, `app/lib/animations/variants.ts` e `app/hooks/useScrollReveal.ts`.
- `FloatingElements`, `HomeButton`, `HorizontalScroll` e `CinematicText`, 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 `Projects` por nome/estado acessível.
- Remover PBTs de `CompanyLogos` junto do componente.
- Remover PBTs do Footer que repetem um componente estático; `Footer.test.tsx` já cobre nomes acessíveis e segurança de links.
- Substituir três PBTs de `Experience` por 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-check` quando 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 por `16.2.11` e `16.2.12`.
- Todas as dependências diretas e de desenvolvimento terão versão exata, sem `^`, `~` ou comparadores.
- `@types/node` acompanha o runtime em `22.20.1`.
- `@types/three` passa para `devDependencies`.
- Remover pacotes cujo último consumidor foi apagado: `@gsap/react`, `class-variance-authority`, `lenis`, `ogl`, `tailwindcss-animate`, `tw-animate-css`, `fast-check`, `@types/resize-observer-browser` e `resize-observer-polyfill`.
- Manter GSAP, cobe, Motion, React Three Fiber, Drei e Three porque ainda há consumidores ativos.
- Regenerar `pnpm-lock.yaml` com pnpm 9.15.9 e sem bloco `overrides`.
- 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:
1. checkout;
2. instalação congelada com cache do pnpm;
3. `pnpm lint`;
4. `pnpm typecheck`;
5. `pnpm test -- --runInBand`;
6. `pnpm build`;
7. `pnpm audit --prod --audit-level=high`;
8. `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.
- `GoogleAnalytics` começa sem `<Script>` e sem requisição a Google.
- Persistir somente `"granted"` ou `"denied"` em `localStorage` sob 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 `granted` monta `https://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3S` e a configuração de GA.
- Consentimento de publicidade permanece negado; somente `analytics_storage` pode ser concedido.
- Revogação chama `gtag("consent", "update", ...)` com estados negados, define `window["ga-disable-G-QTT9QT0T3S"] = true`, expira cookies `_ga` e `_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'` e `form-action 'self'`.
- Remover `unsafe-eval` em produção; mantê-lo somente em desenvolvimento, onde o Next documenta a necessidade para debugging.
- Manter `unsafe-inline` por 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 `immutable` para `/static`, `/fonts`, `/assets` e `/logos`; esses nomes públicos não têm hash garantido. Assets gerados pelo Next em `/_next/static` já 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.md` será substituído por `docs/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.css` nã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:
1. Antes de cada tarefa, ler e registrar os arquivos exatos daquele lote.
2. Se o checkpoint falhar, interromper o lote seguinte.
3. Restaurar somente os arquivos do lote com edições inversas baseadas no snapshot pré-tarefa.
4. Para arquivos novos, removê-los somente se foram criados pelo lote e ainda não são consumidos.
5. Para dependências, restaurar juntos `package.json`, `pnpm-lock.yaml`, `tailwind.config.js` e `nixpacks.toml`; nunca restaurar apenas o lock.
6. Reexecutar o último checkpoint verde.
7. Nunca incluir `data/project-health.json`, `globals.css` da raiz ou mudanças não relacionadas no rollback.
## Critérios observáveis de aceite
1. Home e `/cases` continuam exibindo cards independentemente de snapshot ausente, antigo ou com falha.
2. Demo explicitamente indisponível não navega e tem estado acessível; estado desconhecido não remove o URL nem o card.
3. 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.
4. Skip link recebe foco visível imediatamente, sem splash sobreposto.
5. Links da 404 exibem as cores Tailwind pretendidas e foco visível.
6. Reduced motion mostra todo conteúdo essencial sem scroll suave, loops ou transições longas.
7. Loops restantes deixam de atualizar quando seção está fora da viewport ou `document.hidden` é verdadeiro.
8. Hero mostra o mesmo conteúdo command-center sem infraestrutura de variantes mortas.
9. Não há imports para os arquivos excluídos e TypeScript resolve o workspace sem erros.
10. `package.json` contém versões exatas, Node `22.x`, pnpm `9.15.9` e Next `16.3.0`; lock não contém `overrides`.
11. Instalação Nixpacks e local usa frozen lockfile com pnpm 9.
12. `pnpm audit --prod --audit-level=high` e `pnpm audit --dev --audit-level=high` encerram com código zero, sem force, ignore ou override.
13. Antes de consentimento/rejeição não há request a `googletagmanager.com`/`google-analytics.com` nem cookie `_ga*` criado pela página.
14. Aceite carrega GA diretamente; revogação nega consentimento, desabilita coleta futura e expira cookies acessíveis.
15. 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.
16. CI executa install congelado, Biome, tsc, Jest, build e auditorias prod/dev.
17. README e documentação de project health descrevem comandos, versões e comportamento finais sem depender de cron.
18. `globals.css` da raiz e `data/project-health.json` permanecem 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
@@ -0,0 +1,391 @@
# Design: crescimento SEO e GEO do portfólio
**Status:** pronto para execução somente após o hardening técnico
**Data:** 2026-09-04
**Pesquisa-base:** `.superpowers/seo-research-2026-09-04.md`
**Plano predecessor:** `docs/superpowers/plans/2026-09-04-portfolio-hardening.md`
## Objetivo
Melhorar descoberta, compreensão e confiança do portfólio em busca orgânica e experiências generativas sem redesenhar a interface, criar páginas por cidade ou publicar alegações sem prova. A implementação corrige frescor e entidades, torna metadados e Open Graph específicos por rota, conecta serviços a evidências, aprofunda três cases sustentados pelo próprio repositório e por código público, separa os caminhos de contratação e consultoria e documenta medição/operações externas sem alegar que foram executadas.
## Gate de sequência
SEO começa **depois** do gate final do hardening. Na data deste design, apenas a Task 1 do hardening estava concluída; as Tasks 2–12 ainda alterariam arquivos compartilhados. O executor deve aceitar como baseline o estado final comprovado pelo predecessor, não restaurar o estado observado durante esta pesquisa.
Interfaces finais que este design consome:
- `PortfolioProjectView` preserva todo projeto editorial e anexa `demoAvailability`/`demoCheckedAt`.
- `app/layout.tsx` é server-first, sem providers globais de scroll/animação, e mantém Header, overlays leves, conteúdo e analytics.
- `GoogleAnalytics` usa consentimento opt-in, chave `evandro.analytics-consent.v1`, valores `granted | denied`, endpoint oficial do Google e revogação alcançável.
- `package.json` oferece `pnpm lint`, `pnpm typecheck`, `pnpm test`, `pnpm build`; Node 22, pnpm 9.15.9 e Next 16.3.0 ficam fixados pelo ruling de segurança do hardening.
- `Footer`, `Contact`, hero command-center, globo, starfield, notebook, tipografia, paleta preto/papel/verde ácido, cartões e tokens `ds-*` permanecem como direção visual invariável.
- `data/project-health.json` e o `globals.css` não rastreado da raiz continuam fora deste escopo.
Se o gate final do hardening não estiver verde, a execução deste plano não começa. Não se adapta SEO ao código intermediário.
## Princípios e limites
### Sempre
- Uma URL canônica por intenção; páginas existentes são enriquecidas em vez de duplicadas.
- Conteúdo factual, legível no HTML e apoiado por fonte verificável.
- Base em Garuva e atendimento no Norte de Santa Catarina/remoto somente no alcance já declarado pelo site.
- `Person` é a entidade prestadora e autora; `WebSite` é o site; `ProfilePage` existe na rota de currículo; cada serviço tem `Service`; rotas não-home têm `BreadcrumbList` quando a trilha existe na interface.
- Metadata API e convenções de imagem do Next.js 16; sem pacote novo.
- CTAs usam os componentes/classes atuais. A separação é semântica e de destino, não um novo layout.
- Privacidade descreve exatamente o comportamento do consentimento entregue pelo hardening.
### Nunca
- Página-clone de Garuva, Joinville, Curitiba ou outra cidade.
- `llms.txt`, `FAQPage`, `keywords` em metadata, `ProfessionalService`, ou `LocalBusiness` sem negócio/local comprovado.
- Coordenada, endereço postal, preço, cliente, review, volume, ranking, resultado comercial ou métrica de performance não comprovados.
- Afirmação de que framework, schema ou formatação garante ranking/citação.
- Mudança de paleta, tipografia, grid, hero, efeitos preservados, arquitetura de informação ou biblioteca visual.
- Dependência, commit, branch ou ação em Search Console/Bing/Cloudflare nesta iniciativa de documentação.
## Arquitetura de conteúdo
`lib/site-content.ts` continua sendo a fonte das páginas de serviço e case. Não será criada uma CMS ou segunda camada de dados. A extensão mínima é:
```ts
export const intentCtas = {
hiring: {
label: "Avaliar perfil para contratação",
href: "/curriculo",
},
consulting: {
label: "Solicitar consultoria técnica",
href: "/#contact",
},
} as const;
export type CtaIntent = keyof typeof intentCtas;
export interface EvidenceSource {
id: string;
label: string;
url: string;
}
export interface SourcedStatement {
text: string;
sourceIds: string[];
}
export interface CaseStudyDetail {
answer: SourcedStatement;
challenge: SourcedStatement;
implementation: SourcedStatement[];
boundaries: SourcedStatement[];
sources: EvidenceSource[];
}
export interface ServicePageContent {
slug: string;
navLabel: string;
eyebrow: string;
seoTitle: string;
metaDescription: string;
headline: string;
answer: string;
intro: string;
audience: string[];
outcomes: string[];
process: string[];
stack: string[];
ctaIntent: "consulting";
ctaLabel: string;
ctaHref: string;
relatedCaseIds: string[];
faq?: { question: string; answer: string }[];
}
export interface CaseStudyContent {
id: string;
title: string;
subtitle: string;
summary: string;
impact: string;
stack: string[];
demoUrl?: string | null;
githubUrl?: string | null;
detail?: CaseStudyDetail;
}
```
`detail` é opcional porque somente três cases têm evidência suficiente nesta rodada. O conjunto deve ser exatamente `as-servicos`, `nutriscan` e `twins-lab`, sem depender da ordem. Afirmações técnicas/factuais em `answer`, `challenge`, `implementation` e limites comparativos exigem pelo menos um `sourceId` externo. O limite puramente editorial de AS Serviços mantém `sourceIds: []`: não será criada URL fictícia ou circular para uma decisão deste design. Qualquer ID presente deve resolver para uma fonte única dentro do mesmo case, e toda `EvidenceSource.url` deve ser um permalink GitHub `/blob/<SHA de 40 hex>/...`. A página exibe as fontes ao final do conteúdo aprofundado.
A relação existente `ServicePageContent.relatedCaseIds` permanece como única fonte de relacionamento. A função nova:
```ts
export function getRelatedServices(caseId: string): ServicePageContent[] {
return servicePages.filter(({ relatedCaseIds }) =>
relatedCaseIds.includes(caseId),
);
}
```
permite links recíprocos sem manter duas listas divergentes.
### CTAs
Os seis serviços mantêm `ctaIntent: "consulting"` e levam a `/#contact`, com rótulo específico de conversa técnica/projeto. As páginas de case mostram dois caminhos usando o sistema visual existente:
- **“Avaliar perfil para contratação”** → `/curriculo` (`hiring`);
- **“Solicitar consultoria técnica”** → `/#contact` (`consulting`).
O currículo mantém o caminho de contratação e oferece um link secundário para `/servicos` a quem procura consultoria. Não há formulário, modal ou estado de URL novo.
## Cases escolhidos e fonte de cada campo
### 1. AS Serviços (`as-servicos`)
Foi escolhido por ter demo pública, repositório público atual, README técnico, manifesto e implementação legível da cena.
| Campo novo | Texto factual autorizado | Fontes |
| --- | --- | --- |
| `detail.answer` | “Site institucional em Next.js 16 que representa quatro frentes de engenharia com uma cena Three.js gerada em código: uma casa wireframe e uma placa-mãe ligadas ao progresso da rolagem.” | `README.md`, `package.json`, `lib/three/engine.ts` |
| `detail.challenge` | “Representar engenharia física e software na mesma narrativa visual sem depender de um modelo 3D pronto.” | `README.md`, `lib/three/engine.ts` |
| `detail.implementation[0]` | “O motor monta casa, instalação elétrica e placa-mãe com geometrias e linhas do Three.js.” | `lib/three/engine.ts` |
| `detail.implementation[1]` | “O canvas converte a rolagem em progresso de 0 a 1 e o entrega ao motor da cena.” | `components/scene/scene-canvas.tsx` |
| `detail.implementation[2]` | “A qualidade pode cair para `static`, `low`, `medium` ou `high` conforme WebGL, reduced motion, economia de dados, conexão e capacidade do dispositivo.” | `lib/three/engine.ts` |
| `detail.boundaries[0]` | “O case demonstra implementação e decisões técnicas; não atribui geração de leads, ranking ou ganho de performance.” | Limite puramente editorial deste design; `sourceIds: []` |
Fontes imutáveis de referência:
- `https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/README.md`
- `https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/package.json`
- `https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/lib/three/engine.ts`
- `https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/components/scene/scene-canvas.tsx`
### 2. NutriScan (`nutriscan`)
Foi escolhido porque o código público confirma fluxos concretos de busca, validação e fallback, embora a documentação e o manifesto discordem sobre versão e persistência. O texto novo usa apenas o cruzamento verificável.
| Campo novo | Texto factual autorizado | Fontes |
| --- | --- | --- |
| `detail.answer` | “Aplicação web para consultar produtos alimentícios por nome ou código de barras, combinando cache JSON com a API Open Food Facts.” | `app/api/search/route.ts`, `app/api/products/[barcode]/route.ts`, `lib/openFoodFactsApi.ts` |
| `detail.challenge` | “Reduzir a dependência da API externa e manter um caminho explícito para buscas e consulta por código.” | `app/api/search/route.ts`, `app/api/products/[barcode]/route.ts` |
| `detail.implementation[0]` | “A busca consulta o cache primeiro, permite modo somente-cache e recorre ao Open Food Facts quando faltam resultados.” | `app/api/search/route.ts` |
| `detail.implementation[1]` | “A consulta por código remove caracteres não numéricos, aceita de 8 a 13 dígitos, consulta cache e usa a API externa como fallback.” | `app/api/products/[barcode]/route.ts` |
| `detail.implementation[2]` | “O wrapper cancela requisições após oito segundos e diferencia busca por nome de busca por código.” | `lib/openFoodFactsApi.ts` |
| `detail.boundaries[0]` | “A página não afirma PWA, MongoDB, histórico de usuários, disponibilidade, volume de produtos ou resultado de saúde, porque as fontes atuais não sustentam esse conjunto de forma consistente.” | `package.json`, `prisma/schema.prisma`, README e código comparados |
Fontes imutáveis de referência:
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/app/api/search/route.ts`
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/app/api/products/%5Bbarcode%5D/route.ts`
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/lib/openFoodFactsApi.ts`
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/package.json`
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/prisma/schema.prisma`
- `https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/README.md`
### 3. Twins Lab (`twins-lab`)
Foi escolhido porque o repositório contém um simulador, persistência, autenticação e uma rota de streaming observáveis. As métricas promocionais e recursos marcados como futuros no README são excluídos.
| Campo novo | Texto factual autorizado | Fontes |
| --- | --- | --- |
| `detail.answer` | “Protótipo de gêmeo digital industrial com autenticação, simulador de reator, persistência MongoDB e streaming de eventos do servidor.” | `package.json`, `app/api/reactor/stream/route.ts`, `simulators/reactor/reactor-simulator.ts` |
| `detail.challenge` | “Expor estados simulados do reator a uma interface autenticada e manter histórico/status do simulador.” | `app/api/reactor/stream/route.ts`, `simulators/reactor/reactor-simulator.ts` |
| `detail.implementation[0]` | “A rota exige sessão do NextAuth antes de abrir o stream.” | `app/api/reactor/stream/route.ts` |
| `detail.implementation[1]` | “A resposta usa `text/event-stream`, envia estados/dados/erros em intervalos de dois segundos e limita a conexão a dez minutos.” | `app/api/reactor/stream/route.ts` |
| `detail.implementation[2]` | “O simulador salva dados e estado no MongoDB e notifica clientes quando execução ou cenário mudam.” | `simulators/reactor/reactor-simulator.ts` |
| `detail.boundaries[0]` | “O case não usa as alegações de economia, ROI, precisão, manutenção preditiva futura, equipamento real, WebSocket, OpenRouter ou atualização sub-segundo.” | README comparado ao código público |
Fontes imutáveis de referência:
- `https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/package.json`
- `https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/app/api/reactor/stream/route.ts`
- `https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/simulators/reactor/reactor-simulator.ts`
- `https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/README.md`
## Resposta factual cedo
A primeira descrição abaixo do H1 deve responder o que é oferecido ou demonstrado sem slogan genérico:
- Serviços usam o novo `answer` antes das listas de público/processo.
- Os três cases aprofundados usam `detail.answer.text` no hero; os demais mantêm o `summary` atual, após remoção de afirmações não comprovadas.
- Hubs explicam em uma frase o que o usuário encontra e para qual próximo passo cada cartão aponta.
- Currículo começa com profissão, base e modalidades já declaradas; não promete disponibilidade, prazo de resposta ou presença física nova.
Texto regional comum autorizado: **“Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.”** Ele substitui enumerações extensas de cidades onde a página não demonstra presença local.
## Modelo de entidades e dados estruturados
IDs canônicos, exportados por `lib/seo.ts`:
```ts
export const SEO_IDS = {
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
} as const;
```
### Grafo raiz
Somente:
```json
{
"@type": "WebSite",
"@id": "https://evandro.dev.br/#website",
"url": "https://evandro.dev.br",
"name": "Evandro.dev.br",
"inLanguage": "pt-BR",
"publisher": { "@id": "https://evandro.dev.br/#person" }
}
```
```json
{
"@type": "Person",
"@id": "https://evandro.dev.br/#person",
"name": "Evandro Fonseca Junior",
"url": "https://evandro.dev.br",
"image": "https://evandro.dev.br/edev.jpg",
"homeLocation": { "@type": "Place", "name": "Garuva, Santa Catarina, Brasil" },
"sameAs": ["GitHub público", "LinkedIn público"]
}
```
Não há endereço postal, coordenada, preço, raio, catálogo de ofertas ou Instagram divergente sem validação.
### Currículo
`ProfilePage` aparece somente em `/curriculo`, referencia `SEO_IDS.person`, define `isPartOf` para `SEO_IDS.website` e não inclui `dateModified` até existir uma data editorial persistida. A pessoa pode ser referenciada por `@id`; não é duplicada com propriedades divergentes.
### Serviço
Cada `/servicos/[slug]` emite:
- `Service` com `@id: ${pageUrl}#service`, `url`, `name`, `description`, `provider: { "@id": SEO_IDS.person }`;
- `areaServed` limitado a Garuva e Norte de Santa Catarina, sem coordenada/raio/endereço;
- `BreadcrumbList` com `@id: ${pageUrl}#breadcrumb` e posições contíguas 1–3.
### Hubs, cases e privacidade
Hubs preservam `CollectionPage`/`ItemList` quando os itens visíveis coincidem com a lista. Cases removem `Article` incompleto; usam `BreadcrumbList`, pois autoria/datas/imagem de artigo não estão comprovadas como publicação editorial. Currículo, hubs, cases, serviços e privacidade têm breadcrumbs visíveis/estruturados coerentes.
## Frescor
- `app/sitemap.ts` não inclui `lastModified` enquanto não houver data editorial estável por URL.
- `changeFrequency` e `priority` também são removidos: não há fonte operacional que os mantenha e mecanismos os tratam apenas como dicas.
- `/curriculo` remove `new Date().toISOString()`.
- Nenhuma data muda por build ou request.
Critério determinístico: duas chamadas consecutivas de `sitemap()` em instantes diferentes retornam estruturas profundamente iguais.
## Metadata e Open Graph por rota
Toda rota indexável tem title, description e canonical próprios. Nenhuma metadata contém `keywords`. Title e description refletem a intenção da rota e não enumeram cidades.
Open Graph usa `ImageResponse` e a convenção de arquivo do Next.js, com `1200×630`, `image/png`, alt específico e template visual compartilhado em `lib/og-card.tsx`. O template reutiliza preto, papel e verde ácido e apenas tipografia/texto; não introduz nova direção visual nem asset falso.
Arquivos por segmento:
- `app/opengraph-image.tsx` — marca/home;
- `app/servicos/opengraph-image.tsx` — hub de serviços;
- `app/servicos/[slug]/opengraph-image.tsx` — serviço selecionado pelos params;
- `app/cases/opengraph-image.tsx` — hub de cases;
- `app/cases/[slug]/opengraph-image.tsx` — case selecionado pelos params;
- `app/curriculo/opengraph-image.tsx` — perfil profissional;
- `app/privacidade/opengraph-image.tsx` — privacidade e consentimento.
Metadata dinâmica usa o mesmo conteúdo de `site-content.ts` que a página e a imagem; slug inválido retorna resposta 404 da imagem, sem imagem genérica enganosa. Twitter herda o mesmo card grande.
## Links recíprocos
- Cada cartão de case relacionado dentro de um serviço recebe link interno “Ler case: {título}” antes dos links externos de demo/código.
- Cada case lista os serviços encontrados por `getRelatedServices(caseId)` com anchors descritivos.
- O hub de cases mantém “Ler case”. O hub de serviços mantém “Abrir página”, sem link direto externo.
- Demo/GitHub continuam externos, com `rel="noreferrer"` ou `noopener noreferrer`.
- Estado de demo continua obedecendo `PortfolioProjectView`; SEO não reintroduz filtragem ou health fetch.
## Privacidade e consentimento
Criar `/privacidade` com metadata/canonical/OG próprios e conteúdo factual:
1. responsável e contato: Evandro Fonseca Junior, e-mail já publicado;
2. contato do formulário: os dados digitados são transformados em mensagem e abrem o WhatsApp; o site não possui endpoint próprio de armazenamento desse formulário;
3. analytics: antes de `granted`, nenhuma tag Google é carregada; a escolha fica em `localStorage` sob `evandro.analytics-consent.v1`; publicidade permanece negada; concessão pode criar cookies `_ga*`; revogação desativa novas coletas e expira cookies acessíveis conforme o hardening;
4. links externos: GitHub, LinkedIn, Instagram, demos e WhatsApp seguem políticas próprias;
5. alteração de preferência: usar o controle “Preferências de privacidade” entregue pelo hardening;
6. direitos/contato: solicitações são encaminhadas ao e-mail publicado;
7. data de vigência fixa: 2026-09-04.
A página não declara certificação jurídica, retenção que não foi configurada, compartilhamento que não foi verificado ou conformidade automática. O diálogo de consentimento e o Footer passam a ter link crawlable para `/privacidade`, usando estilos atuais.
## Documento operacional externo
A implementação cria `docs/seo-geo-operations.md` como runbook declarativo. Cada ação começa desmarcada e inclui evidência esperada, sem texto que sugira execução:
- Google Search Console: verificar propriedade, submeter sitemap, inspecionar as 17 URLs finais (as 16 atuais + privacidade), registrar canônica/cobertura e baseline de 90 dias, abrir relatórios de IA quando disponíveis na conta.
- Bing Webmaster Tools: verificar/importar propriedade, submeter sitemap, inspecionar URLs e registrar AI Performance; IndexNow fica fora até haver atualização frequente.
- Cloudflare: comparar `robots.ts` com resposta pública, Managed Robots, AI Crawl Control e WAF; registrar política separada de busca/citação e treino; conferir `200/403/429` em logs.
- Medição: p75 de LCP/INP/CLS por template em CrUX/RUM, consultas/landing/conversão, caminhos case→serviço→contato e citações reportadas pelas plataformas.
- Ações que exigem conta, publicação ou painel externo continuam humanas e consequenciais; o plano apenas documenta.
## Matriz de conflitos
| Área | Hardening/SEO envolvidos | Conflito | Regra |
| --- | --- | --- | --- |
| `app/layout.tsx` | Hardening Task 4; SEO entidades | Hardening achata providers sem tocar JSON-LD | SEO relê o arquivo final e altera apenas metadata/JSON-LD após Task 12. |
| `app/components/GoogleAnalytics.tsx` | Hardening Task 11; SEO privacidade | Consentimento ainda não existe no baseline atual | SEO consome a chave/estados finais e adiciona somente link/cópia coerente. |
| `app/components/Footer.tsx` | Hardening Task 9 ajusta testes; SEO adiciona privacidade | Teste e links podem mudar | Hardening primeiro; SEO preserva o contrato acessível final. |
| `lib/portfolio-data.ts`/JSON | Hardening Tasks 2–3; SEO cases | Tipo de projeto/health muda | SEO não altera o loader nem `PortfolioProjectView`; modifica apenas fatos dos três registros JSON. |
| `app/cases/[slug]/page.tsx` | SEO conteúdo/schema/metadata | Mesma rota em múltiplas preocupações | Uma única slice de case possui toda a rota; OG fica em arquivo irmão. |
| `app/servicos/[slug]/page.tsx` | SEO resposta/schema/links/CTA/metadata | Mesmo arquivo | Uma única slice de serviço possui toda a rota; OG fica em arquivo irmão. |
| `lib/site-content.ts` | Conteúdo factual, serviços e cases | Fonte compartilhada | Contrato e valores fecham na primeira tarefa; slices posteriores são somente consumidoras. |
| `lib/og-card.tsx` | Todas as imagens OG | Template compartilhado | Criar uma vez; arquivos de segmento só fornecem texto. |
| `app/sitemap.ts` | Frescor e nova rota privacidade | Task de entidades remove datas; privacidade adiciona URL | A task de privacidade adiciona a URL ao sitemap já sem datas; reler antes de editar. |
## Critérios observáveis de aceite
1. O gate completo do hardening está verde antes do primeiro arquivo de produção SEO ser alterado.
2. Duas execuções de `sitemap()` produzem a mesma saída e incluem 17 URLs canônicas sem `lastModified` artificial.
3. Nenhum HTML contém `ProfessionalService`, `LocalBusiness`, `FAQPage`, meta `keywords`, coordenada, preço ou endereço postal não comprovado.
4. O grafo raiz tem um `WebSite` e uma `Person`; `/curriculo` tem um `ProfilePage` que referencia a mesma pessoa; serviços têm `Service`; breadcrumbs têm posições contíguas e URLs canônicas.
5. Todas as rotas indexáveis têm title, description, canonical e OG coerentes; cada endpoint de imagem responde `200 image/png` em 1200×630 e alt específico.
6. Serviço → case e case → serviço são links internos reais com nome descritivo.
7. AS Serviços, NutriScan e Twins Lab exibem resposta, desafio, implementação, limites e fontes; nenhuma página atribui resultado comercial.
8. Demais cases não recebem profundidade genérica nem campos vazios.
9. A primeira descrição de cada serviço/case responde factualmente ao propósito da URL.
10. Texto regional usa base em Garuva, Norte de Santa Catarina e atuação remota, sem alegar escritórios ou presença por cidade.
11. CTAs de contratação levam a `/curriculo`; CTAs de consultoria levam a `/#contact`; ambos mantêm o sistema visual atual.
12. `/privacidade` descreve o consentimento final, está linkada pelo diálogo e Footer e não afirma ação/garantia jurídica inexistente.
13. O runbook externo registra ações e evidências esperadas sem marcar nenhuma como feita.
14. Desktop, mobile e reduced motion preservam direção visual, conteúdo, foco e fluxo de consentimento do hardening.
15. Jest focado, Biome, TypeScript, build e smoke em navegador encerram verdes.
## Riscos e mitigação
| Risco | Impacto | Mitigação |
| --- | --- | --- |
| Executar sobre hardening incompleto | Alto | Gate explícito e releitura dos arquivos finais compartilhados. |
| Duplicar ou divergir fatos entre JSON e `site-content.ts` | Alto | Atualizar somente os três cases na mesma tarefa e testar frases proibidas/links de fonte. |
| Fonte pública mudar | Médio | URLs com SHA fixo; tratar o case como retrato técnico daquela revisão. |
| Converter README promocional em fato | Alto | Usar somente código/manifests cruzados; registrar exclusões por case. |
| Schema válido sintaticamente, mas semanticamente incorreto | Alto | Grafo mínimo, propriedades visíveis e testes de IDs/referências/breadcrumbs. |
| OG aumentar duplicação | Médio | Um componente visual puro e arquivos de segmento mínimos. |
| Link de privacidade contradizer consentimento | Alto | Consumir literalmente chave/estados/comportamento final do hardening e testar juntos. |
| Texto regional virar doorway | Alto | Nenhuma nova rota de cidade; uma frase factual compartilhada. |
| CTA separar só a aparência | Médio | Destinos observáveis distintos: contratação `/curriculo`, consultoria `/#contact`. |
| Testes fixarem classes/copy incidental | Médio | Assertar rotas, links, schema, fontes e comportamentos; não classes ou wrappers. |
## Auto-revisão contra as 57 fontes
| Grupo do relatório | Quantidade | Como o design incorporou | Conflito evitado |
| --- | ---: | --- | --- |
| Google [G1–G13] | 13 | Conteúdo útil/original, canônicas, schema visível, sem frescor falso e medição em ferramentas próprias | Sem city-clone, escala artificial, schema especial de IA ou promessa de ranking. |
| Bing + Web Vitals [B1–B3, W1] | 4 | Resposta cedo, páginas focadas, evidência, runbook de AI Performance e p75 de campo | Sem IndexNow prematuro ou score de laboratório alegado como resultado. |
| Schema.org [S1–S6] | 6 | `Person`, `Service`, `ProfilePage`, `BreadcrumbList` mínimos e conectados | `ProfessionalService`, `LocalBusiness` e `FAQPage` excluídos. |
| Next.js [N1–N7] | 7 | Metadata por rota, `generateMetadata`, imagens por segmento e build estático | Sem nonce, pacote experimental ou alteração de CSP/analytics do hardening. |
| GEO/crawlers [A1–A2, C1–C4, L1] | 7 | Fontes visíveis e medição tratadas como experimento; política de bots só documentada | Sem `llms.txt`, allowlist executada ou causalidade de citação. |
| Mercado observado | 20 | Clareza de intenção, links, projetos e prova de primeira mão | Sem stuffing, endereço, testemunho, métrica ou autoridade inferida de concorrente. |
| **Total** | **57** | **Todos os grupos possuem decisão correspondente.** | **Nenhuma recomendação proibida foi reintroduzida.** |
A revisão também confirmou: oito slices bastam; não há dependência nova; os três cases têm fonte por campo técnico/factual, com exceção explícita apenas para o limite editorial de AS; ações externas não são executadas; e a direção visual permanece invariável.