766 lines
35 KiB
Markdown
766 lines
35 KiB
Markdown
# SEO/GEO Growth Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current checkout. SEO starts only after the portfolio-hardening plan is fully complete and green. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Entregar SEO/GEO factual por rota, com entidades corretas, prova técnica em três cases, navegação recíproca, consentimento documentado e operação mensurável, sem alterar a direção visual.
|
||
|
||
**Architecture:** `lib/site-content.ts` continua sendo a fonte editorial; três cases ganham afirmações com fontes versionadas e serviços ganham resposta direta. `lib/seo.ts` centraliza IDs e builders mínimos de JSON-LD, enquanto metadata e imagens Open Graph permanecem colocalizadas nas rotas do App Router. O consentimento final do hardening é documentado em `/privacidade`; ações de Search Console, Bing e Cloudflare ficam em um runbook, sem automação nem alegação de execução.
|
||
|
||
**Tech Stack:** Next.js 16.3.0 App Router, React 19.1.0, TypeScript 5.9.2, Tailwind CSS 4.1.12, Jest 30.3.0, Testing Library, Biome 2.2.0, Node 22 e pnpm 9.15.9; nenhuma dependência nova.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-09-04-seo-geo-growth-design.md`
|
||
|
||
## Global Constraints
|
||
|
||
- O gate final de `docs/superpowers/plans/2026-09-04-portfolio-hardening.md` deve estar concluído antes da Task 1.
|
||
- Trabalhar sobre as interfaces finais do hardening; reler cada arquivo compartilhado antes de editá-lo.
|
||
- Não criar branch, commit, stash, reset, checkout ou worktree.
|
||
- Não adicionar dependência, CMS, backend, formulário, tracker, cidade-clone ou camada de compatibilidade.
|
||
- Não criar `llms.txt`, `FAQPage`, meta `keywords`, `ProfessionalService` ou `LocalBusiness` não comprovado.
|
||
- Não publicar métrica, review, cliente, endereço, coordenada, preço, resultado comercial ou presença física não verificados.
|
||
- Manter hero command-center, globo, starfield, notebook, paleta preto/papel/verde ácido, tipografia, grid, cartões, componentes e tokens `ds-*`.
|
||
- CTAs de contratação usam `/curriculo`; CTAs de consultoria usam `/#contact`; nenhum modal/layout novo.
|
||
- Base regional autorizada: “Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.”
|
||
- `data/project-health.json` e o `globals.css` não rastreado da raiz permanecem intocados.
|
||
- Testes validam comportamento, relações e dados observáveis; não fixam classes, wrappers ou texto irrelevante.
|
||
- Ações externas permanecem desmarcadas e documentadas; nenhuma conta/painel é alterado.
|
||
|
||
## Gate predecessor — obrigatório, fora da contagem de tarefas
|
||
|
||
- [ ] Ler o relatório final do hardening e confirmar que Tasks 1–12 terminaram.
|
||
- [ ] Confirmar que o manifesto final contém Node 22, pnpm 9.15.9, Next 16.3.0 e `typecheck`.
|
||
- [ ] Confirmar o último gate verde registrado:
|
||
|
||
```bash
|
||
pnpm install --frozen-lockfile
|
||
pnpm lint
|
||
pnpm typecheck
|
||
pnpm test -- --runInBand
|
||
pnpm build
|
||
pnpm audit --prod --audit-level=high
|
||
pnpm audit --dev --audit-level=high
|
||
```
|
||
|
||
- [ ] Se qualquer prova estiver ausente ou vermelha, encerrar esta execução antes de editar produção. Corrigir o predecessor no plano de hardening, não neste plano.
|
||
|
||
## Mapa de arquivos
|
||
|
||
### Contratos e fatos
|
||
|
||
- Modify: `lib/site-content.ts` — resposta de serviços, intenção de CTA, detalhes/fontes de três cases e relação inversa.
|
||
- Create: `lib/site-content.test.ts` — integridade das fontes e relações.
|
||
- Modify: `public/portfolio-data.json` — remover afirmações divergentes dos três registros escolhidos.
|
||
- Modify: `lib/portfolio-data.test.ts` — preservar o contrato final de projeto/health e verificar apenas fatos públicos essenciais.
|
||
|
||
### Entidades e frescor
|
||
|
||
- Create: `lib/seo.ts` — IDs e builders JSON-LD mínimos.
|
||
- Create: `lib/seo.test.ts` — relações entre entidades, breadcrumbs e sitemap determinístico.
|
||
- Modify: `app/layout.tsx`, `app/curriculo/page.tsx`, `app/sitemap.ts`.
|
||
|
||
### Serviços e cases
|
||
|
||
- Modify: `app/servicos/page.tsx`, `app/servicos/[slug]/page.tsx`.
|
||
- Create: `app/servicos/[slug]/page.test.tsx`.
|
||
- Modify: `app/cases/page.tsx`, `app/cases/[slug]/page.tsx`.
|
||
- Create: `app/cases/[slug]/page.test.tsx`.
|
||
|
||
### Open Graph
|
||
|
||
- Create: `lib/og-card.tsx`.
|
||
- Modify: `app/opengraph-image.tsx`.
|
||
- Create: `app/servicos/opengraph-image.tsx`, `app/servicos/[slug]/opengraph-image.tsx`.
|
||
- Create: `app/cases/opengraph-image.tsx`, `app/cases/[slug]/opengraph-image.tsx`.
|
||
- Create: `app/curriculo/opengraph-image.tsx`, `app/privacidade/opengraph-image.tsx`.
|
||
|
||
### Privacidade e operação
|
||
|
||
- Create: `app/privacidade/page.tsx`, `app/privacidade/page.test.tsx`.
|
||
- Modify: `app/components/GoogleAnalytics.tsx`, `app/components/GoogleAnalytics.test.tsx`.
|
||
- Modify: `app/components/Footer.tsx`, `app/components/Footer.test.tsx`.
|
||
- Create: `docs/seo-geo-operations.md`.
|
||
|
||
## Dependências e paralelização
|
||
|
||
```text
|
||
Hardening completo
|
||
├─ Task 1: contratos/fatos
|
||
│ ├─ Task 3: serviços
|
||
│ └─ Task 4: cases
|
||
└─ Task 2: entidades/frescor
|
||
├─ Task 3: serviços
|
||
├─ Task 4: cases
|
||
└─ Task 7: privacidade
|
||
|
||
Task 3 + Task 4
|
||
└─ Task 5: OG dinâmico/template
|
||
├─ Task 6: OG de hubs/currículo
|
||
└─ Task 7: OG/privacidade
|
||
|
||
Tasks 1–7
|
||
└─ Task 8: runbook e gate final
|
||
```
|
||
|
||
Após Tasks 1 e 2, Tasks 3 e 4 são slices independentes e podem ser executadas em paralelo somente se editores distintos não tocarem arquivos compartilhados. Task 5 fecha o contrato visual de imagem; Tasks 6 e 7 podem seguir em paralelo. Task 8 é serial.
|
||
|
||
## Matriz de conflitos
|
||
|
||
| Tarefas | Arquivo/interface | Conflito | Serialização |
|
||
| --- | --- | --- | --- |
|
||
| Hardening 4, SEO 2 | `app/layout.tsx` | Shell versus metadata/JSON-LD | Hardening completo; SEO relê e preserva o body final. |
|
||
| Hardening 11, SEO 7 | `GoogleAnalytics` e teste | Consentimento producer/consumer | SEO consome chave/estados finais e só acrescenta link/cópia. |
|
||
| Hardening 2–3, SEO 1 | JSON de portfólio e tipos finais | Health versus fatos editoriais | Não alterar loader/tipos de health; mudar somente três registros. |
|
||
| 1, 3, 4 | `lib/site-content.ts` | Contrato compartilhado | Task 1 fixa tudo; Tasks 3–4 não editam o arquivo. |
|
||
| 2, 3, 4, 7 | `lib/seo.ts` | Builders consumidos por rotas | Task 2 é provider; demais somente importam. |
|
||
| 2, 7 | `app/sitemap.ts`, `lib/seo.test.ts` | Privacidade aumenta 16→17 URLs | Task 2 remove frescor; Task 7 relê, adiciona rota e atualiza a expectativa. |
|
||
| 3, 5 | Conteúdo de serviço usado por OG | Copy/slug | Serviço fecha antes da imagem dinâmica. |
|
||
| 4, 5 | Conteúdo de case usado por OG | Copy/slug | Case fecha antes da imagem dinâmica. |
|
||
| 5, 6, 7 | `lib/og-card.tsx` | Template de imagem | Task 5 possui o template; Tasks 6–7 só fornecem props. |
|
||
| Hardening 9, SEO 7 | `Footer.tsx` e teste | Podas de teste versus link novo | SEO edita somente após o teste final do Footer existir. |
|
||
| 7, 8 | Privacidade/medição | Fato implementado versus documentação | Runbook descreve apenas o comportamento comprovado na Task 7. |
|
||
|
||
---
|
||
|
||
### Task 1: Fechar contrato factual, fontes e relações
|
||
|
||
**Files:**
|
||
- Modify: `lib/site-content.ts`
|
||
- Create: `lib/site-content.test.ts`
|
||
- Modify: `public/portfolio-data.json`
|
||
- Modify: `lib/portfolio-data.test.ts`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `PortfolioProjectView` e projeção de health finais, sem alterá-los.
|
||
- Produces: `intentCtas`, `CtaIntent`, `EvidenceSource`, `SourcedStatement`, `CaseStudyDetail`, `ServicePageContent.answer`, `ServicePageContent.ctaIntent`, `CaseStudyContent.detail`, `getRelatedServices(caseId)`.
|
||
|
||
- [ ] **Step 1: Confirmar referências antes de mudar exports**
|
||
|
||
Usar LSP em `ServicePageContent`, `CaseStudyContent`, `caseStudies`, `servicePages` e `getRelatedCases`. Registrar os callers; não renomear símbolos existentes.
|
||
|
||
- [ ] **Step 2: Escrever teste vermelho de integridade editorial**
|
||
|
||
Criar `lib/site-content.test.ts` com invariantes de consumidor:
|
||
|
||
O teste deve fechar invariantes estáveis, sem snapshot nem copy completa:
|
||
|
||
- o conjunto de cases com `detail`, ordenado apenas no teste, é exatamente `as-servicos`, `nutriscan` e `twins-lab`;
|
||
- IDs de `sources` são únicos dentro de cada case;
|
||
- toda `EvidenceSource.url` corresponde a `https://github.com/<owner>/<repo>/blob/<SHA de 40 hex>/<path>`;
|
||
- `answer`, `challenge`, cada item de `implementation` e os limites comparativos têm pelo menos um `sourceId`;
|
||
- o único limite sem fonte é o limite puramente editorial de AS Serviços, explicitamente `sourceIds: []`;
|
||
- todo ID presente resolve dentro de `detail.sources`;
|
||
- o boundary do NutriScan inclui `nutri-readme`;
|
||
- `getRelatedServices("twins-lab")` produz o conjunto exato `consultoria-nextjs`, `integracao-hardware-software-iot` e `arquitetura-de-software`, rejeitando extras;
|
||
- todos os serviços têm `answer`, `ctaIntent: "consulting"` e `ctaHref: "/#contact"`.
|
||
|
||
- [ ] **Step 3: Rodar o teste vermelho**
|
||
|
||
```bash
|
||
pnpm exec jest lib/site-content.test.ts --runInBand
|
||
```
|
||
|
||
Expected: falha por interfaces/campos/helper ainda ausentes.
|
||
|
||
- [ ] **Step 4: Implementar os tipos e os três detalhes aprovados**
|
||
|
||
Copiar os textos e URLs SHA-pinned exatamente da seção “Cases escolhidos e fonte de cada campo” do spec. IDs de fonte locais e estáveis:
|
||
|
||
```ts
|
||
"as-readme" | "as-package" | "as-engine" | "as-canvas"
|
||
"nutri-search" | "nutri-barcode" | "nutri-wrapper" | "nutri-package" | "nutri-prisma" | "nutri-readme"
|
||
"twins-package" | "twins-stream" | "twins-simulator" | "twins-readme"
|
||
```
|
||
|
||
Ruling de proveniência: não criar fonte fictícia ou circular para política editorial. O boundary puramente editorial de AS usa `sourceIds: []`; afirmações técnicas/factuais e boundaries comparativos exigem fonte externa, e qualquer ID presente precisa resolver.
|
||
|
||
`getRelatedServices` filtra `servicePages` e não cria mapa duplicado.
|
||
|
||
Para cada serviço, `answer` resume oferta, entregável e limite usando apenas conteúdo já presente; substituir enumeração de cidades pela frase regional autorizada onde houver. `ctaIntent` é sempre `consulting`; rótulos usam “Solicitar consultoria”, “Conversar sobre o projeto” ou “Solicitar diagnóstico” conforme o serviço, todos com `/#contact`.
|
||
|
||
- [ ] **Step 5: Corrigir os três registros no JSON sem tocar health**
|
||
|
||
Em AS Serviços, manter fatos confirmados pelo código. Em NutriScan, remover versão fixa, PWA, MongoDB/histórico, scanner, produção e resultados de saúde não cruzados; manter busca por nome/código, cache JSON e Open Food Facts. Em Twins Lab, trocar WebSockets por SSE, remover OpenRouter/sub-segundo/produção/análise preditiva como fato atual; manter autenticação, simulador, MongoDB e stream a cada dois segundos.
|
||
|
||
Não alterar `featured`, `demoUrl`, `githubUrl`, `id` nem campos `demoAvailability` produzidos pelo loader final.
|
||
|
||
- [ ] **Step 6: Ajustar o teste final de portfólio somente para o contrato factual**
|
||
|
||
Serializar com `JSON.stringify` o registro público inteiro de cada um dos três cases e rejeitar termos específicos já removidos em `subtitle`, `description`, `longDescription`, `technologies`, `features`, `status` e `badges`. Manter apenas asserts positivos curtos para `Open Food Facts`, `SSE` e `Three.js`; não usar snapshot, frase completa, classe ou métrica interna.
|
||
|
||
- [ ] **Step 7: Checkpoint da fonte editorial**
|
||
|
||
```bash
|
||
pnpm exec jest lib/site-content.test.ts lib/portfolio-data.test.ts --runInBand
|
||
pnpm exec biome check lib/site-content.ts lib/site-content.test.ts public/portfolio-data.json lib/portfolio-data.test.ts
|
||
pnpm typecheck
|
||
```
|
||
|
||
Expected: zero; o contrato de proveniência distingue fonte externa de limite editorial, os três cases têm IDs/URLs válidos e todos os serviços têm resposta/CTA; health final permanece verde.
|
||
|
||
---
|
||
|
||
### Task 2: Tornar entidades mínimas e frescor determinístico
|
||
|
||
**Files:**
|
||
- Create: `lib/seo.ts`
|
||
- Create: `lib/seo.test.ts`
|
||
- Modify: `app/layout.tsx`
|
||
- Modify: `app/curriculo/page.tsx`
|
||
- Modify: `app/sitemap.ts`
|
||
|
||
**Interfaces:**
|
||
- Produces: `SEO_IDS`, `buildWebSiteJsonLd()`, `buildPersonJsonLd()`, `buildProfilePageJsonLd()`, `buildServiceJsonLd(service)`, `buildBreadcrumbList(pageUrl, items)`.
|
||
- Does not own: visual body, consent, OG renderer ou conteúdo de cases.
|
||
|
||
- [ ] **Step 1: Escrever testes vermelhos de entidade e frescor**
|
||
|
||
`lib/seo.test.ts` importa os builders e `sitemap`:
|
||
|
||
```ts
|
||
it("liga site, perfil e serviço à mesma pessoa", () => {
|
||
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
|
||
expect(buildProfilePageJsonLd().mainEntity).toEqual({ "@id": SEO_IDS.person });
|
||
expect(buildServiceJsonLd(servicePages[0]).provider).toEqual({
|
||
"@id": SEO_IDS.person,
|
||
});
|
||
});
|
||
|
||
it("não publica localização ou oferta não comprovada", () => {
|
||
const serialized = JSON.stringify([
|
||
buildPersonJsonLd(),
|
||
buildServiceJsonLd(servicePages[0]),
|
||
]);
|
||
expect(serialized).not.toMatch(/ProfessionalService|LocalBusiness|GeoCoordinates|PostalAddress|priceRange|OfferCatalog/);
|
||
});
|
||
|
||
it("gera breadcrumb canônico e contíguo", () => {
|
||
const breadcrumb = buildBreadcrumbList(`${siteUrl}/servicos/x`, [
|
||
["Início", siteUrl],
|
||
["Serviços", `${siteUrl}/servicos`],
|
||
["X", `${siteUrl}/servicos/x`],
|
||
]);
|
||
expect(breadcrumb.itemListElement.map(({ position }) => position)).toEqual([1, 2, 3]);
|
||
});
|
||
|
||
it("mantém sitemap determinístico e sem frescor fabricado", () => {
|
||
expect(sitemap()).toEqual(sitemap());
|
||
expect(sitemap()).toHaveLength(16);
|
||
expect(sitemap().every((entry) => !("lastModified" in entry))).toBe(true);
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Rodar o teste vermelho**
|
||
|
||
```bash
|
||
pnpm exec jest lib/seo.test.ts --runInBand
|
||
```
|
||
|
||
Expected: falha porque builders não existem e sitemap contém `new Date()`.
|
||
|
||
- [ ] **Step 3: Implementar builders mínimos**
|
||
|
||
`SEO_IDS` usa os valores do spec. `buildPersonJsonLd()` publica nome, URL, `/edev.jpg`, GitHub, LinkedIn e `homeLocation` nomeado Garuva; não publica telefone, Instagram divergente, endereço ou geo. `buildWebSiteJsonLd()` publica `publisher` por `@id`. `buildProfilePageJsonLd()` usa `isPartOf`/`mainEntity` por `@id`, sem data. `buildServiceJsonLd()` usa nome/description/URL/provider e `areaServed` somente Garuva e Norte de Santa Catarina.
|
||
|
||
`buildBreadcrumbList` recebe tuplas readonly `[name, url]`, atribui posições a partir de 1 e define `@id` como `${pageUrl}#breadcrumb`.
|
||
|
||
- [ ] **Step 4: Migrar layout e currículo sem tocar o shell final**
|
||
|
||
Em `app/layout.tsx`, remover `keywords`; substituir o grafo pelo resultado de `buildWebSiteJsonLd()` e `buildPersonJsonLd()`. Preservar body exatamente como entregue pelo hardening.
|
||
|
||
Em `/curriculo`, usar `buildProfilePageJsonLd()` e `buildBreadcrumbList`; remover `new Date().toISOString()`. Metadata usa canonical absoluto, descrição factual e caminho de contratação. A primeira cópia visível usa a frase regional autorizada sem prometer disponibilidade.
|
||
|
||
- [ ] **Step 5: Remover hints temporais sem fonte**
|
||
|
||
`app/sitemap.ts` retorna apenas `{ url }` para as 16 rotas existentes. Remover `new Date`, `lastModified`, `changeFrequency` e `priority`.
|
||
|
||
- [ ] **Step 6: Checkpoint de entidade/frescor**
|
||
|
||
```bash
|
||
pnpm exec jest lib/seo.test.ts --runInBand
|
||
pnpm exec biome check lib/seo.ts lib/seo.test.ts app/layout.tsx app/curriculo/page.tsx app/sitemap.ts
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
Expected: zero; build preserva as rotas estáticas; o teste não encontra tipos/propriedades proibidos.
|
||
|
||
---
|
||
|
||
### Task 3: Entregar a slice completa de serviços
|
||
|
||
**Files:**
|
||
- Modify: `app/servicos/page.tsx`
|
||
- Modify: `app/servicos/[slug]/page.tsx`
|
||
- Create: `app/servicos/[slug]/page.test.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: conteúdo fechado na Task 1 e builders da Task 2.
|
||
- Produces: resposta factual cedo, `Service`/breadcrumb corretos, CTA consultoria e links internos serviço→case.
|
||
|
||
- [ ] **Step 1: Escrever teste vermelho de um serviço representativo**
|
||
|
||
Mockar somente `next/navigation`, `HeroBackground` e `MagneticButton`; renderizar o JSX retornado pela página de `consultoria-nextjs`:
|
||
|
||
```tsx
|
||
const ui = await ServicePage({
|
||
params: Promise.resolve({ slug: "consultoria-nextjs" }),
|
||
});
|
||
render(ui);
|
||
|
||
expect(screen.getByRole("heading", { level: 1 })).toBeVisible();
|
||
expect(screen.getByText(getServicePage("consultoria-nextjs")!.answer)).toBeVisible();
|
||
expect(screen.getByRole("link", { name: /solicitar (consultoria|diagnóstico)/i }))
|
||
.toHaveAttribute("href", "/#contact");
|
||
expect(screen.getByRole("link", { name: /ler case:/i }))
|
||
.toHaveAttribute("href", expect.stringMatching(/^\/cases\//));
|
||
```
|
||
|
||
Ler o script JSON-LD pelo seletor e provar `Service.provider.@id === SEO_IDS.person`, ausência de `ProfessionalService`/`FAQPage` e breadcrumb 1–3.
|
||
|
||
- [ ] **Step 2: Rodar o teste vermelho**
|
||
|
||
```bash
|
||
pnpm exec jest 'app/servicos/[slug]/page.test.tsx' --runInBand
|
||
```
|
||
|
||
Expected: falha por `answer`, provider por `@id` e link interno ainda ausentes.
|
||
|
||
- [ ] **Step 3: Migrar a rota dinâmica inteira**
|
||
|
||
- `generateMetadata` mantém title/description/canonical/OG/Twitter próprios e não usa keywords.
|
||
- JSON-LD usa `buildServiceJsonLd(service)` e `buildBreadcrumbList`.
|
||
- Hero mostra `service.answer` imediatamente após H1; `intro` pode continuar como explicação seguinte se não repetir.
|
||
- CTA primário usa rótulo factual de `service.ctaLabel` e `/#contact`.
|
||
- Cada case relacionado recebe `<Link href={/cases/id}>Ler case: título</Link>`; demo/código permanecem externos.
|
||
- FAQ visível continua como conteúdo comum, sem JSON-LD de FAQ.
|
||
|
||
- [ ] **Step 4: Corrigir hub de serviços**
|
||
|
||
Usar `buildBreadcrumbList`; preservar CollectionPage/ItemList apenas com itens visíveis. Ajustar a descrição inicial para resposta factual e manter cartões/estilos. Hub aponta para serviços, não para formulário externo.
|
||
|
||
- [ ] **Step 5: Checkpoint de serviços**
|
||
|
||
```bash
|
||
pnpm exec jest 'app/servicos/[slug]/page.test.tsx' lib/site-content.test.ts --runInBand
|
||
pnpm exec biome check app/servicos/page.tsx 'app/servicos/[slug]/page.tsx' 'app/servicos/[slug]/page.test.tsx'
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
Expected: zero; as seis páginas continuam estáticas e cada case relacionado tem link interno.
|
||
|
||
---
|
||
|
||
### Task 4: Entregar a slice completa dos três cases aprofundados
|
||
|
||
**Files:**
|
||
- Modify: `app/cases/page.tsx`
|
||
- Modify: `app/cases/[slug]/page.tsx`
|
||
- Create: `app/cases/[slug]/page.test.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `CaseStudyDetail`, `getRelatedServices`, builders e IDs.
|
||
- Produces: conteúdo aprofundado com fonte, links case→serviço e CTAs separados.
|
||
|
||
- [ ] **Step 1: Escrever teste vermelho da página aprofundada e fallback**
|
||
|
||
Renderizar `as-servicos` e `gcc-installer` para provar os dois caminhos:
|
||
|
||
```tsx
|
||
const detailed = await CasePage({
|
||
params: Promise.resolve({ slug: "as-servicos" }),
|
||
});
|
||
render(detailed);
|
||
expect(screen.getByText(caseStudies.find(({ id }) => id === "as-servicos")!.detail!.answer.text))
|
||
.toBeVisible();
|
||
expect(screen.getByRole("heading", { name: "Decisões de implementação" })).toBeVisible();
|
||
expect(screen.getAllByRole("link", { name: /fonte:/i }).length).toBeGreaterThan(0);
|
||
expect(screen.getByRole("link", { name: "Avaliar perfil para contratação" }))
|
||
.toHaveAttribute("href", "/curriculo");
|
||
expect(screen.getByRole("link", { name: "Solicitar consultoria técnica" }))
|
||
.toHaveAttribute("href", "/#contact");
|
||
```
|
||
|
||
Após cleanup, renderizar `gcc-installer` e confirmar que summary/stack/links existentes aparecem sem headings vazios de aprofundamento.
|
||
|
||
- [ ] **Step 2: Rodar o teste vermelho**
|
||
|
||
```bash
|
||
pnpm exec jest 'app/cases/[slug]/page.test.tsx' --runInBand
|
||
```
|
||
|
||
Expected: falha por seções/fontes/links de serviço/CTA consultoria ainda ausentes.
|
||
|
||
- [ ] **Step 3: Renderizar aprofundamento sem inferência**
|
||
|
||
Para `detail` presente, a ordem é: resposta no hero → “Desafio verificável” → “Decisões de implementação” → “Limites desta evidência” → “Fontes”. Cada fonte é link externo com label e SHA. Para `detail` ausente, não renderizar essas seções.
|
||
|
||
Substituir o par genérico currículo/serviços por:
|
||
|
||
```tsx
|
||
<Link href={intentCtas.hiring.href}>{intentCtas.hiring.label}</Link>
|
||
<Link href={intentCtas.consulting.href}>
|
||
{intentCtas.consulting.label}
|
||
</Link>
|
||
```
|
||
|
||
Listar `getRelatedServices(caseStudy.id)` com links `/servicos/{slug}` e nomes descritivos.
|
||
|
||
- [ ] **Step 4: Corrigir metadata/schema e hub**
|
||
|
||
- Metadata de cada case usa summary factual, canonical, OG/Twitter próprios e sem keywords.
|
||
- Remover `Article` incompleto. Manter breadcrumb por builder; nenhuma data/imagem/autoria editorial é inventada.
|
||
- Hub usa breadcrumb builder e preserva ItemList consistente com cartões visíveis.
|
||
- Hub separa CTA “Avaliar perfil para contratação” (`/curriculo`) e “Solicitar consultoria técnica” (`/#contact`) usando classes atuais.
|
||
|
||
- [ ] **Step 5: Checkpoint dos cases**
|
||
|
||
```bash
|
||
pnpm exec jest 'app/cases/[slug]/page.test.tsx' lib/site-content.test.ts lib/portfolio-data.test.ts --runInBand
|
||
pnpm exec biome check app/cases/page.tsx 'app/cases/[slug]/page.tsx' 'app/cases/[slug]/page.test.tsx'
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
Expected: zero; três cases aprofundados, três no fallback simples, nenhum resultado comercial alegado.
|
||
|
||
---
|
||
|
||
### Task 5: Criar template e OG dinâmico de serviço/case
|
||
|
||
**Files:**
|
||
- Create: `lib/og-card.tsx`
|
||
- Modify: `app/opengraph-image.tsx`
|
||
- Create: `app/servicos/[slug]/opengraph-image.tsx`
|
||
- Create: `app/cases/[slug]/opengraph-image.tsx`
|
||
|
||
**Interfaces:**
|
||
- Produces: `OG_SIZE`, `OG_CONTENT_TYPE`, `OgCard({ eyebrow, title, description })`.
|
||
- Consumes: slugs e conteúdo fechados nas Tasks 1, 3 e 4.
|
||
|
||
- [ ] **Step 1: Implementar componente visual puro**
|
||
|
||
```tsx
|
||
export const OG_SIZE = { width: 1200, height: 630 } as const;
|
||
export const OG_CONTENT_TYPE = "image/png";
|
||
|
||
export function OgCard({
|
||
eyebrow,
|
||
title,
|
||
description,
|
||
}: {
|
||
eyebrow: string;
|
||
title: string;
|
||
description: string;
|
||
}) {
|
||
return (
|
||
<div
|
||
style={{
|
||
width: "100%",
|
||
height: "100%",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
justifyContent: "space-between",
|
||
background: "#050505",
|
||
color: "#f4f1e8",
|
||
padding: 72,
|
||
fontFamily: "Arial, sans-serif",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
color: "#ccff00",
|
||
fontSize: 24,
|
||
letterSpacing: "0.16em",
|
||
textTransform: "uppercase",
|
||
}}
|
||
>
|
||
{eyebrow}
|
||
</div>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
|
||
<div style={{ display: "flex", fontSize: 64, fontWeight: 700 }}>
|
||
{title}
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
maxWidth: 980,
|
||
color: "#c7c3ba",
|
||
fontSize: 28,
|
||
lineHeight: 1.35,
|
||
}}
|
||
>
|
||
{description}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
borderTop: "1px solid #343434",
|
||
paddingTop: 24,
|
||
fontSize: 20,
|
||
letterSpacing: "0.12em",
|
||
}}
|
||
>
|
||
EVANDRO.DEV.BR / ENGENHARIA DE SOFTWARE
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
Usar estilos inline aceitos por `ImageResponse`; nenhuma imagem gerada/foto/claim novo. Migrar a imagem raiz para `OgCard` com copy de marca.
|
||
|
||
- [ ] **Step 2: Criar imagem de serviço por params**
|
||
|
||
Exportar `generateImageMetadata({ params })` com um item contendo `id`, `size: OG_SIZE`, `contentType` e alt `“{navLabel} — Evandro Fonseca Junior”`. A função default recebe `params` e `id` como promises no Next 16, resolve serviço e retorna `new ImageResponse(<OgCard eyebrow={service.eyebrow} title={service.headline} description={service.answer} />, OG_SIZE)`. Slug inválido usa `notFound()`.
|
||
|
||
- [ ] **Step 3: Criar imagem de case por params**
|
||
|
||
Mesma interface, com alt `“Case {title} — Evandro Fonseca Junior”`, eyebrow “Case de engenharia de software”, title e summary factuais. Slug inválido usa `notFound()`.
|
||
|
||
- [ ] **Step 4: Verificar build e respostas reais**
|
||
|
||
```bash
|
||
pnpm exec biome check lib/og-card.tsx app/opengraph-image.tsx 'app/servicos/[slug]/opengraph-image.tsx' 'app/cases/[slug]/opengraph-image.tsx'
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
Iniciar o build por processo supervisionado:
|
||
|
||
```bash
|
||
pnpm start
|
||
```
|
||
|
||
No browser/rede, abrir uma página de serviço e AS Serviços. Obter as URLs `og:image` do head e confirmar para cada resposta: status `200`, `content-type: image/png`, imagem 1200×630 e texto/alt correspondente à rota. Encerrar o processo supervisionado.
|
||
|
||
---
|
||
|
||
### Task 6: Completar OG de hubs e currículo
|
||
|
||
**Files:**
|
||
- Create: `app/servicos/opengraph-image.tsx`
|
||
- Create: `app/cases/opengraph-image.tsx`
|
||
- Create: `app/curriculo/opengraph-image.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `OgCard`, `OG_SIZE`, `OG_CONTENT_TYPE`.
|
||
- Produces: imagem específica para cada segmento estático.
|
||
|
||
- [ ] **Step 1: Criar as três imagens estáticas**
|
||
|
||
Cada arquivo exporta `alt`, `size`, `contentType` e retorna `ImageResponse`:
|
||
|
||
| Segmento | Eyebrow | Title | Descrição curta |
|
||
| --- | --- | --- | --- |
|
||
| `/servicos` | Serviços | Engenharia de software e consultoria técnica | Base em Garuva, Norte de Santa Catarina e remoto. |
|
||
| `/cases` | Trabalho verificável | Cases de software, web e sistemas conectados | Decisões, implementação, limites e fontes. |
|
||
| `/curriculo` | Perfil profissional | Evandro Fonseca Junior | Engenharia da computação e desenvolvimento de software. |
|
||
|
||
- [ ] **Step 2: Verificar metadata herdada por segmento**
|
||
|
||
```bash
|
||
pnpm exec biome check app/servicos/opengraph-image.tsx app/cases/opengraph-image.tsx app/curriculo/opengraph-image.tsx
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
No build iniciado com `pnpm start`, abrir `/servicos`, `/cases` e `/curriculo`, conferir uma `og:image` distinta por segmento, `200 image/png`, 1200×630 e alt específico. Encerrar o processo.
|
||
|
||
---
|
||
|
||
### Task 7: Publicar privacidade coerente e integrar consentimento
|
||
|
||
**Files:**
|
||
- Create: `app/privacidade/page.tsx`
|
||
- Create: `app/privacidade/page.test.tsx`
|
||
- Create: `app/privacidade/opengraph-image.tsx`
|
||
- Modify: `app/components/GoogleAnalytics.tsx`
|
||
- Modify: `app/components/GoogleAnalytics.test.tsx`
|
||
- Modify: `app/components/Footer.tsx`
|
||
- Modify: `app/components/Footer.test.tsx`
|
||
- Modify: `app/sitemap.ts`
|
||
- Modify: `lib/seo.test.ts`
|
||
|
||
**Interfaces:**
|
||
- Consumes: consentimento final `evandro.analytics-consent.v1`, `granted | denied`, revogação e `OgCard`.
|
||
- Produces: `/privacidade`, links crawlable e sitemap de 17 URLs.
|
||
|
||
- [ ] **Step 1: Estender testes de consentimento e Footer**
|
||
|
||
No teste final de `GoogleAnalytics`, adicionar:
|
||
|
||
```tsx
|
||
expect(screen.getByRole("link", { name: "Ler política de privacidade" }))
|
||
.toHaveAttribute("href", "/privacidade");
|
||
```
|
||
|
||
A asserção roda no estado sem escolha e na tela de preferências. No teste do Footer:
|
||
|
||
```tsx
|
||
expect(screen.getByRole("link", { name: "Privacidade" }))
|
||
.toHaveAttribute("href", "/privacidade");
|
||
```
|
||
|
||
Não alterar as provas existentes de zero request antes de consentimento, aceite, recusa e revogação.
|
||
|
||
- [ ] **Step 2: Escrever teste vermelho da página**
|
||
|
||
Renderizar `PrivacyPage` e provar conteúdo observável:
|
||
|
||
```tsx
|
||
expect(screen.getByRole("heading", { level: 1, name: /privacidade/i })).toBeVisible();
|
||
expect(screen.getByText(/evandro\.analytics-consent\.v1/)).toBeVisible();
|
||
expect(screen.getByText(/WhatsApp/)).toBeVisible();
|
||
expect(screen.getByText(/nenhuma tag do Google é carregada/i)).toBeVisible();
|
||
expect(screen.getByText(/2026-09-04/)).toBeVisible();
|
||
```
|
||
|
||
Ler o JSON-LD e confirmar breadcrumb Início→Privacidade; não adicionar entidade de negócio.
|
||
|
||
- [ ] **Step 3: Rodar testes vermelhos**
|
||
|
||
```bash
|
||
pnpm exec jest app/components/GoogleAnalytics.test.tsx app/components/Footer.test.tsx app/privacidade/page.test.tsx --runInBand
|
||
```
|
||
|
||
Expected: falha pelos links/rota ainda ausentes; os testes de consentimento anteriores continuam compilando.
|
||
|
||
- [ ] **Step 4: Implementar a página com o conteúdo fechado no spec**
|
||
|
||
Usar `main`, `ds-section`, `ds-section-shell`, headings, cards e links existentes. Incluir metadata title/description/canonical e breadcrumb. Não criar banner/CMP novo. Não afirmar retenção, venda, certificação legal ou integração não presente.
|
||
|
||
- [ ] **Step 5: Integrar links sem redesenhar**
|
||
|
||
Adicionar link “Ler política de privacidade” ao diálogo/preferências já existentes em `GoogleAnalytics`. Adicionar “Privacidade” a `quickLinks` do Footer. Preservar todos os nomes acessíveis e estados do hardening.
|
||
|
||
- [ ] **Step 6: Adicionar OG e sitemap**
|
||
|
||
`app/privacidade/opengraph-image.tsx` usa `OgCard` com título “Privacidade e consentimento”. Adicionar `${siteUrl}/privacidade` ao sitemap, ainda sem datas/hints. Atualizar `lib/seo.test.ts` para 17 URLs e presença única da rota.
|
||
|
||
- [ ] **Step 7: Checkpoint automatizado e browser**
|
||
|
||
```bash
|
||
pnpm exec jest app/components/GoogleAnalytics.test.tsx app/components/Footer.test.tsx app/privacidade/page.test.tsx lib/seo.test.ts --runInBand
|
||
pnpm exec biome check app/privacidade app/components/GoogleAnalytics.tsx app/components/GoogleAnalytics.test.tsx app/components/Footer.tsx app/components/Footer.test.tsx app/sitemap.ts lib/seo.test.ts
|
||
pnpm typecheck
|
||
pnpm build
|
||
```
|
||
|
||
No browser de produção local: limpar consentimento/cookies; abrir `/privacidade`; navegar pelo link do Footer; voltar, abrir preferências, seguir o link de política; confirmar zero request Google antes de escolha e que revogação continua funcionando. Conferir foco, mobile e reduced motion sem alteração visual estrutural.
|
||
|
||
---
|
||
|
||
### Task 8: Documentar ações externas e fechar com prova integral
|
||
|
||
**Files:**
|
||
- Create: `docs/seo-geo-operations.md`
|
||
- Verify only: todos os arquivos das Tasks 1–7
|
||
|
||
**Interfaces:**
|
||
- Consumes: 17 URLs finais, consentimento final, sitemap e fontes do spec.
|
||
- Produces: runbook externo declarativo e evidência final.
|
||
|
||
- [ ] **Step 1: Criar runbook de Search Console**
|
||
|
||
Registrar ações desmarcadas com evidência exigida:
|
||
|
||
- verificação de propriedade;
|
||
- submissão de `https://evandro.dev.br/sitemap.xml`;
|
||
- inspeção das 17 URLs e canônica escolhida;
|
||
- baseline de 90 dias por consulta/página/dispositivo/localidade;
|
||
- relatório de IA generativa e configuração de inclusão, se disponíveis na conta.
|
||
|
||
Escrever explicitamente “Estado: não executado neste repositório” no cabeçalho.
|
||
|
||
- [ ] **Step 2: Registrar Bing, Cloudflare e medição**
|
||
|
||
Bing: propriedade, sitemap, inspeção, AI Performance; IndexNow permanece excluído até haver publicação frequente. Cloudflare: comparar resposta pública e `robots.ts`, Managed Robots, AI Crawl Control, WAF e logs `200/403/429`; separar crawlers de busca/citação de treino. Medição: p75 LCP/INP/CLS por template, consultas, landing, caminhos case→serviço→contato e conversão.
|
||
|
||
Nenhuma caixa é marcada, nenhum valor é preenchido com estimativa e nenhuma credencial/comando de painel é incluído.
|
||
|
||
- [ ] **Step 3: Auto-revisar contra os 57 itens-fonte**
|
||
|
||
Usar a matriz do spec e confirmar por grupo: Google 13; Bing/Web Vitals 4; Schema 6; Next 7; GEO/crawlers 7; mercado 20. Verificar que nenhuma implementação adicionou city page, conteúdo escalado, `llms.txt`, FAQ schema, keyword metadata, entidade local não comprovada, claim ou data artificial.
|
||
|
||
Executar varredura textual focada:
|
||
|
||
```bash
|
||
pnpm exec biome check app lib docs/seo-geo-operations.md
|
||
```
|
||
|
||
Usar a ferramenta de busca do harness em `app`, `lib`, `public/portfolio-data.json` e no runbook para:
|
||
|
||
```text
|
||
ProfessionalService|LocalBusiness|FAQPage|keywords:|llms\.txt|priceRange|GeoCoordinates|PostalAddress
|
||
```
|
||
|
||
Expected: nenhum resultado em produção. Menções históricas permanecem apenas no spec/pesquisa como proibições documentadas.
|
||
|
||
- [ ] **Step 4: Rodar gate completo uma vez**
|
||
|
||
```bash
|
||
pnpm install --frozen-lockfile
|
||
pnpm lint
|
||
pnpm typecheck
|
||
pnpm test -- --runInBand
|
||
pnpm build
|
||
pnpm audit --prod --audit-level=high
|
||
pnpm audit --dev --audit-level=high
|
||
```
|
||
|
||
Expected: todos código zero; install não altera lock; build lista 17 rotas indexáveis e os endpoints de imagem esperados.
|
||
|
||
- [ ] **Step 5: Smoke real em desktop/mobile/reduced motion**
|
||
|
||
Com `pnpm start` em processo supervisionado, usar navegador real:
|
||
|
||
1. abrir `/`, `/servicos`, os seis serviços, `/cases`, os seis cases, `/curriculo` e `/privacidade`;
|
||
2. verificar title/description/canonical/OG e parsear todo `script[type="application/ld+json"]` com `JSON.parse`;
|
||
3. confirmar resposta factual perto do H1, links recíprocos e destinos de CTA;
|
||
4. abrir as sete classes de OG (root, hubs, dinâmicos, currículo, privacidade) e validar `200 image/png`, 1200×630;
|
||
5. testar menu/Footer, consentimento, foco, 404, mobile e reduced motion;
|
||
6. confirmar console sem erro/hidratação e nenhuma request Google antes de consentimento.
|
||
|
||
Encerrar o processo supervisionado.
|
||
|
||
- [ ] **Step 6: Conferir corte limpo**
|
||
|
||
Confirmar por leitura:
|
||
|
||
- três cases aprofundados e somente eles;
|
||
- cada afirmação técnica/factual resolve para fonte SHA-pinned; somente o limite editorial de AS usa `sourceIds: []`;
|
||
- 17 URLs únicas no sitemap, sem datas artificiais;
|
||
- nenhum loader/health/interface do hardening foi alterado;
|
||
- `data/project-health.json`, `globals.css` raiz, lock e dependências estão inalterados por SEO;
|
||
- runbook não alega execução externa;
|
||
- direção visual existente está preservada.
|
||
|
||
## Critérios de saída por slice
|
||
|
||
| Slice | Observável independente |
|
||
| --- | --- |
|
||
| 1. Fatos | Dados compilam; três cases têm proveniência válida com exceção editorial explícita; relações inversas derivam da lista existente. |
|
||
| 2. Entidades/frescor | IDs são únicos/referenciados; sitemap determinístico; zero tipos/propriedades proibidos. |
|
||
| 3. Serviços | Seis rotas com resposta cedo, Service/breadcrumb, CTA consultoria e link para case. |
|
||
| 4. Cases | Três aprofundados com fonte, três sem preenchimento genérico, links para serviços e dois CTAs distintos. |
|
||
| 5. OG dinâmico | Home, serviço e case retornam imagem correta por conteúdo/slug. |
|
||
| 6. OG estático | Hubs e currículo não herdam card genérico. |
|
||
| 7. Privacidade | Rota, consentimento, Footer, sitemap e OG concordam; coleta opt-in continua comprovada. |
|
||
| 8. Operação | Runbook declarativo, 57 fontes reconciliadas e gate/smoke completos verdes. |
|
||
|
||
## Auto-revisão do plano
|
||
|
||
- **Cobertura:** todos os requisitos do spec aparecem em uma task e em critério observável.
|
||
- **Tamanho:** oito tarefas; nenhuma cria subsistema, dependência ou página por cidade.
|
||
- **Interfaces:** Tasks 1–2 são providers; 3–4 consumidores independentes; 5 fornece OG; 6–7 consomem; 8 integra.
|
||
- **Conflitos:** todos os arquivos compartilhados com hardening ou entre tasks têm ordem explícita.
|
||
- **Fontes:** AS Serviços, NutriScan e Twins Lab têm fonte SHA-pinned por novo campo e limites editoriais expressos.
|
||
- **Verificação:** cada slice tem comando real; mudanças de rota/OG/consentimento têm smoke no produto real.
|
||
- **Restrições:** sem commit, dependência, redesign, city-clone, schema proibido, dado inventado ou ação externa alegada.
|
||
- **57 fontes:** a revisão segue a contagem 13 + 4 + 6 + 7 + 7 + 20 e preserva as conclusões da pesquisa.
|
||
|
||
A execução termina apenas após a Task 8. Este documento não autoriza nenhuma alteração de produção antes do hardening completo.
|