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,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.