---
id: CORE-BOOT
tipo: arquitetura
onda: 1
status: concluido
unidades: [CORE-BOOT]
fontes:
  - V2/Core/Routes/
depende_de: [CORE-BOOT]
consumidores: [todos os MOD-*, todos os SVC-*, UI-PORTAL, UI-CMS]
criticidade: alta
dono: BootNavigation
---

# Arquitetura: roteamento (core, módulos, serviços e fallback)

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Cada requisição é resolvida por uma tabela de rotas montada no boot em ordem fixa: Core → módulos → fallback `/{slug}` → serviços. |
| Entrada | Arquivos `Routes/{web,cms,api}.php` dentro de `V2/Core`, `V2/Modules/<Grupo>/<Modulo>` e `V2/Services/<Servico>`. |
| Saída | Rotas com prefixo, middleware e namespace definidos pelo contexto; nomes declarados no próprio arquivo. |
| Efeitos colaterais | `page.cache` grava HTML em Redis (5 min); `public.cache` aplica `Cache-Control: public, max-age=60, must-revalidate`; redirects 301 permanentes. |
| Particularidade central | O provider **não deriva nomes de rota nem prefixo CMS**; ele só injeta namespace e middleware. Todo `name()` e todo `prefix` são literais no arquivo. |
| Como se integra | `V2ServiceProvider::mapCoreRoutes()`, `mapModuleRoutes()`, `mapFallbackRoutes()`, `mapServiceRoutes()`. |
| Se removido, quebra | Portal e CMS inteiros: não há `RouteServiceProvider` no projeto. |

## 1. Efeito

O sistema expõe três famílias de URI, com contratos distintos:

| Família | Origem | Middleware aplicado pelo provider | Observação |
|---|---|---|---|
| Portal (`/...`) | `Routes/web.php` de Core, módulo ou serviço | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | Cache de página em Redis + cabeçalho HTTP. |
| CMS (`/cms/...`) | `Routes/cms.php` | Core: `web`; módulo: `web`, `module.active:<Modulo>`; serviço: `web` | `CmsAuthenticate` **não** é aplicado automaticamente. |
| API (`/api/v2/...`) | `Routes/api.php` | `api` + prefixo | Core: `api/v2/core`; módulo: `api/v2/<modulo-minusculo>`; serviço: `api/v2/<slug-sem-Service>`. |
| Fallback (`/{slug}`) | `V2/Core/Routes/fallback.php` | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | Registrado por último; só `GET`. |

O que **não** acontece:

- Não existe rota nomeada gerada automaticamente. Se o arquivo não chamar `->name()`, a rota não tem nome e `route()` falha.
- Não há prefixo automático de CMS: um arquivo de módulo pode registrar `/qualquer-coisa` se não declarar `prefix => 'cms/...'`.
- Não há agrupamento de middleware `cms` no Kernel; o que existe são aliases individuais (`app/Http/Kernel.php:60-70`).

## 2. Abordagem

### 2.1 Ordem de registro (determinística)

```mermaid
flowchart TD
  A[mapCoreRoutes] --> A1[/busca, /login, /cms/*/]
  A1 --> B[foreach grupo]
  B --> B1[foreach módulo: web, cms, api]
  B1 --> C[instance v2.modules.configs]
  C --> D[mapFallbackRoutes: GET /{slug}]
  D --> E[booted: refreshNameLookups]
  E --> F[foreach serviço: cms, web, api]
```

Cada chamada é um `Route::middleware([...])->namespace(...)->group($arquivo)` (`V2/Core/Providers/V2ServiceProvider.php:203-232,256-281,287-313,327-336`).

### 2.2 Regras por arquivo

