---
id: MOD-CONEXOES-UNIDADEREGIONAL-PORTAL
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-UNIDADEREGIONAL]
fontes:
  - V2/Modules/Conexoes/UnidadeRegional/
depende_de: [MOD-CONEXOES-UNIDADEREGIONAL, UI-PORTAL, SVC-SEO, SVC-SEARCH, SVC-MEDIA, SVC-GALLERY, SVC-CONTENTRICHEDITOR, MOD-PUBLICACOES-NOTICIAS]
consumidores: [UI-PORTAL, SVC-SEO, SVC-SEARCH]
criticidade: media
dono: Regional
---

# Conexoes/UnidadeRegional — portal público

> Páginas `/regionais` e `/regional/{slug}`, SEO, cache, galeria, associados client-side e
> notícias da regional. Contratos de rota em [`rotas.md`](rotas.md); dados em
> [`dados.md`](dados.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Publica a vitrine de cada unidade regional ativa, com descrição rica, informações de contato, galeria, associados ativos e as notícias publicadas da regional. |
| Entrada | `GET /regionais?page=N` e `GET /regional/{slug}`; dados de `v2_unidade_regionais`, `v2_unidade_regional_media`, `v2_unidade_regional_empresas`, `v2_associado_instituicoes`, `v2_news` e `v2_portal_configs`. |
| Saída | HTML no layout `v2-portal::app-layout`; JSON embutido `#ur-associados-data`; schema.org `Organization`; modal de galeria e modal de associado. |
| Efeitos colaterais | Page cache Redis 5 min; `Cache-Control: public, max-age=60, must-revalidate`; log `[UnidadeRegionalPortal] show`; request XHR na modal de associado. |
| Particularidade central | O grid de associados é **renderizado no cliente** a partir de JSON embutido, com busca por **regex** e paginação “Ver mais” de 20 em 20, sem endpoint de listagem. |
| Como se integra | Namespace `v2-unidaderegional::portal.*`; `SEOService`; `ContentRichEditorRendererService`; `v2-service-media::components.picture`; layout `v2-portal::app-layout`. |
| Se removido, quebra | Navegação pública de regionais, URLs do sitemap e o destino de `/regional/{slug}` referenciado por notícias e busca. |
| Superfície pública | Duas rotas nomeadas, contrato de IDs/JSON interno e variáveis de view. |

## 1. Efeito

### 1.1 O que o visitante consegue fazer

| Ação | Página | Resultado |
|---|---|---|
| listar regionais | `/regionais` | cards com logo, nome, cidade/UF, resumo e link de detalhe; 12 por página |
| abrir uma regional | `/regional/{slug}` | hero com capa, logo, sigla, cidade/UF e resumo |
| ler a descrição | `/regional/{slug}` | HTML rico (Tiptap) ou texto com quebras preservadas |
| ver contato | `/regional/{slug}` | card “Informações” com sigla, UF, cidade, endereço, telefone, e-mail, site e redes sociais |
| abrir a galeria | `/regional/{slug}` | modal com navegação anterior/próxima e teclado |
| buscar associados | `/regional/{slug}` | filtro instantâneo via regex sobre nome/cidade/tags/sigla |
| ver mais associados | `/regional/{slug}` | revela mais 20 cards |
| abrir detalhe de associado | `/regional/{slug}` | modal carrega HTML de `v2.associados.portal.detalhe` |
| ler notícias | `/regional/{slug}` | destaque + grid com até 11 adicionais e link “Ver as notícias da regional” |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Cache Redis `page:<sha1(fullUrl)>` por 5 min + `X-Cache-Status` | `PageCacheMiddleware` | `V2/Core/Middleware/PageCacheMiddleware.php:14-16,45-70` |
| `Cache-Control: public, max-age=60, must-revalidate` e `Vary: Accept-Encoding` | `PublicCacheMiddleware` | `V2/Core/Middleware/PublicCacheMiddleware.php:83-91` |
| Título/descrição/keywords no `SEOService` | `index()`/`show()` | `Controllers/UnidadeRegionalPortalController.php:32-35,53-56` |
| Metadados `metadata.seo` sobrescrevem título/descrição | `SEOService::bindModel()` | `V2/Services/SEO/Services/SEOService.php:29-58` |
| Schema.org `Organization` | `show.blade.php` via `schemas` do layout | `Views/portal/show.blade.php:14-22` |
| `gtag.page_context` = `unidades-regionais` / `unidade-regional` | layout | `Views/portal/index.blade.php:8`; `Views/portal/show.blade.php:12` |
| Log `[UnidadeRegionalPortal] show` com contagens | `show()` | `Controllers/UnidadeRegionalPortalController.php:79-84` |
| Nenhuma escrita no banco | — | controllers só leem |

### 1.3 Contrato observável (URL → página)

| URL | Resultado |
|---|---|
| `/regionais` | `200`; hero com fundo configurado; cards das ativas; 12/página |
| `/regionais?page=2` | `200`; próxima página (page cache distinto por URL) |
| `/regionais` sem unidades ativas | `200` com “Nenhuma regional encontrada” |
| `/regional/{slug}` | `200`; página completa |
| `/regional/{slug}` de unidade inativa | `404` |
| `/regional/{slug}` inexistente | `404` |
| `/regional/{slug}` sem descrição | `200`; card de informações centralizado em coluna única |
| `/regional/{slug}` sem notícias | `200` com “Nenhuma notícia publicada para esta regional ainda.” |
| `/regional/{slug}` sem associados | `200` com “Nenhum associado vinculado a esta regional.” |

### 1.4 O que o portal NÃO faz

- Não exibe unidades **inativas** nem **arquivadas** (`active()`).
- Não lista notícias `pending_approval`/`draft` (`published()`).
- Não exibe associados com `is_active = false`.
- Não pagina os associados no servidor: o JSON embute **todos** os ativos.
- Não exibe a lista completa de notícias: limita a 12 e delega ao módulo Notícias.
- Não tem formulário de contato nem mapa embutido (`map_coordinates` não é renderizado).
- Não usa `metadata.cover_image` no card da lista (só no hero do detalhe).

## 2. Abordagem

### 2.1 Estratégia em uma página

```mermaid
flowchart LR
  R1[/regionais] --> PC[page.cache] --> C1[PortalController@index]
  R2[/regional/slug] --> PC --> C2[PortalController@show]
  C1 --> Q1[UnidadeRegional active ordered paginate 12]
  C1 --> BG[v2_portal_configs.theme_regionais_bg]
  C2 --> U[unidade + media + empresas.empresa.tags]
  C2 --> N[News published byRegional take 12]
  C2 --> H[Tiptap → HTML]
  C2 --> J[JSON de associados]
  C2 --> SEO[SEOService title/description/bindModel]
  C1 --> V[view portal.index]
  C2 --> V2[view portal.show]
```

### 2.2 Mecânica passo a passo

1. Middleware `page.cache` consulta Redis; HIT devolve HTML com `X-Cache-Status: HIT`.
2. `index()`: `active()->ordered()->paginate(12)`; define SEO; lê `theme_regionais_bg` de
   `v2_portal_configs` (`whereNull('deleted_at')->pluck(...)`) com fallback
   `asset('assets/img/hero/breadcrumb.png')`; devolve `portal.index`.
3. `show($slug)`: `active()->where('slug',$slug)->with(['media','empresas.empresa.tags'])->firstOrFail()`.
4. SEO: título `"<nome> — Regionais SUCESU — <app.name>"`, descrição `strip_tags(metadata.resumo ??
   'Regional SUCESU')`, keywords com sigla; `bindModel($unidade)` aplica `metadata.seo`.
5. Conteúdo: se `description` decodifica como JSON com `type`, usa
   `ContentRichEditorRendererService::toHtml()`; se for string, escapa e aplica `nl2br`.
6. Notícias: `News::published()->byRegional($id)->with('coverImage')->orderByDesc('published_at')->take(12)`.
7. Associados: `$unidade->empresas->sortByDesc('created_at')->pluck('empresa')->filter(is_active)`.
8. `buildAssociadosJson`: monta `search` (nome, category, cidade, sigla, UF, nome da unidade,
   tags) em minúsculas, codifica com flags de escape e substitui `</` por `<\/`.
9. Capa: `metadata.cover_image` → `v2_storage_url()` + variantes WebP via `GalleryMedia` por
   `file_path`; hero usa `v2_bg_image_css()` ou gradiente do tenant.
10. Redes: `getOrderedSocialMedia()` ordena por lista fixa e ignora redes sem `url`.
11. View renderiza seções; JS inicializa grid, busca, “Ver mais”, modais e galeria.

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

- **JSON embutido + render no cliente** — evita endpoint e permite busca instantânea; custo é
  transferir todos os associados na página. `[C]`
- **Busca por regex** — aceita padrões digitados pelo usuário (`new RegExp(term,'i')`), com
  fallback escapado quando a regex é inválida. `[C]`
- **Capa com `image-set()`** — usa a maior variante WebP com fallback para o original
  (`v2_bg_image_css`). `[C]`
- **Fallback de fundo por gradiente do tenant** — mantém a identidade visual quando não há capa. `[C]`
- **Limite de 12 notícias** — evita N+1 e mantém a página leve; o restante fica no módulo de
  Notícias. `[C]`
- **Modal de associado via XHR para o portal de Associados** — reutiliza a view de detalhe sem
  duplicar markup. `[C]`

### 2.4 Trechos de código pivô

```php
// V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php:47-56 — resolução e SEO
$unidade = UnidadeRegional::active()
    ->where('slug', $slug)
    ->with(['media', 'empresas.empresa.tags'])
    ->firstOrFail();

$this->seo->setTitle($unidade->name . ' — Regionais SUCESU — ' . config('app.name'));
$this->seo->setDescription(strip_tags($unidade->metadata['resumo'] ?? 'Regional SUCESU'));
$this->seo->setKeywords('regional, ' . $unidade->name . ', ' . ($unidade->acronym ?? '') . ', SUCESU');
$this->seo->bindModel($unidade);
```

`bindModel` roda **depois** dos setters, portanto `metadata.seo.*` sobrepõe os valores calculados.

```php
// V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php:57-70 — descrição rica com fallback
$htmlContent = '';
if (!empty($unidade->description)) {
    try {
        $decoded = json_decode($unidade->description, true);
        if (is_array($decoded) && isset($decoded['type'])) {
            $renderer = app(ContentRichEditorRendererService::class);
            $htmlContent = $renderer->toHtml($decoded);
        } elseif (is_string($unidade->description)) {
            $escaped = e($unidade->description);
            $htmlContent = '<p>' . nl2br($escaped) . '</p>';
        }
    } catch (\Exception $e) {
        \Log::error('[UnidadeRegionalPortal] Erro ao renderizar descrição: ' . $e->getMessage());
    }
}
```

O `elseif` é alcançável quando o JSON é válido mas não tem `type` (ex.: `"texto"` entre aspas) —
nesse caso `is_string($unidade->description)` continua verdadeiro e o texto cru é escapado.

```php
// V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php:122-164 — JSON de associados
$dtos = $associados->map(function ($empresa) use ($unidade) {
    $metadata = $empresa->metadata;
    $city = '';
    if (is_array($metadata)) {
        $city = isset($metadata['city']) ? $metadata['city'] : (isset($metadata['cidade']) ? $metadata['cidade'] : '');
    }

    $tagNames = [];
    if ($empresa->tags) {
        $tagNames = $empresa->tags->pluck('name')->filter()->all();
    }

    $searchParts = array_merge([
        $empresa->name,
        $empresa->category,
        $city,
        $unidade->acronym,
        $unidade->state,
        $unidade->name,
    ], $tagNames);

    $search = mb_strtolower(trim(preg_replace('/\s+/u', ' ', implode(' ', array_filter($searchParts)))));

    return [
        'id' => $empresa->id,
        'name' => $empresa->name,
        'logo' => $empresa->logo ? v2_storage_url($empresa->logo) : null,
        'search' => $search,
    ];
})->values()->all();
```

`$empresa->category` não existe como coluna (foi removida) e contribui com `null`, descartado
por `array_filter`. `logo` já vai como URL absoluta.

```js
// V2/Modules/Conexoes/UnidadeRegional/Views/portal/show.blade.php:384-404 — busca por regex e reset de paginação
function applySearch() {
    var term = searchInput ? (searchInput.value || '').trim() : '';
    if (term === '') {
        filtered = DATA;
    } else {
        var rx;
        try {
            rx = new RegExp(term, 'i');
        } catch (e) {
            rx = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
        }
        filtered = DATA.filter(function(item) {
            return rx.test(item.search || '');
        });
    }
    visible = PAGE;
    render();
}
```

Busca é **regex**, não substring: `.*` funciona, e um parêntese não balanceado cai no fallback
escapado.

```js
// V2/Modules/Conexoes/UnidadeRegional/Views/portal/show.blade.php:452-475 — modal de associado via XHR
var detalheUrlBase = '{{ route("v2.associados.portal.detalhe", ["id" => "__ID__"]) }}';

modalEl.addEventListener('show.bs.modal', function(event) {
    var trigger = event.relatedTarget;
    if (!trigger) return;
    var id = trigger.getAttribute('data-id');
    if (!id) return;
    var url = detalheUrlBase.replace('__ID__', id);
    ...
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function() {
        if (xhr.status >= 200 && xhr.status < 300) {
            modalBody.innerHTML = xhr.responseText;
            var firstFocusable = modalBody.querySelector('button, a, input, [tabindex]:not([tabindex="-1"])');
            if (firstFocusable) {
                setTimeout(function() { firstFocusable.focus(); }, 200);
            }
        }
```

O `id` vem de `data-id` gerado a partir do JSON (UUID do associado) e é interpolado na URL.

### 2.5 Algoritmos, consultas e custos

- `/regionais`: 1 `COUNT` + 1 página; 1 query de configuração; page cache de 5 min.
- `/regional/{slug}`: unidade + `media` + `empresas` + `empresa` + `tags` (eager loading),
  1 query de notícias, 1 query de `GalleryMedia` (capa) e 1 query de configuração implícita no
  layout (menu/SEO).
- Filtro client-side é O(n) por tecla (`input`), sem debounce.
- JSON payload: 1 objeto por associado ativo (id, name, logo, search) — cresce linearmente.
- Galeria: `picture` renderiza variantes para cada item; `loading=lazy` nos cards.
- `page.cache` usa `sha1(fullUrl)`, então cada `?page=` cria uma entrada por 5 min.

## 3. Particularidades

### 3.1 O que só existe aqui

- JSON `#ur-associados-data` com campo agregado `search`.
- Busca por regex + “Ver mais” sem endpoint.
- Ordem fixa de redes sociais (`instagram, linkedin, facebook, twitter, youtube, whatsapp, telegram`).
- Hero com `image-set()` WebP a partir das variantes da mídia de capa.
- Modal de associado que reutiliza a rota de detalhe do módulo Associados.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-UNIDADEREGIONAL-P01 | Só unidades `is_active = true` aparecem; detalhe usa `firstOrFail` | inativa = 404 | `Controllers/UnidadeRegionalPortalController.php:30,47-51` |
| RN-UNIDADEREGIONAL-P02 | Só notícias `scopePublished` são exibidas, ordenadas por `published_at desc`, limite 12 | `pending_approval` nunca aparece | `:72-77`; `News.php:391-393` |
| RN-UNIDADEREGIONAL-P03 | Associados são filtrados por `is_active` **em memória** | vínculo com associado inativo não aparece | `:64-69` |
| RN-UNIDADEREGIONAL-P04 | `search` é montado com nome, category, cidade, sigla, UF, unidade e tags | busca encontra por qualquer um | `:126-145` |
| RN-UNIDADEREGIONAL-P05 | `PAGE = 20` e “Ver mais” só aparece se `count > 20` | 20 iniciais | `Views/portal/show.blade.php:172-177,329-330` |
| RN-UNIDADEREGIONAL-P06 | Busca não faz debounce e roda no evento `input` | cada tecla re-renderiza | `:408-412` |
| RN-UNIDADEREGIONAL-P07 | Fundo do índice vem de `theme_regionais_bg` ou do asset de breadcrumb | configuração global impacta todas as regionais | `Controllers/UnidadeRegionalPortalController.php:37-45` |
| RN-UNIDADEREGIONAL-P08 | Capa do detalhe não usa fallback de asset: sem capa → gradiente do tenant | visual consistente sem imagem | `Views/portal/show.blade.php:29-36` |
| RN-UNIDADEREGIONAL-P09 | `metadata.seo` sobrepõe SEO calculado | título/OG vêm do JSON da unidade, se existir | `SEOService.php:29-58` |
| RN-UNIDADEREGIONAL-P10 | Redes sociais sem `network` ou `url` são descartadas | builder aceita linhas incompletas | `Controllers/UnidadeRegionalPortalController.php:171-195` |

### 3.3 Casos-limite e surpresas

- **Unidade sem `metadata.resumo`**: descrição SEO vira `Regional SUCESU`; o card da lista
  usa o texto padrão “Regional SUCESU oferecendo benefícios e representação regional.”
- **`metadata.resumo` com HTML**: `strip_tags` remove no SEO, mas o card usa `str_limit` sem
  escape? Usa `{{ }}` (escapado) — seguro.
- **Associado arquivado**: `pluck('empresa')` pode conter `null`; o filtro `$empresa !== null`
  remove.
- **Associado sem logo**: card mostra ícone `bi-building`.
- **Descrição inválida (JSON quebrado)**: `json_decode` devolve `null`; cai no `elseif` de string
  e renderiza o texto cru escapado.
- **Galeria com 1 item**: navegação anterior/próxima dá a volta no mesmo item.
- **Teclado na galeria**: setas só funcionam com o modal aberto (`.show`).
- **Busca com termo inválido**: fallback escapa metacaracteres e busca literal.
- **`?page=` acima do total**: Laravel devolve página vazia, sem 404.
- **Sitemap**: inclui unidades inativas (ver `rotas.md` §3.3) → URL do sitemap pode dar 404.
- **Link “Ver as notícias da regional”**: aponta com `?regional=<slug>`, mas a página de
  Notícias ignora o parâmetro — mostra todas as publicadas.

### 3.4 Armadilhas de reimplementação

1. **Trocar a busca regex por `includes`**: muda o comportamento (`.*`, `^`, `|`).
2. **Paginar associados no servidor**: quebra o JSON e o “Ver mais”.
3. **Esquecer o escape de `</`**: o JSON inline poderia fechar o `<script>`.
4. **Usar `metadata.cover_image` no card da lista**: hoje só o detalhe usa capa.
5. **Renderizar `description` sem o renderer Tiptap**: nós `contentRichMedia` apareceriam como JSON.
6. **Assumir que o layout aplica SEO**: título/description vêm do `SEOService` compartilhado.

### 3.5 Divergências e dívidas

- `@php` define `$pageTitle`/`$metaDescription` e o layout recebe `pageTitle`/`pageDescription`
  com `str_limit(...,160)` no detalhe, mas o `SEOService` também define título — duas fontes
  concorrentes (o layout usa `pageTitle`; o SEO é consumido pelo `<head>` do layout).
- O card da lista usa `str_limit($resumo, 250)` mesmo o CMS limitando a 250 — redundante.
- `category` no payload de busca é sempre `null` (coluna removida).
- O iframe de preview do CMS carrega esta página com `sandbox="allow-scripts allow-same-origin"`.
- `map_coordinates` não é usado no portal, apesar de existir no formulário.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `portal.index` | view | `$unidades` (paginator), `$regionaisBg` | rota `/regionais` | layout portal |
| `portal.show` | view | `$unidade`, `$htmlContent`, `$noticias`, `$coverBg`, `$coverBgPath`, `$coverBgVariants`, `$associados`, `$associadosJson`, `$orderedSocialMedia` | rota `/regional/{slug}` | layout portal |
| `portal.partials.informacoes` | partial | `$unidade`, `$orderedSocialMedia` | — | `show` |
| `#ur-associados-data` | contrato JS | JSON `[{id,name,logo,search}]` | — | JS inline |
| `v2.associados.portal.detalhe` | rota externa | `GET /associado/{id}` (HTML) | associado existente | modal |

### 4.2 Contrato de UI/JS

| Elemento | Contrato |
|---|---|
| `#ur-associados-data` | `<script type="application/json">` com array; parse tolerante a falha (`[]`) |
| `#ur-associados-grid` | container do grid; preenchido por `innerHTML` |
| `#ur-associados-search` | input de busca; evento `input` e botão `.search-form-btn` |
| `.js-associados-more` | botão “Ver mais”; esconde via `d-none` quando não há mais |
| `#ur-associados-count` | badge com o total filtrado |
| `.unidade-regional-associado-card` | `data-id`, `role=button`, `tabindex=0`; Enter/Espaço acionam click |
| `#associadoRegionalModal` | `show.bs.modal` dispara XHR e injeta HTML; foco no primeiro focável após 200 ms |
| `#galleryModal` | thumbnails `.gallery-thumb` com `data-index`; `.gallery-prev`/`.gallery-next`; setas do teclado |
| `.gallery-modal-img` | recebe `src` da variante atual |

**CSS**: variáveis `--tenant-primary`, `--tenant-secondary`, `--color-bg-canvas`,
`--color-bg-main`, `--color-text-main`; classes `search-form-group`, `search-form-input`,
`search-form-btn`, `unidade-regional-associados-grid`, `unidade-regional-associado-*`,
`unidade-regional-news*`, `team-box-items`, `news-card-items`, `breadcrumb-wrapper`.

**Acessibilidade**: labels visuais ocultos (`visually-hidden`), `aria-label` nos cards e botões,
`role=status` + `visually-hidden` no spinner do modal, foco movido para o conteúdo, setas da
galeria, `alt` da imagem de galeria (“Galeria <nome>”), `loading=lazy`/`decoding=async`.

### 4.3 Ordem e ciclo de vida

1. Layout `v2-portal::app-layout` fornece `<head>` com SEO/meta/OG/JSON-LD e Bootstrap.
2. `page.cache` antes do controller; `public.cache` depois da resposta.
3. O controller resolve SEO e dados; a view só renderiza.
4. Scripts são empilhados em `@push('scripts')` e executam em `DOMContentLoaded`.
5. A modal de associado depende da rota do módulo Associados estar registrada.

### 4.4 Receitas de uso

```blade
{{-- Link para a página de uma regional a partir de qualquer view --}}
<a href="{{ route('v2.unidade-regional.portal.show', ['slug' => $unidade->slug]) }}">{{ $unidade->name }}</a>
```

Resultado: `/regional/<slug>`.

```php
// Consulta equivalente à lista pública
$unidades = \V2\Modules\Conexoes\UnidadeRegional\Models\UnidadeRegional::active()
    ->ordered()
    ->paginate(12);
```

Resultado: mesmas 12 unidades e mesma ordem da página.

```js
// Forçar a busca programaticamente no grid
var input = document.getElementById('ur-associados-search');
input.value = 'DF';
input.dispatchEvent(new Event('input'));
```

Resultado: grid filtrado pela regex `DF` (case-insensitive).

### 4.5 Anti-uso

- **Adicionar paginação server-side de associados** sem mudar o JS: “Ver mais” deixa de funcionar.
- **Retornar HTML não escapado em `search`**: o JSON usa `JSON_HEX_*`, mas a renderização
  client-side depende de `esc()`.
- **Confiar em `?regional=` na página de notícias**: parâmetro ignorado.

### 4.6 Custos e limites

- Page cache 5 min; `Cache-Control` 60 s (browser/CDN).
- 12 unidades por página; 12 notícias por regional; 20 associados por lote.
- JSON de associados sem limite de tamanho (cresce com a base).
- `take(12)` sem paginação de notícias no detalhe.

## 5. Rotas e contratos HTTP

| Método | URI | Nome | Middleware | Controller | Query | Cache | Fonte |
|---|---|---|---|---|---|---|---|
| GET | `/regionais` | `v2.unidade-regional.portal.index` | `web`, maintenance, `page.cache`, `public.cache` | `UnidadeRegionalPortalController@index` | `page` | Redis 5 min + `max-age=60` | `Routes/web.php:7-10` |
| GET | `/regional/{slug}` | `v2.unidade-regional.portal.show` | idem | `@show` | — | idem | `Routes/web.php:12-15` |

Detalhe de middleware e demais rotas em [`rotas.md`](rotas.md).

## 6. Dados e modelo de domínio

| Fonte | Uso na página |
|---|---|
| `v2_unidade_regionais` | nome, slug, sigla, UF, cidade, endereço, telefone, e-mail, site, logo, `metadata` |
| `v2_unidade_regional_media` | galeria (com `galleryMedia->file_path`/`variants`) |
| `v2_unidade_regional_empresas` | vínculo com associados e `status` |
| `v2_associado_instituicoes` | nome, logo, `is_active`, `metadata`, tags |
| `v2_news` | notícias publicadas (`byRegional`, inclui FK e metadata) |
| `v2_gallery_media` | variantes da capa e das imagens da galeria |
| `v2_portal_configs` | `theme_regionais_bg` |

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant V as Visitante
  participant P as page.cache
  participant C as PortalController
  participant DB as Banco
  V->>P: GET /regional/sucessu-df
  alt HIT
    P-->>V: HTML + X-Cache-Status: HIT
  else MISS
    P->>C: passa adiante
    C->>DB: unidade ativa + mídia + empresas + tags
    C->>DB: notícias publicadas (12)
    C->>C: SEO + HTML da descrição + JSON
    C-->>P: HTML
    P->>P: grava page:<sha1> por 5 min
    P-->>V: HTML + X-Cache-Status: MISS
  end
```

- **FL-UNIDADEREGIONAL-P01 (listar)**: cache → query paginada → configuração de fundo → view.
- **FL-UNIDADEREGIONAL-P02 (detalhar)**: cache → `firstOrFail` → SEO → descrição → notícias →
  associados → JSON → view.
- **FL-UNIDADEREGIONAL-P03 (buscar associado)**: `input` → regex → filtro → `render()`.
- **FL-UNIDADEREGIONAL-P04 (abrir associado)**: click/Enter → `show.bs.modal` → XHR →
  `innerHTML` → foco.

## 8. UI CMS

A edição da regional tem preview desta página e campos que alimentam o portal — ver
[`cms.md`](cms.md) §8.2.

## 9. UI Portal

### 9.1 `/regionais` — índice

- **Hero**: `div.breadcrumb-wrapper.bg-cover` com `background-image: url(<regionaisBg>)`;
  breadcrumb “Home › Regionais SUCESU”; `<h1>Regionais <span class="text-primary">SUCESU</span></h1>`.
- **Grid**: `section.team-section` com `row g-4`; cada card em `col-md-6` (2 por linha em md+).
- **Card**: logo 185×185 `object-fit: contain` com padding e fundo; `<h3>` com link; badge
  cidade/UF (`bg-primary`); resumo `str_limit(...,250)`; link “Ver Detalhes”.
- **Sem logo**: ícone `bi-geo-alt` 3rem centralizado.
- **Vazio**: ícone 3rem, “Nenhuma regional encontrada”, “Em breve anunciaremos nossas unidades
  regionais.”
- **Paginação**: `v2-portal::partials.pagination`.
- **SEO**: título “Regionais SUCESU — <app.name>”; description fixa; keywords fixas + app.name;
  `gtag.page_context = unidades-regionais`.

### 9.2 `/regional/{slug}` — detalhe

**Ordem das seções**

1. **Hero**: fundo = capa (`v2_bg_image_css`) ou gradiente tenant; logo (máx. 80 px, fundo branco)
   + `<h1>` + badge da sigla (ou “Regional SUCESU”) + badge cidade/UF.
2. **Resumo**: `p.lead` com `metadata.resumo`.
3. **Sobre**: `<h2>` “Sobre a Regional” + `.content-rich-viewer` com HTML do editor; ao lado
   (col-lg-4) o partial de informações. Sem descrição: informações centralizadas.
4. **Galeria**: `<h2>` “Galeria”; grid `col-md-4 col-6` com `picture` e variantes; abre modal.
5. **Associados**: `<h2>` + badge com contagem; busca; grid client-side; “Ver mais”.
6. **Notícias**: `<h2>` “Notícias da Regional”; destaque (primeira por `published_at`) com
   badges “Super-destaque”/“Destaque”, data, título, subtítulo e “Ler mais”; grid com até 11
   itens (dia + mês abreviado em PT, título, resumo de 150 chars, “Leia Mais”); botão
   “Ver as notícias da regional”.
7. **Vazios**: “Nenhum associado vinculado a esta regional.”; “Nenhuma notícia publicada para
   esta regional ainda.”

**SEO**

| Item | Origem |
|---|---|
| `<title>` | `<nome> — Regionais SUCESU — <app.name>` (view) |
| `pageDescription` | `str_limit(strip_tags(metadata.resumo ?? 'Regional SUCESU'), 160)` |
| Título/description/keywords do `SEOService` | setters do controller; `bindModel` sobrepõe com `metadata.seo` |
| JSON-LD | `Organization` com `name`, `description`, `url` |
| Canonical/OG/Twitter | layout do portal (não definidos pelo módulo) |
| `gtag.page_context` | `unidade-regional` |

**Mídia**

- Logo: `loading=eager`, `max-height:80px`, `object-fit:contain`.
- Capa: variantes WebP via `v2_bg_image_css`; sem capa, gradiente.
- Galeria: `picture` com variantes; `alt="Galeria <nome>"`; `loading=lazy`.
- Notícias: `picture` com variantes e alt do cover; fallback `assets/sucesu/img/news-default.jpg`.

**Responsividade/print**: grid Bootstrap (12/6/4 colunas); hero `min-height: 200px`; não há CSS
de impressão específico.

**Cache**: herdado das rotas (5 min Redis + 60 s HTTP).

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

| Item | Efeito | Fonte |
|---|---|---|
| `theme_regionais_bg` (`v2_portal_configs`) | fundo do hero do índice | `Controllers/UnidadeRegionalPortalController.php:38-45` |
| `app.name` | sufixo dos títulos | `:32-34` |
| flag `UnidadeRegional` | desativada → 404 no portal | `ModuleAccessMiddleware` não se aplica ao web; apenas CMS |
| `cache.prefix` | chave real do Redis (`<prefix>:page:*`) | `PageCacheMiddleware` |

Não há permissão de acesso: páginas públicas.

## 11. Observabilidade, erros e limites

- Log `[UnidadeRegionalPortal] show` com `slug`, `regional_id`, `noticias_count`,
  `associados_count` (`Controllers/UnidadeRegionalPortalController.php:79-84`).
- Log de erro `[UnidadeRegionalPortal] Erro ao renderizar descrição`.
- Falha de `v2_portal_configs` é silenciosa (`try/catch` vazio) — usa o asset padrão.
- `404` para slug inválido/inativo.
- Limites: 12 unidades/página, 12 notícias, 20 associados por lote; JSON sem teto.
- Se o Redis cair, `PageCacheMiddleware` loga warning e serve sem cache.

## 12. Seeds e dados iniciais

A unidade `Nacional` (`acronym = 'NAC'`, `state = 'BR'`) é pública em `/regional/nacional`.
Nenhum dado de portal é semeado.

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| LG-UNIDADEREGIONAL-P01 | Não há OG/Twitter específicos da unidade; dependem do layout | `V2/layouts/portal` | compartilhamento genérico |
| LG-UNIDADEREGIONAL-P02 | O link para notícias da regional não filtra | `Views/portal/show.blade.php:293`; `NoticiasPortalController.php:52` | UX enganosa |
| LG-UNIDADEREGIONAL-P03 | `map_coordinates` não renderiza mapa | `Views/portal/partials/informacoes.blade.php` | campo sem efeito público |
| LG-UNIDADEREGIONAL-P04 | Não há limite/compactação do JSON de associados | `buildAssociadosJson` | páginas grandes com muitas empresas |
| LG-UNIDADEREGIONAL-P05 | Não há teste de acessibilidade do modal/galeria | `tests/Feature/.../UnidadeRegionalPortalTest.php` | regressões |
| LG-UNIDADEREGIONAL-P06 | Não se sabe se o layout já injeta canonical/robots | `V2/layouts/portal/app-layout.blade.php` | SEO duplicado/ausente |

## 14. Checklist de paridade

- [ ] `PC-UNIDADEREGIONAL-P01` — `/regionais` lista 12 ativas ordenadas (`Controllers/UnidadeRegionalPortalController.php:30-31`).
- [ ] `PC-UNIDADEREGIONAL-P02` — fundo do hero usa `theme_regionais_bg` ou o asset padrão (`:37-45`).
- [ ] `PC-UNIDADEREGIONAL-P03` — card mostra logo 185×185 com `object-fit:contain` (`Views/portal/index.blade.php:53-63`).
- [ ] `PC-UNIDADEREGIONAL-P04` — vazio exibe “Nenhuma regional encontrada” (`:87-93`).
- [ ] `PC-UNIDADEREGIONAL-P05` — `/regional/{slug}` inativo → 404 (`:47-51`).
- [ ] `PC-UNIDADEREGIONAL-P06` — descrição Tiptap renderiza HTML; texto puro vira `<p>` com `nl2br` (`:57-70`).
- [ ] `PC-UNIDADEREGIONAL-P07` — redes sociais na ordem fixa (`:171-195`).
- [ ] `PC-UNIDADEREGIONAL-P08` — galeria abre modal e navega com setas (`Views/portal/show.blade.php:538-557`).
- [ ] `PC-UNIDADEREGIONAL-P09` — busca filtra pelo campo `search` com regex case-insensitive (`:384-404`).
- [ ] `PC-UNIDADEREGIONAL-P10` — “Ver mais” aparece só com > 20 associados e adiciona 20 (`:166-177,417-421`).
- [ ] `PC-UNIDADEREGIONAL-P11` — clique/Enter no card abre modal com HTML de `v2.associados.portal.detalhe` (`:452-486`).
- [ ] `PC-UNIDADEREGIONAL-P12` — notícias limitadas a 12 e ordenadas por `published_at desc` (`Controllers/UnidadeRegionalPortalController.php:72-77`).
- [ ] `PC-UNIDADEREGIONAL-P13` — `<title>` e `pageDescription` conforme §9.2 (`Views/portal/show.blade.php:4-5,10-11`).
- [ ] `PC-UNIDADEREGIONAL-P14` — JSON-LD `Organization` presente (`:14-22`).
- [ ] `PC-UNIDADEREGIONAL-P15` — resposta traz `X-Cache-Status` e `max-age=60` (`PageCacheMiddleware.php:45-70`; `PublicCacheMiddleware.php:83-91`).

## 15. Evidências consultadas

- `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php:16-195`
- `V2/Modules/Conexoes/UnidadeRegional/Views/portal/index.blade.php:1-98`
- `V2/Modules/Conexoes/UnidadeRegional/Views/portal/show.blade.php:1-700`
- `V2/Modules/Conexoes/UnidadeRegional/Views/portal/partials/informacoes.blade.php:1-88`
- `V2/Modules/Conexoes/UnidadeRegional/Routes/web.php:1-16`
- `V2/Modules/Conexoes/UnidadeRegional/Models/UnidadeRegional.php:156-188,235-248`
- `V2/Core/Middleware/PageCacheMiddleware.php:14-91`
- `V2/Core/Middleware/PublicCacheMiddleware.php:30-91`
- `V2/Services/SEO/Services/SEOService.php:29-58,293-409`
- `V2/Services/SEO/Controllers/SeoPortalController.php:363-395`
- `V2/Services/Search/Services/SearchService.php:74-78`
- `V2/Core/Helpers/helpers.php:259-299`
- `V2/Modules/Publicacoes/Noticias/Models/News.php:150-153,391-402,439-445`
- `V2/Modules/Conexoes/Associados/Models/AssociadoInstituicao.php:23-70`
- `V2/Services/Media/Views/components/picture.blade.php` (componente de imagem responsiva)
- `tests/Feature/V2/Modules/Conexoes/UnidadeRegional/UnidadeRegionalPortalTest.php`
