35 KiB
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.mddeve 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, metakeywords,ProfessionalServiceouLocalBusinessnã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.jsone oglobals.cssnã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:
PortfolioProjectViewe 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, é exatamenteas-servicos,nutriscanetwins-lab; -
IDs de
sourcessão únicos dentro de cada case; -
toda
EvidenceSource.urlcorresponde ahttps://github.com/<owner>/<repo>/blob/<SHA de 40 hex>/<path>; -
answer,challenge, cada item deimplementatione os limites comparativos têm pelo menos umsourceId; -
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 exatoconsultoria-nextjs,integracao-hardware-software-iotearquitetura-de-software, rejeitando extras; -
todos os serviços têm
answer,ctaIntent: "consulting"ectaHref: "/#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
-
generateMetadatamantém title/description/canonical/OG/Twitter próprios e não usa keywords. -
JSON-LD usa
buildServiceJsonLd(service)ebuildBreadcrumbList. -
Hero mostra
service.answerimediatamente após H1;intropode continuar como explicação seguinte se não repetir. -
CTA primário usa rótulo factual de
service.ctaLabele/#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
Articleincompleto. 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 eOgCard. -
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:
- abrir
/,/servicos, os seis serviços,/cases, os seis cases,/curriculoe/privacidade; - verificar title/description/canonical/OG e parsear todo
script[type="application/ld+json"]comJSON.parse; - confirmar resposta factual perto do H1, links recíprocos e destinos de CTA;
- abrir as sete classes de OG (root, hubs, dinâmicos, currículo, privacidade) e validar
200 image/png, 1200×630; - testar menu/Footer, consentimento, foco, 404, mobile e reduced motion;
- 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.cssraiz, 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.