| Arquivo | Namespace do controller | Prefixo de URI | Middleware |
|---|---|---|---|
| Core `web.php` | `V2\Core\Controllers` | nenhum | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` (`:263`) |
| Core `cms.php` | `V2\Core\Controllers` | nenhum (o próprio arquivo usa `prefix => cms`) | `web` (`:269`) |
| Core `api.php` | `V2\Core\Controllers` | `api/v2/core` | `api` (`:275-278`) |
| Módulo `web.php` | `V2\Modules\<Grupo>\<Modulo>\Controllers` | nenhum | `web`, maintenance, `page.cache`, `public.cache` (`:298`) |
| Módulo `cms.php` | idem | nenhum | `web`, `module.active:<Modulo>` (`:304`) |
| Módulo `api.php` | idem | `api/v2/<modulo-minusculo>` | `api` (`:310`) |
| Serviço `cms.php` | `V2\Services\<Servico>\Controllers` | nenhum | `web` (`:211`) |
| Serviço `web.php` | idem | nenhum | `web`, maintenance, caches (`:217`) |
| Serviço `api.php` | idem | `api/v2/<slug>` | `api` (`:223-227`) |
| Fallback | FQCN no arquivo | nenhum | `web`, maintenance, caches (`:332`) |

O namespace só é útil quando o arquivo usa strings `'Controller@action'`; quando usa FQCN (`BasicPagePortalController::class . '@runtime'`), o namespace do grupo é irrelevante (`V2/Core/Routes/fallback.php:14-19`).

### 2.3 Nomes de rota: como são (e não são) derivados

Não há derivação. As convenções são sociais, verificáveis nos arquivos:

- Convenção documentada no exemplo: `v2.{slug-do-modulo}.{action}` (`V2/Modules/_Exemplos/ExemploAPI/Routes/cms.php:14-18`).
- Nomes reais divergem dela: `v2.exemplo-api.index` (hífen), `v2.certificadodigital.index` (sem separador), `v2.supportconfigs.index`, `v2.noticias.cms.index` (com `cms`).
- O Core usa `v2.cms.*` e `v2.portal.*` (`V2/Core/Routes/cms.php:15-33`, `web.php:14`).

O único momento em que o provider toca nomes é a reconstrução dos lookups (`refreshNameLookups`, `refreshActionLookups`) em `booted()` (`:139-142`): sem ela, `route()` não encontraria rotas agrupadas por arquivo.

### 2.4 Trechos de código pivô

```php
// V2/Core/Routes/web.php:10-14 — única rota de portal do Core.
// Portal Home transferida para o Módulo Homepage
// Route::get('/', 'HomePortalController@index')->name('v2.portal.home');

// Busca Global do Portal
Route::get('/busca', 'SearchController@portalSearch')->name('v2.portal.search');
```

A home não é rota do Core; ela pertence ao módulo `Layouts/Homepage` (`V2/Modules/Layouts/Homepage/Routes/web.php`). O Core mantém `/busca` porque a busca é transversal.

```php
// V2/Core/Routes/cms.php:10-24 — redirect legado e grupo de login.
Route::redirect('/login', '/cms/login');

Route::group(['prefix' => 'cms', 'namespace' => 'Auth'], function() {
    Route::get('/login', 'LoginCmsController@showLoginForm')->name('v2.cms.login');
    Route::post('/login', 'LoginCmsController@login')->name('v2.cms.login.post');
    Route::get('/logout', 'LoginCmsController@logout')->name('v2.cms.logout.get');
    Route::post('/logout', 'LoginCmsController@logout')->name('v2.cms.logout');
});

