# 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///blob//`; - `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 `Ler case: título`; 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 {intentCtas.hiring.label} {intentCtas.consulting.label} ``` 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 (
{eyebrow}
{title}
{description}
EVANDRO.DEV.BR / ENGENHARIA DE SOFTWARE
); } ``` 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(, 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.