# F5.I1.T1 — services/Seo headless + <x-service-seo::tags> + @graph JSON-LD

| Campo | Valor |
|---|---|
| **Fase** | F5 — Borda |
| **Issue** | F5.I1 — SEO e performance (plano 14) |
| **kd task** | `task_00ot6qc7` (`closed`) |
| **Âncora** | `wiki/implementation/05-borda.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- Service reescrito (`services/SEO` legado → `services/Seo`, ADR-015): contratos `SeoBinder`/`SeoMetadataGenerator`/`SeoGraphBuilder`, `SeoData` (readonly), `SeoSettings`.
- `<x-service-seo::tags>` é a **fonte da verdade do `<head>`** (sufixo do título pertence ao service); `@graph` determinístico (`GovernmentOrganization` + `WebSite`/`SearchAction` + `GovernmentPage` eMAG + `BreadcrumbList` + nós de módulo).

## Arquivos para revisar

- `src/services/Seo/**`
- `src/services/Seo/Contracts/*`
- `src/services/Seo/DTOs/SeoData.php`
- `src/services/Seo/SeoService.php`, `SeoSettings.php`
- `src/services/Seo/Actions/{GenerateSeoMetadata,BuildSeoGraph}.php`
- `src/services/Seo/View/components/tags.blade.php`
- `src/services/Seo/Routes/{portal,api,cms}.php`
- `src/services/Seo/Lang/*` (se houver)
- `src/services/Seo/README.md`
- `src/services/Seo/Test/{SeoServiceTest,SeoGraphTest,SeoTagsTest}.php`
- `src/layout/Portal/partials/metadata.blade.php`
- `src/layout/Portal/partials/analytics.blade.php`
- `src/layout/Components/portal.blade.php`
- `config/seo.php`
- `.pi/trash/services-SEO-legacy/**`

## Pontos de revisão

### PHP 8.4
- [ ] `declare(strict_types=1)`; tipos nativos (union/enum/`readonly`); sem `mixed` desnecessário.
- [ ] Dependências **injetadas** (nenhum `new`/`app()`/`resolve()` no corpo); sem traits/abstract/generics.
- [ ] `match` em vez de cadeias de `if`; `?->`/`??`; sem código morto; PHPStan `max` limpo.

### Laravel 12
- [ ] Nativo primeiro (Eloquent casts/scopes, `FormRequest`, `Gate`, scheduler, events) — sem reimplementar.
- [ ] Sem closure em rota; resources/rotas nomeadas; `config()` só lê, `env()` só em `config/`.
- [ ] Migração **idempotente**; `down()` → `backup_*` (nunca `DROP`); sem `orderBy` cru quando `latest/oldest` serve.

### HTML semântico + a11y (eMAG/WCAG 2.2 AA)
- [ ] Um `<main>`, headings em nível correto, landmarks; só ARIA quando necessário.
- [ ] Alvos ≥ 24 px (≥ 44 px em ponteiro grosso); foco visível; rótulos em todos os campos; contraste (axe).
- [ ] Funciona **sem JS**; `sniffCSS` sem `fail`/`warn` fora do ledger.

### CSS moderno
- [ ] `oklch`, `clamp`, `rem`/`fr`/`dvh`/`%`; `@layer` (a11y por último); `@property`; subgrid com fallback.
- [ ] Sem `!important`, sem `style="…"` inline, sem hex/rgb (exceto justificado); sem CDN.

### Decisões do projeto (ADRs)
- [ ] Respeita a ADR do escopo (ver cabeçalho) e não escapa das demais (001, 002, 004, 013, 015…).
- [ ] Determinismo: relógio/id injetados, ordem explícita, dados idempotentes (ADR-001).

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa; 4 locales com paridade de chaves; `route()`/`localized_url()` (nunca path literal).

### Observabilidade/segurança/testes
- [ ] Span + `log_event('{module}.{action}')` com atributos `scalar|null`, **sem PII** (ADR-024).
- [ ] Teste que falha sem a mudança (vermelho→verde) + evidência no `kd`; `make quality` 2×.

### Específicos desta task

- [ ] `@graph` com ordem fixa e `@context` único; nós do módulo sem `@context`.
- [ ] `seo.track` legado removido (tabela inexistente); baseline PHPStan encolhido (130→87).
- [ ] Shell só inclui o componente; sufixo do título no service.

## Comandos úteis

```bash
make quality            # lint + PHPStan max + gates + testes
make e2e                # Playwright + axe (se a task toca UI)
make check-boundaries   # fronteiras core/services/layout/modules
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-18 | agente | 🔧 | **A** (**alto**) o `@graph` declarava um `SearchAction` apontando para `/busca` — que responde **404** (medido com `curl`, e o service `Search` é **backlog**): dado estruturado apontando para o vazio (ADR-027) → `seo.settings.search_path` deixou de ter default e o nó passou a ser **condicional** (`SeoSettings::searchUrl()` devolve `null`; `website()` só monta `potentialAction` quando há página de busca), com teste novo **vermelho→verde** (`the_search_action_only_exists_when_a_search_page_is_configured`) e o ramo positivo coberto por `config()` explícita; `SeoTagsTest` passou a afirmar a **ausência** no default — a home ao vivo confirma os 3 nós base sem `potentialAction` (`fact_007ofka0`); **B** (médio → `task_01btnk4j`) `alternatesForPath()` gera os **4 locales para qualquer caminho** sem verificar se a rota existe → num caminho só-do-default (a página 404, um módulo futuro) o `hreflang` aponta para 404 (hoje todo route do portal é registrado nos 4 locales, então o impacto é latente) (`risk_01111rdx`); **C/D** (baixo) `SeoService::resolve()` usa `url()->current()` — **estado de request** num método documentado como *função pura* (candidato: injetar `UrlGenerator`) — e a conversão BCP-47 (`str_replace('_','-')`) está **triplicada** (`SeoSettings::languages`, `BuildSeoGraph::page`, `tags.blade.php`), com `Core\Localization\Locales` como dono natural (`risk_01yrqp1i`, `risk_01j5sg9f`); **E–I** (obs) `canonical`/`hreflang` **não carregam a paginação** (`/noticias?page=2` canoniza para a página 1 e não há `rel=next/prev` — medido; é decisão de SEO, não correção óbvia); o `GovernmentPage.name` carrega o sufixo do site; falta `og:image:alt`; a `description` cai no nome da entidade (meta description duplicada em páginas sem descrição) e `merge()` não distingue override vazio de ausente; só o **Demo** usa `SeoMetadataGenerator`/`metadata['seo']` (a Notícias passa `schemas`/`breadcrumbs` direto na view — o caminho jsonb está sub-exercitado e a aba SEO do CMS não existe) (`risk_00jfeg7s`, `risk_003dqbkw`); e o formulário de busca da **casca** ainda posta para `/busca` (404, já marcado `@debt [SEARCH]`). **Verificado OK:** o legado (`Services\SEO`, `seo.track`) tem **zero** ocorrências; ordem fixa dos nós + `@context` único; a casca **só inclui** o componente e o sufixo do título mora no service; `JSON_HEX_*` no `<script>` (XSS-safe) e nenhum `<script>` de terceiros; `x-default` do locale primário; `hreflang` por locale; span + `log_event('seo.metadata'/'seo.graph')`; `env()` só em `config/`; `arch-check: OK`; e os aliases de `SeoData::fromArray` (`image_url`/`schema`/`json_ld`) **não** são código morto — têm consumidor (Demo) e teste (`SeoRootFilesCoverageTest`). `make check E2E=1`: 20 OK · 0 FAIL · 1 SKIP, e2e **91 passed**, cobertura **91,14%**. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
