Files

766 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.