Files
portifolio-evandrodevbr/docs/superpowers/plans/2026-09-04-seo-geo-growth.md
T

35 KiB
Raw Blame History

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:
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

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

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:

"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
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:

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
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
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:

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
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
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:

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
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:

<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

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

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
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:

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
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:

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:

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:

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
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
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:

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:

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