---
id: MOD-PUBLICACOES-NOTICIAS-ROTAS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-PUBLICACOES-NOTICIAS]
fontes:
  - V2/Modules/Publicacoes/Noticias/
depende_de: [CORE-BOOT, CORE-AUTH, CORE-CMS, CORE-RUNTIME, MOD-CONEXOES-UNIDADEREGIONAL]
consumidores: [UI-CMS, UI-PORTAL, MOD-LAYOUTS-HOMEPAGE]
criticidade: alta
dono: NoticiasA
---

# Publicações/Notícias — rotas

> Anexo do [`spec.md`](spec.md). Vinte e quatro endpoints e três declarações de grupo em
> dois arquivos (20 endpoints CMS, 4 públicos — `/noticias`, `/noticia/{slug}`,
> `/noticia/{slug}/preview` e `/news-sitemap.xml`); nenhum endpoint de API no módulo. Telas em
> [`cms.md`](cms.md) e [`portal.md`](portal.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Expõe o CRUD editorial em `cms/noticias/*` e a publicação em `/noticias`, `/noticia/{slug}` e `/news-sitemap.xml`. |
| Entrada | HTTP GET/POST/PUT/DELETE; parâmetros de query na lista CMS; `{id}` e `{slug}` na URI. |
| Saída | HTML (views `v2-noticias::`), JSON (tags, categorias, destaque), XML (sitemap), redirects `302` com flash. |
| Efeitos colaterais | Escrita em `v2_news` e pivôs; auditoria; índice de busca; feed; purga de page cache; log estruturado. |
| Particularidade central | O grupo nacional-only (`RegionalRestrictMiddleware`) cobre 13 rotas; duas rotas `POST /categorias` colidem e a primeira é engolida pelo `RouteCollection`. |
| Como se integra | `Routes/cms.php` recebe `web`+`module.active:Noticias` do provider e `CmsAuthenticate` do próprio arquivo; `Routes/web.php` recebe `web`+maintenance+`page.cache`+`public.cache`. |
| Se removido, quebra | Menu do CMS, portal de notícias, sitemap do Google News, widgets da Homepage. |
| Superfície pública | As rotas da tabela §2. |

## 1. Efeito

### 1.1 Mapa de efeito por rota

As rotas CMS mudam dados (exceto as três de leitura: `index`, `create`, `edit`, `preview-iframe`, `archived`, `categories`, `categories/json`, `tags.index`). As rotas públicas só leem — nunca criam sessão, nunca gravam.

### 1.2 O que muda no sistema por requisição

| Requisição | Banco | Outros efeitos |
|---|---|---|
| `POST /cms/noticias/novo` | 1 `INSERT v2_news` + pivôs | auditoria `created`, busca, feed (se publicado), page cache purgado, `Log::info` |
| `PUT/POST /cms/noticias/editar/{id}` | 1 `UPDATE v2_news` + `sync` de tags/galeria | auditoria `updated`, busca, feed, cache, log |
| `DELETE /cms/noticias/{id}` | `deleted_at` | auditoria `archived`, remoção do índice público/feed, cache, log |
| `POST /cms/noticias/{id}/restaurar` | `deleted_at=NULL`, `status=draft` | auditoria `restored`, busca, cache |
| `POST /cms/noticias/{id}/toggle-highlight` | toggle `is_highlighted` | auditoria `highlight_toggled`, cache |
| `POST /cms/noticias/set-super-highlight` | `UPDATE ... SET false` em todas + `true` em uma | auditoria `super_highlight_set`, cache |
| `POST /cms/noticias/{id}/seo` | `metadata->seo` | auditoria `seo_updated`, cache |
| `POST /cms/noticias/tags` | `firstOrCreate v2_news_tags` | nenhum log de auditoria |
| `DELETE /cms/noticias/tags/{id}` | `DELETE` físico da tag (model sem `SoftDeletes`); pivôs ficam órfãos | nenhum log |
| `POST /cms/noticias/categorias` | `INSERT v2_news_categories` | auditoria `category_created` |
| `PUT/POST /cms/noticias/categorias/{id}` | `UPDATE` (bloqueado se `is_system`) | auditoria `category_updated` |
| `DELETE /cms/noticias/categorias/{id}` | `category_id=NULL` nas notícias + `DELETE` físico da categoria; filhas ficam com `parent_id` órfão | auditoria `category_deleted` |
| `GET /noticias`, `/noticia/{slug}`, `/news-sitemap.xml` | somente leitura | cache de página (Redis) e/ou cache próprio do sitemap |

### 1.3 O que **não** acontece

- Rotas CMS **não** são cacheadas: `cms*` está na lista `except` do `PageCacheMiddleware` (`V2/Core/Middleware/PageCacheMiddleware.php:16-23`).
- Não há throttle próprio, CSRF explícito no módulo (vem do grupo `web`) nem versionamento de API.
- Não há rota de aprovação, rejeição ou preview com token funcional.
- Não há redirect de `POST /categorias` para a página de categorias (retorna JSON).

## 2. Abordagem

### 2.1 Como as rotas são registradas

`V2ServiceProvider::mapModuleRoutes` aplica o namespace `V2\Modules\Publicacoes\Noticias\Controllers` e, conforme o arquivo:

| Arquivo | Middleware do provider | Efeito |
|---|---|---|
| `Routes/cms.php` | `web`, `module.active:Noticias` | rotas de administração; o arquivo adiciona `CmsAuthenticate` e o subgrupo `RegionalRestrictMiddleware` |
| `Routes/web.php` | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | rotas públicas; o arquivo declara `web` de novo — `Route::gatherMiddleware()` aplica `array_unique`, então `web` roda uma vez |

Fonte: `V2/Core/Providers/V2ServiceProvider.php:290-313`; `V2/Core/Middleware/PageCacheMiddleware.php:74-91`; `vendor/laravel/framework/src/Illuminate/Routing/Route.php:744-760`.

```mermaid
flowchart LR
  A[Request] --> B{prefixo cms?}
  B -- sim --> C[web + module.active:Noticias]
  C --> D[CmsAuthenticate: Sentinel + roles]
  D --> E[V2PermissionMiddleware: Noticias read/write]
  E --> F{rota nacional-only?}
  F -- sim --> G[RegionalRestrictMiddleware]
  F -- não --> H[Controller]
  G --> H
  B -- não --> I[web + maintenance + page.cache + public.cache]
  I --> J[Controller público]
```

### 2.2 Ordem de declaração e colisões

1. O grupo `cms/noticias` declara `GET /`, `GET|POST /novo`, `GET|PUT|POST /editar/{id}`, `GET /preview-iframe/{id}` e `DELETE /{id}` **antes** do subgrupo nacional-only.
2. `DELETE /cms/noticias/{id}` aceita qualquer segmento (inclusive `tags` ou `categorias`), mas `DELETE /tags/{id}` e `DELETE /categorias/{id}` têm dois segmentos e não colidem.
3. `POST /cms/noticias/categorias` é declarado **duas vezes** com nomes diferentes (`categories.index` e `categories.store`). O `RouteCollection` indexa por método+URI (`$this->routes[$method][$domainAndUri] = $route`), então a segunda sobrescreve a primeira; `refreshNameLookups()` no boot reconstrói a lista de nomes só com as rotas vivas, e `v2.noticias.cms.categories.index` deixa de existir.
4. `GET /cms/noticias/categorias` (página) e `GET /cms/noticias/categorias/json` convivem sem conflito.

### 2.3 Trechos de código pivô

```php
// V2/Modules/Publicacoes/Noticias/Routes/cms.php:10-13 — o grupo CMS.
Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/noticias',
], function () {
    Route::get('/', [
        'as'   => 'v2.noticias.cms.index',
        'uses' => 'NoticiasCmsController@index',
    ]);
```

**Efeito.** Toda rota do arquivo herda autenticação; o prefixo não é aplicado pelo provider, é declarado aqui. O namespace do controller vem do provider.

```php
// V2/Modules/Publicacoes/Noticias/Routes/cms.php:52-54,111-119 — subgrupo nacional-only
// e a dupla declaração de POST /categorias.
Route::group([
    'middleware' => [\V2\Core\Middleware\RegionalRestrictMiddleware::class],
], function () {
    // ... 13 rotas (destaques, arquivadas, SEO, tags, categorias)
});

Route::get('/categorias/json', ['as' => 'v2.noticias.cms.categories.json', 'uses' => '...@categoriesIndex']);
Route::post('/categorias', ['as' => 'v2.noticias.cms.categories.index', 'uses' => '...@categoriesIndex']);
Route::post('/categorias', ['as' => 'v2.noticias.cms.categories.store', 'uses' => '...@categoriesStore']);
```

**Efeito.** A segunda declaração vence; criar categoria funciona, e o nome `categories.index` desaparece do roteador. A ausência de `active_pattern` específico não importa: o RBAC já resolveu o módulo pelo prefixo `cms/noticias*`.

```php
// V2/Modules/Publicacoes/Noticias/Routes/web.php:10-37 — rotas públicas.
Route::group(['middleware' => ['web']], function () {
    Route::get('/noticias', ['as' => 'v2.noticias.portal.index', 'uses' => 'NoticiasPortalController@index']);
    Route::get('/noticia/{slug}', ['as' => 'v2.noticias.portal.show', 'uses' => 'NoticiasPortalController@show']);
    Route::get('/noticia/{slug}/preview', ['as' => 'v2.noticias.portal.preview', 'uses' => 'NoticiasPortalController@preview']);
    Route::get('/news-sitemap.xml', ['as' => 'v2.noticias.sitemap.news', 'uses' => 'NoticiasSitemapController@index']);
});
```

**Efeito.** `/noticia/{slug}` e `/noticia/{slug}/preview` são registradas antes do catch-all `/{slug}` do Core; o fallback não as captura. Qualquer slug (`[a-z0-9-]+` não é imposto aqui) é aceito pelo parâmetro.

```php
// V2/Modules/Publicacoes/Noticias/Controllers/NoticiasSitemapController.php:23-38 — cache
// próprio do sitemap, independente do page cache.
$currentHour = (int) date('H');
$currentMinute = (int) date('i');
if ($currentHour === 3 && $currentMinute <= 10) {
    Cache::forget('v2_news_sitemap_xml');
}

$xml = Cache::remember('v2_news_sitemap_xml', 360, function () {
    return $this->generateSitemap();
});

return response($xml, 200)->header('Content-Type', 'application/xml');
```

**Efeito.** O XML é recalculado a cada 6 h (360 min) e purgado na janela 03:00–03:10 (a cada requisição nessa janela). Como a rota é pública, `page.cache` também pode servir o XML por 5 min.

### 2.4 Decisões de design e por quê

- **Rotas públicas fora de prefixo** `[C]`: `/noticias` e `/noticia/{slug}` são URLs de SEO consolidadas; o fallback do Core é registrado depois justamente para não capturá-las.
- **CMS sem prefixo de módulo na URI** `[C]`: o prefixo é `cms/noticias`, não `cms/publicacoes/noticias`; a permissão usa `Noticias` (nome do diretório).
- **Nacional-only como middleware de grupo** `[C]`: destaques, arquivadas, SEO, tags e categorias são administração nacional; regionais editam apenas o próprio conteúdo.
- **Sem endpoint de API** `[C]`: não há `Routes/api.php`; a API de notícias regionais vive em `Conexoes/UnidadeRegional`, e a importação em `Support/NoticiasImport`.

### 2.5 Parâmetros, filtros e paginação

**`GET /cms/noticias`** (`Controllers/NoticiasCmsController.php:62-126`):

| Parâmetro | Tipo | Efeito |
|---|---|---|
| `status` | string | filtro exato; valor fora de `News::$validStatuses` é ignorado |
| `category_id` | uuid | `where('category_id', ...)` |
| `tag_id` | uuid | `whereHas('tags', ...)` |
| `search` | string | `ILIKE %termo%` em `title`, `subtitle`, `summary` + `whereHas` em `category.name` e `tags.name` |
| `page` | int | paginação de 15, ordenação `created_at desc` |

O escopo regional (`RegionalScopeService::applyScope`) é aplicado **depois** dos filtros; os contadores também são escopados (`scopedStats`).

**`GET /cms/noticias/arquivadas`**: sem filtros; `onlyTrashed()->orderBy('deleted_at','desc')->paginate(15)`; **não** aplica escopo regional (rota nacional-only) e usa `News::getStatsByStatus()` sem escopo (`:146-160`).

**`GET /noticias`** (público): o controller ignora a query string. Parâmetros como `?categoria=`, `?regional=`, `?q=` aparecem na URL e são lidos **apenas** pelo JS da view; o servidor devolve sempre todas as publicadas (`NoticiasPortalController.php:62-91`). Não há paginação.

**`GET /noticia/{slug}`**: `{slug}` é livre; `firstOrFail()` com `published()`; `404` quando não existe ou não está publicado.

**`GET /news-sitemap.xml`**: sem parâmetros; limite 1.000, janela de 6 meses, ordenação `published_at desc`.

## 3. Particularidades

### 3.1 O que só existe aqui

- **A única rota pública de detalhe é por slug**, não por id — e o slug é imutável (ver [`spec.md`](spec.md) RN-NOTICIAS-01).
- **A rota de preview pública existe e está quebrada**: `GET /noticia/{slug}/preview` chama `News::isAccessible($token)`, método inexistente (`NoticiasPortalController.php:156`); qualquer requisição resulta em `500`. O preview usado pelo editor é `GET /cms/noticias/preview-iframe/{id}`.
- **Cache duplo no sitemap**: `page.cache` (5 min, chave `page:<sha1(url)>`) + `Cache::remember('v2_news_sitemap_xml', 360)`.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-NOTICIAS-R01 | Rotas CMS passam por `CmsAuthenticate`, que delega ao `V2PermissionMiddleware`. | Sem permissão `Noticias` → `403 Forbidden. Access denied for this module.`; `read` bloqueia POST/PUT/DELETE com `403 Forbidden. Write actions are not allowed for your profile.` | `V2/Core/Middleware/CmsAuthenticate.php:44`; `V2PermissionMiddleware.php:84-127` |
| RN-NOTICIAS-R02 | Rotas nacional-only retornam `403` (AJAX/JSON) ou `302` ao dashboard. | Operador regional não vê arquivadas/tags/categorias. | `RegionalRestrictMiddleware.php:34-48` |
| RN-NOTICIAS-R03 | `module.active:Noticias` desativado → `404 Modulo desativado.` (HTML) e `404` JSON para AJAX/API; não há redirect ao dashboard. | Módulo desligado some do CMS. | `ModuleAccessMiddleware.php:38-61` |
| RN-NOTICIAS-R04 | O `DELETE /{id}` declara `{id}` sem `where`, aceitando qualquer string. | `DELETE /cms/noticias/qualquer` → `404` do `findOrFail`. | `Routes/cms.php:46-49` |
| RN-NOTICIAS-R05 | `POST /cms/noticias/categorias` resolve para `categoriesStore`; o nome `.index` não existe. | Chamar `route('v2.noticias.cms.categories.index')` lança exceção. | `Routes/cms.php:111-119`; `Routes/cms.php:111-114` (rota sobrescrita); `RouteCollection.php:65-77,120-129` |
| RN-NOTICIAS-R06 | `seo-update` exige `seo.title` (`required|max:255`); os demais campos são opcionais. | `302` com erros de validação quando falta o título. | `NoticiasCmsController.php:647-681` |
| RN-NOTICIAS-R07 | `tags.store` exige `name` (`required|max:100`) e responde o model JSON, inclusive em conflito (usa `firstOrCreate`). | Repetir o nome devolve a tag existente com `200`. | `NoticiasCmsController.php:703-719` |
| RN-NOTICIAS-R08 | `toggle-highlight` responde JSON e **não** valida existência prévia. | Id inexistente → `404` de `findOrFail`. | `NoticiasCmsController.php:588-607` |
| RN-NOTICIAS-R09 | O sitemap não é excluído do `page.cache`. | XML pode ficar 5 min atrás do banco, além das 6 h do cache próprio. | `PageCacheMiddleware.php:16-23,85-91` |
| RN-NOTICIAS-R10 | Rotas públicas não têm throttle nem rate limit. | Scraping não é limitado pelo módulo. | `Routes/web.php:10-37` |

### 3.3 Casos-limite

1. **`GET /noticia/{slug}/preview`** → `500` (método inexistente), mesmo com `?token=`.
2. **`GET /noticia/{slug}` de notícia agendada** (`published_at > now()`) → `200`, apesar de não aparecer em `/noticias`.
3. **`POST /cms/noticias/novo` com `status=pending_approval`** → aceito e persistido; a notícia não aparece no portal.
4. **`POST /cms/noticias/categorias` por operador regional** → bloqueado pelo grupo nacional-only (403/redirect), mesmo que o RBAC permita `write` em Notícias.
5. **`GET /cms/noticias/tags` por operador regional** → também bloqueado (rota dentro do subgrupo nacional-only), embora o editor use tags ao criar notícias.
6. **`PUT` sem `_method`** → o formulário precisa de `@method('PUT')`; `POST` na mesma URI também é aceito (`Route::match(['PUT','POST'])`).
7. **Slug com caracteres fora de `[a-z0-9-]`** → nenhuma restrição de rota; o valor chega literal ao `where('slug', ...)`.
8. **`news-sitemap.xml` com `titulo_site` indisponível** → fallback silencioso para `config('app.name')` (`NoticiasSitemapController.php:48-55`).

### 3.4 Armadilhas de reimplementação

1. **Recriar `POST /categorias` duas vezes** achando que ambos os nomes existirão: no Laravel 5.5 o segundo vence e o primeiro nome morre no boot.
2. **Aplicar `PageCacheMiddleware` nas rotas CMS** "por simetria": o CMS precisa ser fresco; o middleware já exclui `cms*`.
3. **Esperar que `/noticias` honre `?categoria=`**: o filtro é client-side; testes do legado apenas conferem que o HTML contém os cartões.
4. **Trocar a ordem dos grupos** (nacional-only antes do CRUD): muda a precedência de `DELETE /{id}` e de `POST /{id}/...`; manter a ordem do arquivo.
5. **Assumir que o `slug` da rota é validado**: não há `where`; URLs com barra não casam, mas qualquer outro caractere chega ao banco.

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

| Ponto | Afirma | Realidade |
|---|---|---|
| `NoticiasPortalController.php:150-152` | "Preview de notícia com token de rascunho" | Sem token funcional; 500 por método ausente. |
| `Routes/cms.php:106-119` | Rotas de categorias com `index`, `json` e `store` | `POST /categorias` duplicado; `.index` morto. |
| `Views/cms/index.blade.php:204` | Link de preview via `getPreviewUrl()` | Método inexistente (guardado por `draft_token`, sempre nulo). |

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `route('v2.noticias.cms.index')` | nome | — | módulo ativo + permissão | sidebar, redirects |
| `route('v2.noticias.cms.create')` | nome | — | idem | sidebar, botão "criar" |
| `route('v2.noticias.cms.edit', ['id'=>...])` | nome | — | idem | listagem, redirects de update/restore |
| `route('v2.noticias.cms.archived')` | nome | — | nacional | sidebar (submenu) |
| `route('v2.noticias.cms.categories')` | nome | — | nacional | sidebar (submenu) |
| `route('v2.noticias.cms.preview-iframe', ['id'=>...])` | nome | — | idem | editor (iframe) |
| `route('v2.noticias.portal.show', ['slug'=>...])` | nome | — | notícia publicada | `News::getPublicUrl()`, feed, views |
| `route('v2.noticias.portal.index')` | nome | — | — | JSON-LD do portal, menus |
| `route('v2.noticias.sitemap.news')` | nome | — | — | `robots.txt`/sitemap index (externo) |

### 4.2 Contrato de UI/JS (quando aplicável)

- Endpoints AJAX usados pelas telas: `GET /cms/noticias/tags` (lista), `POST /cms/noticias/tags` (`{name}`), `DELETE /cms/noticias/tags/{id}`, `GET /cms/noticias/categorias/json`, `POST /cms/noticias/categorias`, `PUT/POST /cms/noticias/categorias/{id}`, `DELETE /cms/noticias/categorias/{id}`, `POST /cms/noticias/{id}/toggle-highlight`. Todos retornam JSON e exigem o cookie de sessão + token CSRF do grupo `web`.
- Formulários de notícia são submissões clássicas (`POST`/`PUT` com redirect), não AJAX.
- `preview-iframe` é carregada em `<iframe>` e o editor envia `postMessage` (contrato visual em [`cms.md`](cms.md)).

### 4.3 Ordem e ciclo de vida

1. `web` (sessão, CSRF, cookies) → `module.active:Noticias` → `CmsAuthenticate` → (`RegionalRestrictMiddleware`) → controller.
2. Público: `web` → `MaintenanceMiddleware` → `page.cache` → `public.cache` → controller.
3. O catch-all `/{slug}` é registrado depois de todos os módulos; se `/noticias` não existisse, o fallback tentaria uma `BasicPage` de slug `noticias`.

### 4.4 Receitas de uso

**Chamar o endpoint de destaque com CSRF (JS):**

```js
await fetch(`/cms/noticias/${id}/toggle-highlight`, {
  method: 'POST',
  headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content},
  credentials: 'same-origin'
}).then(r => r.json());      // { success: true, is_highlighted: true }
```

**Criar categoria via HTTP:**

```http
POST /cms/noticias/categorias
Content-Type: application/x-www-form-urlencoded
X-CSRF-TOKEN: <token>

name=Entrevistas&parent_id=
```

Resultado: `200` com o JSON da categoria criada (`slug` gerado por `str_slug`).

**Publicar uma notícia existente:**

```http
PUT /cms/noticias/editar/0192f0c4-...
title=...&status=published&published_at_date=2026-10-21 09:00&content={"type":"doc","content":[]}
```

Resultado: `302` para a edição; `published_at` definido; item no feed/índice público.

### 4.5 Anti-uso

1. **Consumir `POST /cms/noticias/categorias` esperando JSON de listagem** — a rota `categories.index` não existe; ela sempre cria.
2. **Colocar a rota pública de notícias sob prefixo** — quebraria URLs indexadas e a canonical/JSON-LD do portal (`route('...portal.show')`).
3. **Chamar `toggle-highlight` com `Accept: text/html`** — a resposta é JSON cru; o navegador exibe o corpo.

### 4.6 Custos e limites

- Lista CMS: 15 itens/página; `search` com 5 `ILIKE` + 2 `EXISTS`; contadores com 4 `COUNT`.
- Portal: sem paginação; resposta cresce com o acervo (mitigada por page cache de 5 min e `Cache-Control: public, max-age=60`).
- Sitemap: 1.000 URLs; 2 níveis de cache.
- Nenhum throttle; nenhum limite de corpo além do `post_max_size` do PHP.

## 5. Rotas e contratos HTTP (tabela completa)

Middleware comum às rotas CMS: `web`, `module.active:Noticias`, `CmsAuthenticate` (+ `RegionalRestrictMiddleware` nas marcadas "nacional"). Todas usam o guard Sentinel por sessão; CSRF ativo (grupo `web`). Nenhuma tem throttle ou cache.

| # | Tipo | Método | URI | Nome | Middleware extra | Controller@ação | Permissão | Requisição | Resposta | Fonte |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | grupo | — | `cms/noticias` | — | — | — | `Noticias` | — | — | `Routes/cms.php:10-13` |
| 2 | endpoint | GET | `/cms/noticias` | `v2.noticias.cms.index` | — | `NoticiasCmsController@index` | `Noticias` (read) | `status`, `category_id`, `tag_id`, `search`, `page` | HTML `v2-noticias::cms.index` | `Routes/cms.php:16-19` |
| 3 | endpoint | GET | `/cms/noticias/novo` | `v2.noticias.cms.create` | — | `@create` | read | — | HTML `cms.create`; executa `syncAll()` | `Routes/cms.php:21-24` |
| 4 | endpoint | POST | `/cms/noticias/novo` | `v2.noticias.cms.store` | — | `@store` | write | `NewsRequest` + `gallery_ids[]` + `metadata[]` | `302` → index; flash `created_item` | `Routes/cms.php:26-29` |
| 5 | endpoint | GET | `/cms/noticias/editar/{id}` | `v2.noticias.cms.edit` | — | `@edit` | read + `authorize` | `{id}` uuid | HTML `cms.edit` | `Routes/cms.php:31-34` |
| 6 | endpoint | PUT/POST | `/cms/noticias/editar/{id}` | `v2.noticias.cms.update` | — | `@update` | write + `authorize` | `NewsRequest` + `gallery_ids[]` | `302` → edit; flash `Notícia atualizada com sucesso!` | `Routes/cms.php:36-39` |
| 7 | endpoint | GET | `/cms/noticias/preview-iframe/{id}` | `v2.noticias.cms.preview-iframe` | — | `@previewIframe` | read + `authorize` | `{id}` | HTML `cms.preview-iframe` | `Routes/cms.php:41-44` |
| 8 | endpoint | DELETE | `/cms/noticias/{id}` | `v2.noticias.cms.destroy` | — | `@destroy` | write + `authorize` | `{id}` | `302` → index; flash `Notícia "<título>" arquivada com sucesso!` | `Routes/cms.php:46-49` |
| 9 | grupo | — | `cms/noticias` (nacional) | — | `RegionalRestrictMiddleware` | — | nacional | — | — | `Routes/cms.php:52-54` |
| 10 | endpoint | POST | `/cms/noticias/{id}/toggle-highlight` | `v2.noticias.cms.toggle-highlight` | nacional | `@toggleHighlight` | nacional | `{id}` | JSON `{success:true,is_highlighted:bool}` | `Routes/cms.php:57-60` |
| 11 | endpoint | POST | `/cms/noticias/set-super-highlight` | `v2.noticias.cms.set-super-highlight` | nacional | `@setSuperHighlight` | nacional | `news_id` opcional | `302` → index; flash de definição ou remoção | `Routes/cms.php:62-65` |
| 12 | endpoint | GET | `/cms/noticias/arquivadas` | `v2.noticias.cms.archived` | nacional | `@archived` | nacional | `page` | HTML `cms.archived` | `Routes/cms.php:68-71` |
| 13 | endpoint | POST | `/cms/noticias/{id}/restaurar` | `v2.noticias.cms.restore` | nacional | `@restore` | nacional | `{id}` | `302` → edit; flash `Notícia reativada como rascunho!` | `Routes/cms.php:73-76` |
| 14 | endpoint | POST | `/cms/noticias/{id}/seo` | `v2.noticias.cms.seo-update` | nacional | `@updateSeo` | nacional | `seo.title` (req.), `seo.description`, `seo.keywords`, `seo.json_ld`, `seo.image_id`, `seo.image_url` | `302` → edit; flash `Metadados de SEO atualizados com sucesso!` | `Routes/cms.php:79-82` |
| 15 | endpoint | GET | `/cms/noticias/tags` | `v2.noticias.cms.tags.index` | nacional | `@tagsIndex` | nacional | — | JSON array de tags | `Routes/cms.php:85-88` |
| 16 | endpoint | POST | `/cms/noticias/tags` | `v2.noticias.cms.tags.store` | nacional | `@tagsStore` | nacional | `name` (req., ≤100) | JSON da tag | `Routes/cms.php:90-93` |
| 17 | endpoint | DELETE | `/cms/noticias/tags/{id}` | `v2.noticias.cms.tags.destroy` | nacional | `@tagsDestroy` | nacional | `{id}` | JSON `{"success":true}` | `Routes/cms.php:95-98` |
| 18 | endpoint | GET | `/cms/noticias/categorias` | `v2.noticias.cms.categories` | nacional | `@categoriesPage` | nacional | — | HTML `cms.categories` | `Routes/cms.php:101-104` |
| 19 | endpoint | GET | `/cms/noticias/categorias/json` | `v2.noticias.cms.categories.json` | nacional | `@categoriesIndex` | nacional | — | JSON array de categorias | `Routes/cms.php:106-109` |
| 20 | endpoint | POST | `/cms/noticias/categorias` | `v2.noticias.cms.categories.store` | nacional | `@categoriesStore` | nacional | `name` (req., ≤100), `parent_id` (uuid existente) | JSON da categoria criada (a rota `:111-114` `.index` é sobrescrita) | `Routes/cms.php:116-119` |
| 21 | endpoint | PUT/POST | `/cms/noticias/categorias/{id}` | `v2.noticias.cms.categories.update` | nacional | `@categoriesUpdate` | nacional | `name`, `parent_id` | JSON da categoria; `403` se `is_system` | `Routes/cms.php:121-124` |
| 22 | endpoint | DELETE | `/cms/noticias/categorias/{id}` | `v2.noticias.cms.categories.destroy` | nacional | `@categoriesDestroy` | nacional | `{id}` | JSON `{"success":true}`; `403` se `is_system` | `Routes/cms.php:126-129` |
| 23 | grupo | — | `/` (portal) | — | `web` | — | pública | — | — | `Routes/web.php:10-12` |
| 24 | endpoint | GET | `/noticias` | `v2.noticias.portal.index` | `page.cache`, `public.cache`, maintenance | `NoticiasPortalController@index` | pública | query ignorada | HTML `portal.index` | `Routes/web.php:15-18` |
| 25 | endpoint | GET | `/noticia/{slug}` | `v2.noticias.portal.show` | idem | `@show` | pública | `{slug}` | HTML `portal.show`; `404` se não publicada | `Routes/web.php:21-24` |
| 26 | endpoint | GET | `/noticia/{slug}/preview` | `v2.noticias.portal.preview` | idem | `@preview` | pública | `token` (não verificado) | **500** (método ausente) | `Routes/web.php:27-30` |
| 27 | endpoint | GET | `/news-sitemap.xml` | `v2.noticias.sitemap.news` | idem + cache próprio | `NoticiasSitemapController@index` | pública | — | `200 application/xml` | `Routes/web.php:33-36` |

> O `PageCacheMiddleware` cobre as quatro rotas públicas (`/noticias`, `/noticia/*`,
> `/news-sitemap.xml`); `cms*` e `api*` estão na lista de exceção.
>
> A declaração `POST /cms/noticias/categorias` de `Routes/cms.php:111-114`
> (`v2.noticias.cms.categories.index`) está no CSV como `...-R28`: ela é sobrescrita
> pela homônima `:116-119` e nunca resolve.

## 6. Dados e modelo de domínio

Ver [`dados.md`](dados.md). Rotas que mais escrevem: #4, #6, #8, #13, #20-#22.

## 7. Fluxos e estados

Ver [`servicos-e-fluxos.md`](servicos-e-fluxos.md). Resumo rota→estado: #4 cria `draft`/`published`; #6 transita status; #8 → arquivado; #13 → `draft`; #11 não muda status.

## 8. Configuração, permissões e flags

- Permissão de módulo `Noticias` em `v2_roles.permissions_v2` (`read`/`write`/`none`), verificada por `V2PermissionMiddleware` para **todas** as rotas CMS.
- `RegionalRestrictMiddleware` nas rotas #10-#22 (12 endpoints).
- Flag `module.active:Noticias` (tabela `v2_module_flags`) em todas as rotas CMS.
- Rotas públicas: nenhuma permissão, nenhuma flag (o `MaintenanceMiddleware` global pode responder 503 quando `maintenance_mode='true'`).

## 9. Observabilidade

- `Log::info('[NoticiasCms] store|update|destroy', [...])` com `module`, `action`, `id`, `title`, `category`, `unidade_regional_id`, `status`, `user`, `ts`, `duration_ms`.
- `Log::info('[NoticiasPortal] show', [...])` (slug, id, category, regional) e `Log::error('[NoticiasPortal] Falha ao renderizar conteúdo')`.
- `Log::warning('[NoticiasCmsController] Falha ao limpar cache de páginas')`.
- Sem log de acesso às rotas de tags/categorias.

## 10. Seeds

Não se aplica a rotas. Categorias iniciais vêm da migration (`dados.md` §9).

## 11. UI CMS

Ver [`cms.md`](cms.md). Contrato de rota→tela: #2 lista; #3 formulário de criação; #5 formulário de edição; #7 iframe de preview; #12 arquivadas; #18 categorias.

## 12. UI Portal

Ver [`portal.md`](portal.md). Contrato de rota→página: #24 listagem; #25 detalhe; #27 sitemap.

## 13. Lacunas

- `LG-NOTICIAS-R01` — `GET /noticia/{slug}/preview` não tem contrato observável (500). Onde procurar: histórico da coluna `draft_token`, migration de aprovação removida.
- `LG-NOTICIAS-R02` — Não há evidência de que `v2.noticias.cms.categories.index` seja usada por algum consumidor externo; o nome é criado e destruído no boot.
- `LG-NOTICIAS-R03` — Não está claro se o sitemap deveria ser excluído de `page.cache`; hoje não é.

## 14. Checklist de paridade

- [ ] `PC-NOTICIAS-R01` — `GET /cms/noticias` deve paginar em 15 com filtros `status/category_id/tag_id/search`. Fonte: `Controllers/NoticiasCmsController.php:62-105`.
- [ ] `PC-NOTICIAS-R02` — `POST /cms/noticias/novo` deve retornar 302 para o index. Fonte: `Routes/cms.php:26-29`; `tests/Feature/V2/Modules/Publicacoes/Noticias/NoticiasCmsStoreTest.php:120-140`.
- [ ] `PC-NOTICIAS-R03` — `DELETE /cms/noticias/{id}` deve fazer soft delete. Fonte: `Controllers/NoticiasCmsController.php:524-545`.
- [ ] `PC-NOTICIAS-R04` — `GET /cms/noticias/arquivadas` deve responder 302 ao dashboard para operador regional. Fonte: `RegionalRestrictMiddleware.php:34-48`.
- [ ] `PC-NOTICIAS-R05` — `POST /cms/noticias/categorias` deve criar categoria (não listar). Fonte: `Routes/cms.php:111-119`.
- [ ] `PC-NOTICIAS-R06` — `POST /cms/noticias/{id}/seo` deve exigir `seo.title`. Fonte: `Controllers/NoticiasCmsController.php:652-660`.
- [ ] `PC-NOTICIAS-R07` — `GET /noticias` deve devolver todas as publicadas independentemente da query. Fonte: `NoticiasPortalController.php:62-91`.
- [ ] `PC-NOTICIAS-R08` — `GET /noticia/{slug}` de rascunho deve retornar 404. Fonte: `NoticiasPortalController.php:104`.
- [ ] `PC-NOTICIAS-R09` — `GET /news-sitemap.xml` deve responder `application/xml` com cache `v2_news_sitemap_xml`. Fonte: `NoticiasSitemapController.php:23-38`.

## 15. Evidências consultadas

- `V2/Modules/Publicacoes/Noticias/Routes/cms.php:1-133`.
- `V2/Modules/Publicacoes/Noticias/Routes/web.php:1-37`.
- `V2/Core/Providers/V2ServiceProvider.php:286-313`.
- `V2/Core/Middleware/CmsAuthenticate.php:18-45`.
- `V2/Core/Middleware/V2PermissionMiddleware.php:40-136`.
- `V2/Core/Middleware/RegionalRestrictMiddleware.php:34-70`.
- `V2/Core/Middleware/ModuleAccessMiddleware.php:34-64`.
- `V2/Core/Middleware/PageCacheMiddleware.php:11-91`.
- `vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php:50-77,120-129`.
- `vendor/laravel/framework/src/Illuminate/Routing/Route.php:744-780`.
- `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:62-160,236-350,400-545,588-683,703-855`.
- `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasPortalController.php:62-166`.
- `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasSitemapController.php:16-110`.
