# F5.I1.T2 — Arquivos de raiz dinâmicos + 404 real + 301 + hreflang

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

## O que foi implementado

- `sitemap.xml` (índice) + `sitemap-{fonte}.xml` a partir do `Core\Contracts\SitemapRegistry` (porta `Ports\SitemapSource`); `robots.txt` por área, `security.txt`, `manifest.webmanifest`; `ETag`/304 + `Cache-Control`.
- 301 de URLs legadas (`seo.redirects` + middleware **global** restrito ao portal; rota viva nunca sombreada; same-origin) e 404 real (`X-Robots-Tag: noindex` + `http.not_found`).

## Arquivos para revisar

- `src/services/Seo/Actions/{BuildSeoSitemap,ResolveSeoRedirect}.php`
- `src/services/Seo/Http/Controllers/SeoRootFilesController.php`
- `src/services/Seo/Http/Middleware/RedirectLegacy.php`
- `src/services/Seo/Migrations/2026_10_20_000000_create_seo_redirects_table.php`
- `src/services/Seo/Models/SeoRedirect.php`
- `src/services/Seo/Contracts/{RedirectLookup}.php`
- `src/core/Contracts/Ports/SitemapSource.php`
- `src/core/Contracts/SitemapRegistry.php`
- `src/core/Internal/Seo/InMemorySitemapRegistry.php`
- `src/modules/Portal/Home/Seo/HomeSitemapSource.php`
- `src/modules/Publicacoes/Noticias/Seo/NoticiaSitemapSource.php`
- `src/core/Internal/Exceptions/ProblemDetailsHtmlRenderer.php`
- `bootstrap/app.php`
- `src/core/CoreServiceProvider.php`
- `tests/Feature/SeoSitemapTest.php`
- `src/services/Seo/Test/SeoRedirectTest.php`

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

- [ ] `ETag`/304 estáveis; só URLs públicas no sitemap; `hreflang` por locale.
- [ ] Middleware global só no portal; open-redirect bloqueado; rota viva nunca sombreada.
- [ ] `public/robots.txt` estático removido (não sombreia a rota).

## 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**) a **página de erro não passa pelo resolvedor de locale** (não há rota → o `ResolveLocaleFromUrl`, registrado só no grupo do portal, nunca roda) → medido: `/en/pagina-inexistente` respondia com `<html lang="pt-BR">` e o texto em português ✗ (WCAG 3.1.1/ADR-012) → `ProblemDetailsHtmlRenderer` passou a resolver `Locales::fromPath($request->path())` + `Context('locale')` antes de renderizar, com teste nos **4 locales** (vermelho antes) (`fact_00egugli`); **B** (**alto**) o título/detalhe do erro eram **pt_BR hardcoded no Core** ✗ (ADR-012) e o `ModuleI18nTest` não os vê (a string mora em PHP, não em view) → o renderer manda só o **status** (+ título/detalhe próprios da `ProblemDetailsException`/debug) e a **view** resolve `layout::shell.error_{status}_{title,detail}` — **17 chaves × 4 locales** (o Core não pode depender do `layout`) (`fact_01djxthk`); **C** (**alto**) o "voltar ao início" usava **`url('/')`** → em `/en/…` levava para o pt_BR ✗ (a proibição do AGENTS, mas `ModuleI18nTest`/`LocaleUrlInvariantTest` não cobriam `resources/` — não há rota para um 404) → `localized_url('/')` + teste novo em `LocaleUrlInvariantTest` (`the_error_page_keeps_the_locale_in_its_home_link`, vermelho antes) (`fact_00yv4je5`); **D** (médio) o 404 declarava **`canonical` + `hreflang` para 4 URLs que também são 404** (o achado B da T1) → regra única no componente: página **`noindex`** não declara canônica nem alternates (teste novo, vermelho antes); o caso geral (rota só-do-default) segue em `task_01btnk4j` (`fact_00p9xgh8`); **E/F** (baixo) `security.txt` tinha `Expires` de **exatamente um ano** (RFC 9116 §2.5.5 exige **menos**) e `now()` fora do relógio injetado ✗ → `Clock` injetado + 6 meses + `Preferred-Languages` de `SeoSettings::languages()` (teste com `FixedClock` provando a regra do RFC, vermelho com `addYear()`) (`fact_0197rsvh`); e `NoticiaSitemapSource` não usava o **`publicadas()`** que o próprio docblock citava → refactor sem mudança de comportamento (`fact_00bh3chj`); **G/H** (obs) o **manifest** declara o **mesmo logo** como `192x192`/`512x512` `image/png` (sizes e MIME inventados — o PWA não instala com ícone correto; o dono é o **Media**, ADR-026 → `task_01kvi19b`), com `theme_color`/`background_color` `oklch()` hardcoded fora dos tokens e sem `lang`/`dir` (`risk_00znkqy1`); e a coluna **`status`** do redirect **nunca é lida** (o middleware devolve sempre 301, embora o CHECK aceite 302/307/308), `safeTarget` aceita destino **relativo** (resolvido contra o caminho atual), o registro de fontes deixa um `id()` duplicado **sobrescrever** em silêncio e o formulário DSAR do portal não é `noindex` (`risk_01cl9z5i`). **Verificado OK:** `ETag`/304 (`If-None-Match`) + `Cache-Control` em todos os arquivos de raiz; só URLs **públicas** no sitemap (ordenação por `loc`, `lastmod`/`hreflang`, `urlset` da fonte desconhecida → 404); o middleware global só age no **domínio do portal** e uma **rota viva vence** (`MethodNotAllowed` inclusive); destino **same-origin** (bloqueia `//evil` e `scheme:`); `down()` → `backup_*` (ADR-017) e a migração é idempotente (`hasTable` + `CREATE SCHEMA IF NOT EXISTS` + CHECK `NOT VALID` → `VALIDATE`); `X-Robots-Tag: noindex, nofollow, noarchive` + `http.not_found` sem PII; `resources/views/errors/problem.blade.php` segue um `<h1>` dentro do shell, sem stack trace. `make check E2E=1`: 20 OK · 0 FAIL · 1 SKIP, e2e **91 passed**, cobertura **91,37%** (subiu de 91,14). |

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