# L2.3 — Full-Page Cache (FPC) de origem para rotas públicas

**Seed:** `public_infra_update-4289` · **Critérios:** A1 (origem/TTFB) · **Esforço:** médio · **Risco:** médio

Implementado em 2026-09-09 (Laravel 5.5 / PHP 7.2 / EA4). Complementa o cache de
borda (L1.1/L1.2): o CDN já cacheia o HTML na borda; o FPC de **origem** evita
regenerar o HTML por request (DB + render de view) quando a borda volta vazia,
reduzindo `origem`/`ttfb` das páginas públicas.

## Abordagem

Middleware `app/Http/Middleware/PageCache.php`, registrado **antes** do
`CacheControl` no grupo `web` e em `$middlewarePriority`.

- **MISS:** deixa o `CacheControl` rodar (que remove o cookie e estabiliza o HTML
  via strip do token CSRF em L1.0), e só então grava o corpo final no store de
  Cache (`CACHE_DRIVER`, file/redis) por URL.
- **HIT:** devolve cedo — **antes do `StartSession`** (economiza sessão em página
  100% pública, alinhado a L3.3) — e reaplica o `CacheControl` sobre o HTML
  cacheado para manter `Cache-Control: public`, `ETag` e o `304` (A2) idênticos a
  uma resposta fresca. Marca `X-Page-Cache: HIT` (MISS no primeiro hit).

## Regras de cacheabilidade (defesa em profundidade)

- Só `GET` de controllers `App\Http\Controllers\Portal\` (anonimos; o portal não
  usa `auth`). Admin/Prefecture/Api ficam de fora.
- Só resposta `200` e `Content-Type: text/html`.
- `Auth::check() === true` → não cacheia (nunca servir conteúdo de uma sessão a
  outra).
- Rota `csrf.token` excluída.
- Se a resposta ainda tiver `Set-Cookie`, não cacheia.

## Chave e invalidação

- Chave: `page_cache:v1:` + `md5(fullUrl + '|' + locale)`.
- **TTL** (`config/cache.page_cache.ttl`, default 300) = teto de obsolescência
  (alinhado ao `s-maxage` da borda).
- **Eventos de conteúdo:** `created`/`deleted` dos models de conteúdo
  (`config/cache.page_cache.invalidate_models`: `Content`, `CategoryContent`,
  `StaticPage`, `Survey`, `CalendarEvent`) disparam `PageCache::flush()`.
  Usa-se `created`/`deleted` (e **não** `updated`) para não purgar o cache a cada
  atualização trivial — ex.: `ServiceLetter` tem contador de visualizações
  (`increment('visualizacoes')`); se incluíssemos `updated`, o FPC seria purgado
  em toda visualização. Edições de conteúdo refletem em até `ttl` segundos.
- **Purga manual:** `php artisan cache:page:clear` (limpa o registry + chaves).
- (A4) purge do Multi-CDN é feito à parte; aqui só o cache de origem.

## Configuração (`.env`)

```
PAGE_CACHE_ENABLED=true      # liga/desliga o FPC
PAGE_CACHE_TTL=300           # segundos
```

## Verificação (canário)

- `X-Page-Cache: MISS` na 1ª requisição, `HIT` nas seguintes (mesmo corpo; o
  "CONTEUDO-NOVO" de um miss simulado não vaza).
- `Cache-Control: public` + `ETag` presentes no HIT (CacheControl reaplicado).
- `php artisan cache:page:clear` limpa; requisição seguinte volta a `MISS`.
- Criar/remover um `Content` → FPC limpo.
- Validar `origem`/`ttfb` das páginas públicas (medir `ETAPA=...`) e que forms
  (CSRF client-side) continuam sem 419.

## Testes

`tests/Unit/PageCacheTest.php` (6 testes): cache+serviço de hit, escopo Portal,
não cacheia POST, não cacheia `csrf.token`, flush, só `text/html` 200.