Route::group(['prefix' => 'cms', 'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class]], function() {
    Route::get('/', 'DashboardCmsController@index')->name('v2.cms.dashboard');
    Route::redirect('/dashboard', '/cms');
```

`Route::redirect` no Laravel 5.5 aponta para `RedirectController` com status **301** por default e registra **todos** os métodos (`vendor/laravel/framework/src/Illuminate/Routing/Router.php:238-243`). Logo `/login` responde 301 para `/cms/login`, e `/cms/dashboard` responde 301 para `/cms`.

```php
// V2/Core/Routes/fallback.php:14-21 — catch-all de um segmento.
use V2\Modules\Layouts\BasicPage\Controllers\BasicPagePortalController;

Route::get('/{slug}', BasicPagePortalController::class . '@runtime')
    ->name('v2.portal.runtime')
    ->where('slug', '[a-z0-9-]+');
```

Sem o `where`, o fallback engoliria qualquer caminho de um segmento; com ele, só letras minúsculas, dígitos e hífen — maiúsculas em URL geram 404 direto, sem passar pelo `BasicPage`.

```php
// V2/Core/Providers/V2ServiceProvider.php:295-310 — rotas de módulo por contexto.
$controllerNamespace = "V2\\Modules\\{$groupName}\\{$moduleName}\\Controllers";

if (File::exists($webRoutes)) {
    Route::middleware(['web', \V2\Core\Middleware\MaintenanceMiddleware::class, 'page.cache', 'public.cache'])
        ->namespace($controllerNamespace)
        ->group($webRoutes);
}

if (File::exists($cmsRoutes)) {
    Route::middleware(['web', 'module.active:' . $moduleName])
        ->namespace($controllerNamespace)
        ->group($cmsRoutes);
}

if (File::exists($apiRoutes)) {
    Route::prefix('api/v2/' . strtolower($moduleName))
        ->middleware('api')
        ->namespace($controllerNamespace)
        ->group($apiRoutes);
}
```

A ordem interna dos três blocos é `web → cms → api` por módulo, mas isso não afeta o matching entre contextos, que usam URIs diferentes.

```php
// V2/Core/Middleware/PageCacheMiddleware.php:13-23,25-40 — cache HTML em Redis.
protected $ttl = 5;

protected $except = [
    'cms*', 'api*', 'trabalhe-conosco*', 'associe-se*', 'fale-conosco*', '_debugbar*',
];

public function handle($request, Closure $next)
{
    if (!$this->shouldCache($request)) {
        return $next($request);
    }
    try {
        $cache = Cache::store($this->store);
        $key = $this->cacheKey($request);
        $cached = $cache->get($key);
        if ($cached) {
            return response($cached['content'], $cached['status'])
                ->withHeaders($cached['headers'])
                ->header('X-Cache-Status', 'HIT');
        }
```

Só `GET` não-AJAX fora dos prefixos é cacheado; a chave é `page:` + `sha1(fullUrl)` (`:88-90`), ou seja, query string entra na chave. Falha de Redis loga `[PageCache] Redis indisponivel, servindo sem cache: ...` e segue sem cache (`:62-64`).

```php
// V2/Core/Middleware/PublicCacheMiddleware.php:31-36,86-93 — cabeçalho HTTP.
protected $excludedPrefixes = ['/cms', '/trabalhe-conosco', '/associe-se', '/fale-conosco'];
// ...
$response->headers->set('Cache-Control', 'public, max-age=60, must-revalidate');
if (!$response->headers->has('Vary')) {
    $response->headers->set('Vary', 'Accept-Encoding');
}
```

Aplicado a GET/HEAD 2xx; respeita `Cache-Control` explícito do controller (`:82-84,109-120`).

### 2.5 Colisões e ordem

- **Matching**: o Laravel percorre as rotas na ordem de registro e usa a primeira que casa (método + URI). Por isso o fallback precisa ser o último — se fosse o primeiro, `GET /noticias` casaria `/{slug}`.
- **Lookup por nome**: `refreshNameLookups()` percorre a coleção e grava `nameList[nome] = rota`; em nomes duplicados, a **última** rota registrada vence. Consequência: se um módulo registrar `v2.cms.dashboard`, `route('v2.cms.dashboard')` aponta para ele, embora o Core continue respondendo em `/cms`.
- **URI duplicada**: o matching continua devolvendo a primeira rota registrada; a segunda fica inalcançável, mas ainda aparece em `route:list`.
- **Ordem entre módulos**: depende de `File::directories()` (sem `sort()` em `V2/Core/Providers/V2ServiceProvider.php:92,96`). Colisões de URI entre módulos são, portanto, não determinísticas entre filesystems.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Redirect 301 permanente** para `/login` e `/cms/dashboard`; navegadores memorizam. [C] `vendor/.../Router.php:238-243` + `V2/Core/Routes/cms.php:11,24`.
- **Fallback com regex `[a-z0-9-]+`** e método exclusivamente `GET`: POST em slug inexistente retorna 405 (rota existe para GET) e maiúsculas não são tentadas.
- **Cache em duas camadas** no portal: HTML no Redis (`page.cache`) + `Cache-Control` no navegador (`public.cache`). `X-Cache-Status: HIT|MISS` é observável.
- **CMS fora do `public.cache` por dois mecanismos**: middleware não é aplicado a arquivos `cms.php` e `/cms` está na lista de exclusão.
- **`api.php` do Core vazio**: `Route::get('/status', ...)` está comentado (`V2/Core/Routes/api.php:10`), portanto `api/v2/core` não tem nenhum endpoint.

### 3.2 Inventário completo das rotas do Core (`V2/Core/Routes/*`)

| # | Tipo | Métodos | URI | Nome | Middleware efetivo | Controller@ação | Fonte |
|---|---|---|---|---|---|---|---|
| 1 | redirect | ANY | `/login` | — | `web` | → `/cms/login` (301) | `cms.php:11` |
| 2 | endpoint | GET | `/cms/login` | `v2.cms.login` | `web` | `Auth\LoginCmsController@showLoginForm` | `cms.php:14-15` |
| 3 | endpoint | POST | `/cms/login` | `v2.cms.login.post` | `web` (CSRF) | `Auth\LoginCmsController@login` | `cms.php:16` |
| 4 | endpoint | GET | `/cms/logout` | `v2.cms.logout.get` | `web` | `Auth\LoginCmsController@logout` | `cms.php:17` |
| 5 | endpoint | POST | `/cms/logout` | `v2.cms.logout` | `web` (CSRF) | `Auth\LoginCmsController@logout` | `cms.php:18` |
| 6 | endpoint | GET | `/cms` | `v2.cms.dashboard` | `web`, `CmsAuthenticate` | `DashboardCmsController@index` | `cms.php:22-23` |
| 7 | redirect | ANY | `/cms/dashboard` | — | `web`, `CmsAuthenticate` | → `/cms` (301) | `cms.php:24` |
| 8 | endpoint | GET | `/cms/search` | `v2.cms.search` | `web`, `CmsAuthenticate` | `SearchController@cmsSearch` | `cms.php:27` |
| 9 | endpoint | GET | `/cms/conexao-tags` | `v2.cms.conexao-tags.index` | `web`, `CmsAuthenticate` | `ConexaoTagCmsController@index` | `cms.php:30` |
| 10 | endpoint | GET | `/cms/conexao-tags/json` | `v2.cms.conexao-tags.json` | `web`, `CmsAuthenticate` | `ConexaoTagCmsController@indexJson` | `cms.php:31` |
| 11 | endpoint | POST | `/cms/conexao-tags` | `v2.cms.conexao-tags.store` | `web`, `CmsAuthenticate` | `ConexaoTagCmsController@store` | `cms.php:32` |
| 12 | endpoint | DELETE | `/cms/conexao-tags/{id}` | `v2.cms.conexao-tags.destroy` | `web`, `CmsAuthenticate` | `ConexaoTagCmsController@destroy` | `cms.php:33` |
| 13 | endpoint | GET | `/busca` | `v2.portal.search` | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | `SearchController@portalSearch` | `web.php:14` |
| 14 | endpoint | GET | `/{slug}` | `v2.portal.runtime` | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | `V2\Modules\Layouts\BasicPage\Controllers\BasicPagePortalController@runtime` | `fallback.php:19-21` |

`CmsAuthenticate` delega a `V2PermissionMiddleware` (`V2/Core/Middleware/CmsAuthenticate.php:44`), que aplica RBAC por `active_pattern` do módulo — por isso a coluna “middleware efetivo” não inclui o `V2PermissionMiddleware` separadamente.

### 3.3 Casos-limite

- **`/cms/login` com usuário logado**: `showLoginForm()` redireciona para `v2.cms.dashboard` antes de renderizar (`V2/Core/Controllers/Auth/LoginCmsController.php:19-23`).
- **Logout por GET**: existe e não exige CSRF (`cms.php:17`); é um vetor de logout forçado (CSRF de logout).
- **`/cms/conexao-tags/{id}`**: não há `where` numérico/UUID; qualquer string chega ao controller; o binding é manual via `$id`.
- **Slug com maiúsculas ou acento**: não casa `[a-z0-9-]+`; 404 sem consultar `BasicPage`.
- **Slug com barra (`/a/b`)**: não casa; 404.
- **Rota de módulo `/foo` e fallback**: a de módulo vence se registrada antes; a ordem relativa entre módulos é a do filesystem.
- **`page.cache` em `/busca?q=x`**: chave diferente por query; um número ilimitado de chaves pode ser criado (sem limite de cardinalidade).
- **Redis indisponível**: portal continua servindo sem cache e sem `X-Cache-Status`, com warning no log (`PageCacheMiddleware.php:62-64`).

### 3.4 Armadilhas de reimplementação

1. **Trocar 301 por 302** nos redirects: muda cache do navegador e comportamento após deploy.
2. **Aplicar `CmsAuthenticate` globalmente a `cms.php`**: hoje o arquivo de login precisa ficar fora; aplicar no grupo inteiro tornaria o login inacessível.
3. **Colocar o fallback antes dos módulos**: `/{slug}` captura `/noticias`, `/agenda`, `/busca`.
4. **Assumir prefixo `cms/` automático**: módulos que já declaram `prefix => 'cms/exemplo-api'` seriam prefixados duas vezes.
5. **Achar que `name()` é opcional**: `redirect()->route('v2.cms.dashboard')` no `CmsAuthenticate` depende de o Core ter registrado o nome antes (`CmsAuthenticate.php:25,40`).

### 3.5 Divergências documentação × código

| Documento | Afirma | Código |
|---|---|---|
| `V2/Modules/README.md:16-18` | `/cms/...`, `/...`, `/api/v2/...` | Confirmado, mas o prefixo é do arquivo, não do provider. |
| `V2/Modules/README.md:30` | `cms.php` "deve sempre usar `CmsAuthenticate`" | Não há enforcement: `cms/v2/gallery` é registrado sem auth de rota (`V2/Services/Gallery/Routes/cms.php:5`), o controller não tem guarda (`V2/Services/Gallery/Controllers/GalleryController.php:33-36`) e `cms/v2/gallery/*` está isento de CSRF (`app/Http/Middleware/VerifyCsrfToken.php:15-17`) — superfície anônima de escrita/leitura de mídia. |
| `V2/Core/README.md:17` | Rotas vitais "localizadas em `Core/Routes/`" | Confirmado; a home está no módulo Homepage (`web.php:10-11`). |
| `V2/README.md:24` | "Rotas: Divididas em `cms.php`, `web.php` e `api.php`" | Confirmado, mas `api.php` do Core está vazio. |

## 4. Integrabilidade

### 4.1 Receitas de uso

**Rota CMS de módulo, forma canônica** (`V2/Modules/_Exemplos/ExemploAPI/Routes/cms.php:20-31`):

```php
Route::group([
    'prefix' => 'cms/exemplo-api',
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
], function () {
    Route::get('/', 'ExemploAPICmsController@index')->name('v2.exemplo-api.index');
    Route::post('/demo-store', 'ExemploAPICmsController@demoStore')->name('v2.exemplo-api.demo-store');
});
```

Resultado: `GET /cms/exemplo-api` → `ExemploAPICmsController@index`, com `web`, `module.active:ExemploAPI` e `CmsAuthenticate` (nesta ordem: grupo do provider, depois grupo do arquivo).

**Rota de portal de módulo**:

```php
// V2/Modules/Layouts/Homepage/Routes/web.php
Route::get('/', 'HomepagePortalController@index')->name('v2.portal.home');
```

Resultado: home em `/`, herdando maintenance + page cache + public cache.

**Rota de API de serviço**:

```php
// V2/Services/MailService/Routes/api.php:21-25
Route::middleware([CmsAuthenticate::class])->group(function () {
    Route::post('/send', 'MailApiController@send')->name('v2.mail.send');
});
```

Resultado: `POST /api/v2/mail/send`. O prefixo vem do provider; o middleware de auth vem do arquivo.

### 4.2 Ordem de inicialização

1. O `V2ServiceProvider` é registrado em `config/app.php:185`.
2. `mergeConfigFrom` e helpers rodam em `register()` (`V2ServiceProvider.php:345-351`).
3. Rotas do Core; depois módulos; depois fallback; depois serviços (`:85,137,191`).
4. `booted()` atualiza lookups de nome/ação (`:139-142`).

Qualquer código que chame `route('v2....')` durante o boot (ex.: outro provider) pode falhar se rodar antes de `booted()`.

### 4.3 Anti-uso

1. **Registrar `/{qualquer}` em módulo**: passa a competir com o fallback e com outras rotas de um segmento; a ordem decide.
2. **Reutilizar nomes de rota do Core** (`v2.cms.dashboard`): o lookup por nome passa a apontar para a última rota registrada, quebrando redirects do middleware.
3. **Depender de `Route::redirect` para fluxo pós-login**: 301 é permanente e cacheável; trocar o destino depois exige limpar cache do navegador.

### 4.4 Custos e limites

- `page.cache`: TTL 5 min, store `redis`, sem limite de chaves e sem invalidação por prefixo no middleware; o comando `v2:clear-page-cache` remove `laravel:page:*` (`V2/Core/Console/Commands/ClearPageCache.php:29-38`).
- `public.cache`: `max-age=60`, `must-revalidate`, `Vary: Accept-Encoding`.
- Fallback: uma consulta ao `BasicPage` por request não cacheado.
- `Route::redirect`: zero controller, 301.

## 5. Inventário

| Arquivo | Papel | Efeito | Fonte |
|---|---|---|---|
| `V2/Core/Routes/web.php` | Rotas públicas do Core | `GET /busca` | `:10-14` |
| `V2/Core/Routes/cms.php` | Rotas administrativas | Login/logout/dashboard/busca/tags + 2 redirects | `:10-34` |
| `V2/Core/Routes/api.php` | API do Core | Nenhuma rota ativa | `:10` |
| `V2/Core/Routes/fallback.php` | Catch-all | `GET /{slug}` → BasicPage | `:17-21` |
| `V2/Core/Middleware/PageCacheMiddleware.php` | Cache HTML | Redis 5 min, `X-Cache-Status` | `:13-90` |
| `V2/Core/Middleware/PublicCacheMiddleware.php` | Cache HTTP | `Cache-Control` 60s | `:31-93` |
| `V2/Core/Middleware/MaintenanceMiddleware.php` | Manutenção | Bloqueio de portal | ver `CORE-RUNTIME` |

## 6. Riscos de reimplementação

1. **Ordem** é o contrato principal; qualquer mudança altera quem responde a `/{slug}`.
2. **301** em redirects é observável e persistente.
3. **Nomes de rota manuais** significam que não existe um esquema único; a reimplementação deve preservar cada nome listado.
4. **`module.active` ausente em rotas web/API**: desativar um módulo só bloqueia o CMS; portal e API continuam publicando. [C] `V2ServiceProvider.php:298-311`.
5. **Cache de página serve HTML autenticado?** Não: apenas GET não-AJAX fora de `cms*`/`api*`; ainda assim, qualquer rota pública que reflita sessão não deve usar o middleware.

## 7. Lacunas

- **LG-CORE-BOOT-04** — Não foi verificado se o `MaintenanceMiddleware` permite bypass por IP/rota; detalhes em `CORE-RUNTIME`.
- **LG-CORE-BOOT-05** — Não há teste automatizado localizado que fixe a ordem fallback × módulo; a garantia é o código e o comentário em `fallback.php:7-12`.
- **LG-CORE-BOOT-06** — `Route::redirect` com `ANY` + 301 foi confirmado no vendor; não foi observado em execução.

## 8. Checklist de paridade

- [ ] `PC-CORE-BOOT-09` — `GET /login` deve responder 301 com `Location: /cms/login`. Fonte: `cms.php:11`; `Router.php:238-243`.
- [ ] `PC-CORE-BOOT-10` — `GET /cms/dashboard` autenticado deve responder 301 para `/cms`. Fonte: `cms.php:24`.
- [ ] `PC-CORE-BOOT-11` — `DELETE /cms/conexao-tags/{id}` deve exigir `CmsAuthenticate` e CSRF. Fonte: `cms.php:22,33`.
- [ ] `PC-CORE-BOOT-12` — `GET /Busca` deve retornar 404 (regex do fallback), não `/busca`. Fonte: `fallback.php:21`.
- [ ] `PC-CORE-BOOT-13` — `GET /noticias` deve ir para o controller do módulo Noticias, nunca para `BasicPagePortalController`. Fonte: `V2ServiceProvider.php:135-137`.
- [ ] `PC-CORE-BOOT-14` — Segunda requisição a `GET /busca` deve retornar `X-Cache-Status: HIT`. Fonte: `PageCacheMiddleware.php:35-40`.
- [ ] `PC-CORE-BOOT-15` — Resposta de `/busca` deve ter `Cache-Control: public, max-age=60, must-revalidate`. Fonte: `PublicCacheMiddleware.php:88`.
- [ ] `PC-CORE-BOOT-16` — `api/v2/core` não deve responder nenhum endpoint. Fonte: `api.php:10`.

## 9. Evidências consultadas

- `V2/Core/Routes/web.php:1-15`; `cms.php:1-34`; `api.php:1-10`; `fallback.php:1-21`
- `V2/Core/Providers/V2ServiceProvider.php:85,203-336`
- `V2/Core/Middleware/PageCacheMiddleware.php:1-90`
- `V2/Core/Middleware/PublicCacheMiddleware.php:1-120`
- `V2/Core/Middleware/CmsAuthenticate.php:1-46`
- `V2/Modules/_Exemplos/ExemploAPI/Routes/cms.php:1-32`
- `V2/Services/MailService/Routes/api.php:15-27`; `V2/Services/Gallery/Routes/cms.php:1-13`
- `vendor/laravel/framework/src/Illuminate/Routing/Router.php:238-243`
- `app/Http/Kernel.php:28-70`
