---
id: MOD-CONEXOES-PARCEIROSINSTITUCIONAIS-PORTAL
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-PARCEIROSINSTITUCIONAIS]
fontes:
  - V2/Modules/Conexoes/ParceirosInstitucionais/
depende_de: [UI-PORTAL, SVC-MEDIA, SVC-STORAGE, SVC-CONTENTRICHEDITOR, SVC-SEO, SVC-SEARCH, MOD-LAYOUTS-HOMEPAGE]
consumidores: [MOD-LAYOUTS-HOMEPAGE, SVC-SEARCH, SVC-SEO]
criticidade: media
dono: ParceirosInstitucionais
---

# Conexões/ParceirosInstitucionais — portal

> Anexo do módulo [`spec.md`](spec.md). Duas superfícies públicas: a página
> `/parceiros-institucionais` (hero breadcrumb + grade 2 colunas + modal) e o fragmento
> `/parceiros-institucionais/detalhe/{id}`, consumido tanto pelo modal da página quanto
> pelo carrossel da Homepage. Não há filtro, busca ou paginação configurável. A Homepage
> tem **dois** caminhos de integração (widget LSE atual e integrador/resolver legados),
> com contratos de item diferentes.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Exibe os parceiros ativos em uma grade paginada e mostra detalhes em modal, sem sair da página. |
| Entrada | `GET /parceiros-institucionais` (query só `page`); `GET /parceiros-institucionais/detalhe/{id}`; widget `sucesu-associados` da Homepage. |
| Saída | HTML completo (index), fragmento HTML (detalhe), JSON-LD `CollectionPage`, slides do carrossel. |
| Efeitos colaterais | Page cache Redis 5 min; `Cache-Control: public, max-age=60`; `trackAccess()` do SEO; inclusão no sitemap. |
| Particularidade central | O detalhe é **fragmento** e não tem layout/SEO; a Homepage usa a mesma rota via `data-url`, então qualquer mudança no fragmento afeta os dois lugares. |
| Como se integra | `ParceirosInstitucionaisPortalController` + views `v2-parceirosinstitucionais::portal.*`; `SucesuAssociadosWidget` no `WidgetRegistry` como `sucesu-associados`. |
| Se removido, quebra | Página pública, modal da Homepage, resultados de busca de parceiros (URL), sitemap (entradas de detalhe). |
| Superfície pública | `portal.index`, `portal.detalhe`, `SucesuAssociadosWidget::prepare`, campos `metadata.social_media`. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

| Perfil | Ação | Resultado observável |
|---|---|---|
| Visitante | Abrir a página de parceiros | Hero "Parceiros" com breadcrumb, grade de cards (2 colunas) com logo 185×185, nome, resumo e ícones sociais. |
| Visitante | Paginar | 12 por página, preservando a query (`appends(request()->except('page'))`). |
| Visitante | Clicar no card | Modal `#parceiroModal` abre com spinner e recebe o fragmento do detalhe via `$.get`. |
| Visitante | Clicar em ícone social do card | Abre o link em nova aba; `event.stopPropagation()` impede o modal. |
| Visitante da Homepage | Clicar em um logo do carrossel | Modal `#parceiroHomeModal` carrega a mesma URL de detalhe e injeta o HTML. |
| Mecanismo de busca | Rastrear | Sitemap inclui a página e um detalhe por parceiro não deletado. |
| Busca interna | Buscar por "parceiro" | Resultado com `result_url` para o detalhe por `entity_id`. |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| HTML da grade com 12 ativos | `index()` | `Controllers/ParceirosInstitucionaisPortalController.php:26-36` |
| Título/description/keywords no `SEOService` | `index()` | `:28-30` |
| JSON-LD `CollectionPage` | view (prop `schemas`) | `Views/portal/index.blade.php:9-17` |
| Fragmento do modal | `detalhe()` | `Controllers/ParceirosInstitucionaisPortalController.php:38-50` |
| Page cache HIT/MISS (`X-Cache-Status`) | `PageCacheMiddleware` | `V2/Core/Middleware/PageCacheMiddleware.php:35-59` |
| Cabeçalho HTTP de 60 s | `PublicCacheMiddleware` | `V2/Core/Middleware/PublicCacheMiddleware.php:80-92` |
| `trackAccess()` por render de metadata | `SEOServiceProvider` | `V2/Services/SEO/Providers/SEOServiceProvider.php:33-36` |
| Entradas no sitemap | `SeoPortalController::addPartners` | `V2/Services/SEO/Controllers/SeoPortalController.php:248-283` |
| Resultado de busca com URL do detalhe | `SearchService::publicResultUrl` | `V2/Services/Search/Services/SearchService.php:79-86` |

### 1.3 Contrato observável (entrada → saída)

| Entrada | Saída |
|---|---|
| `GET /parceiros-institucionais` com 0 ativos | `200`; bloco "Nenhum parceiro cadastrado" / "Em breve divulgaremos nossos parceiros." |
| `GET /parceiros-institucionais` com 13 ativos | `200`; 12 cards + link de paginação para `?page=2`. |
| `GET /parceiros-institucionais/detalhe/{id}` (AJAX) | `200` fragmento iniciando em `<div class="text-center">`; sem `<html>`, sem SEO. |
| Mesmo endpoint sem AJAX | Mesmo fragmento, porém cacheado 5 min e com `Cache-Control` de 60 s. |
| `{id}` inexistente | `404` (página de erro padrão). |
| Parceiro `is_active=false` no detalhe | `200` (não há filtro `active()`). |
| Logo ausente | Placeholder `bi-building` 185×185 (index) / 180×180 (detalhe). |
| Descrição vazia | Bloco de descrição omitido. |
| Descrição rica inválida | `500` (renderer sem `try/catch` no controller). |

### 1.4 O que este anexo NÃO faz

- Não há filtro por tag, categoria, letra ou busca na página.
- Não há página de detalhe navegável (o detalhe é fragmento); não existe URL canônica de "página do parceiro".
- Não há paginação por query customizável (`per_page` ignorado).
- Não há compartilhamento social/OG específico por parceiro.
- Não há fallback de logo a partir da coluna legada `logo`.

## 2. Abordagem

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

A página é um grid server-rendered cujo único comportamento é abrir um modal que busca HTML no mesmo controller. A Homepage reaproveita a mesma rota via `data-url`. SEO é montado em duas camadas: `SEOService` (controller) e `schemas`/`pageTitle` (view).

```mermaid
flowchart LR
  A[GET /parceiros-institucionais] --> B[PageCache]
  B -->|HIT| R1[HTML cacheado]
  B -->|MISS| C[PortalController@index]
  C --> D[Parceiro active ordered paginate 12]
  C --> E[SEOService title/description/keywords]
  D --> V[index.blade: grade + CollectionPage]
  V --> F[public.cache 60s]
  G[Clique no card] --> H[GET detalhe id via AJAX]
  H --> I[findOrFail + toHtml + StorageService::url]
  I --> J[fragmento injetado no modal]
```

### 2.2 Mecânica passo a passo

1. `PageCacheMiddleware` testa GET, não-AJAX e não-`cms*`; monta `page:sha1(fullUrl)` e devolve HIT se existir (`V2/Core/Middleware/PageCacheMiddleware.php:25-59,68-91`).
2. Em MISS, `ParceirosInstitucionaisPortalController@index` consulta `Parceiro::with('galleryLogo')->active()->ordered()->paginate(12)` (`Controllers/ParceirosInstitucionaisPortalController.php:26`).
3. Seta SEO: `Parceiros — {app.name}`, `Conheça os parceiros institucionais da {app.name}.`, `parceiros, instituições, {app.name}` (`:28-30`).
4. Renderiza `v2-parceirosinstitucionais::portal.index` com `pageTitle`, `pageDescription`, `gtag['page_context']='parceiros-institucionais'` e um schema `CollectionPage` (`Views/portal/index.blade.php:5-26`).
5. O layout `v2-portal::app-layout` desliga header, footer, acessibilidade e colunas laterais; o conteúdo vai para `main-section-area`.
6. Hero fixo com `assets/sucesu/img/hero/breadcrumb.png`, breadcrumb "Home › Parceiros" e `<h1>Parceiros</h1>` (`:32-44`).
7. Cada card monta `data-id`, `data-bs-toggle="modal"`, `data-bs-target="#parceiroModal"`; a imagem vem de `galleryLogo` via componente `picture` com variantes WebP e `lazy=true` (`:51-71`).
8. O resumo é `summary` ou `str_limit(strip_tags(getHtmlDescription()), 250)` (`:74`).
9. Ícones sociais vêm de `metadata['social_media']`, filtrando `url` e `active` (`:75-102`).
10. O `PublicCacheMiddleware` adiciona `Cache-Control` e `Vary` à resposta 2xx (`V2/Core/Middleware/PublicCacheMiddleware.php:64-93`).
11. No clique, o JS substitui `__ID__` na URL de `detalhe` e faz `$.get`; injeta no `#parceiroModalBody` e foca o primeiro elemento focável (`Views/portal/index.blade.php:199-221`).
12. `detalhe()` busca `Parceiro::with('galleryLogo')->findOrFail($id)`, renderiza a descrição via `ContentRichEditorRendererService::toHtml`, resolve a URL do logo via `StorageService::url` e devolve o fragmento (`Controllers/ParceirosInstitucionaisPortalController.php:38-50`).

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

- **Modal por AJAX, não por página** `[C]`: mantém o visitante na grade e permite reuso do fragmento na Homepage.
- **Detalhe por `id`** `[C]`: o slug existe mas não é usado em URL pública, evitando colisões e redirecionamentos (`Routes/web.php:12-15`).
- **Sem filtros** `[C]`: o volume é baixo e a ordenação é editorial.
- **`summary` com fallback para HTML** `[C]`: permite mini-descrição curada sem quebrar parceiros antigos.
- **SEO no controller + schema na view** `[C]`: o `SEOService` é singleton e alimenta o partial de metadata; a view adiciona `schemas` diretamente.
- **Todos os parceiros no sitemap** `[C]` — inclusive inativos: o filtro é só `deleted_at` (`SeoPortalController.php:258-266`). `[I]` Isso mantém URLs válidas (o detalhe não filtra ativo), mas expõe conteúdo desativado a crawlers.
- **Widget ignora o limite da seção** `[C]`: `prepare()` não lê `limit`/`visible_slugs`; só `hidden_slugs` e `title` (`SucesuAssociadosWidget.php:26-41`).

### 2.4 Trechos de código pivô

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Controllers/ParceirosInstitucionaisPortalController.php:26-36
// listagem pública: 12 ativos, SEO definido no controller
public function index()
{
    $parceiros = Parceiro::with('galleryLogo')->active()->ordered()->paginate(12);

    $this->seo->setTitle('Parceiros — ' . config('app.name'));
    $this->seo->setDescription('Conheça os parceiros institucionais da ' . config('app.name') . '.');
    $this->seo->setKeywords('parceiros, instituições, ' . config('app.name'));

    return view('v2-parceirosinstitucionais::portal.index', compact('parceiros'));
}
```

Não há `request` no método: nenhum filtro/ordenação por query é aceito. O `with('galleryLogo')` evita N+1 na grade; o eager load usa `logo_id` sem FK.

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Controllers/ParceirosInstitucionaisPortalController.php:38-50
// detalhe: fragmento sem layout, sem active(), renderer sem try/catch
public function detalhe($id)
{
    $parceiro = Parceiro::with('galleryLogo')->findOrFail($id);

    $htmlContent = '';
    if (!empty($parceiro->description)) {
        $renderer = app(ContentRichEditorRendererService::class);
        $htmlContent = $renderer->toHtml($parceiro->description);
    }

    $logoUrl = $parceiro->galleryLogo
        ? $this->storage->url($parceiro->galleryLogo->file_path)
        : null;

    return view('v2-parceirosinstitucionais::portal.detalhe', compact('parceiro', 'htmlContent', 'logoUrl'));
}
```

`$logoUrl` é calculado mas a view **não o usa** (usa o componente `picture` com `$logo->file_path`) — dívida inofensiva. O ponto crítico é `findOrFail` sem `active()`: qualquer id válido é público.

```blade
{{-- V2/Modules/Conexoes/ParceirosInstitucionais/Views/portal/index.blade.php:53-71 — card/modal (recorte de 53-74) --}}
<div class="col-md-6 d-flex">
    <div class="team-box-items mt-0 parceiro-card h-100 w-100 d-flex flex-column"
         data-id="{{ $parceiro->id }}"
         data-bs-toggle="modal"
         data-bs-target="#parceiroModal">
        <div class="team-img align-items-stretch flex-grow-1">
            @if($logo && $logo->file_path)
                @include('v2-service-media::components.picture', [
                    'originalPath' => $logo->file_path,
                    'variants'     => $logo->variants ?? [],
                    'alt'          => $logo->alt_text ?: $parceiro->name,
                    'class'        => 'parceiro-logo',
                    'lazy'         => true,
                ])
            @else
                <div class="parceiro-logo-placeholder">
                    <i class="bi bi-building display-4 text-muted"></i>
                </div>
            @endif
```

O card não é um `<a>`: é um `div` com `data-bs-toggle`, por isso o `role`/teclado dependem do Bootstrap. Os ícones sociais dentro dele usam `onclick="event.stopPropagation();"` para não abrir o modal (`:89-101`).

```blade
{{-- Views/portal/index.blade.php:199-214 — modal carregado por AJAX (recorte de 199-221) --}}
$('#parceiroModal').on('show.bs.modal', function(event) {
    var card = $(event.relatedTarget);
    var id = card.data('id');
    var url = detalheUrlBase.replace('__ID__', id);

    $.get(url, function(html) {
        $('#parceiroModalBody').html(html);
        // After modal content loaded:
        var modalEl = document.getElementById('parceiroModal');
        if (modalEl) {
            var firstFocusable = modalEl.querySelector('button, a, input, [tabindex]:not([tabindex="-1"])');
            if (firstFocusable) setTimeout(function() { firstFocusable.focus(); }, 200);
        }
    }).fail(function() {```

O fail não diferencia 404 de 500; o spinner é substituído pela mensagem de erro. O foco vai para o primeiro focável do modal **global**, não do conteúdo injetado (o `btn-close` do header é o primeiro).

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuAssociadosWidget.php:26-41
// widget: todos os ativos, blacklist de slugs
public function prepare(array $parameters, Collection $anexos = null): array
{
    // Por padrão TODOS os parceiros ativos aparecem. A opção manual é uma
    // blacklist: hidden_slugs serve apenas para OCULTAR parceiros específicos.
    $hiddenSlugs = $parameters['hidden_slugs'] ?? [];
    if (!is_array($hiddenSlugs)) {
        $hiddenSlugs = [];
    }

    $query = Parceiro::with('galleryLogo')->active()->ordered();

    if (!empty($hiddenSlugs)) {
        $query->whereNotIn('slug', $hiddenSlugs);
    }

    $parceiros = $query->get();```

`$parameters['items']` é sobrescrito, ignorando qualquer item estático salvo na seção. `visible_slugs` é validado pelo schema mas **não** usado no `prepare` (`Schemas/SucesuAssociadosSchema.php:7-16`) — apenas `hidden_slugs`.

### 2.5 Algoritmos, consultas e custos

| Item | Query | Cache |
|---|---|---|
| Index | count + page + eager `galleryLogo` | page cache 5 min + HTTP 60 s |
| Detalhe | 1 select + eager | page cache 5 min quando não-AJAX; AJAX sempre renderiza |
| Widget da Homepage | 1 select + eager (todos os ativos, **sem limite**) | nenhum próprio; herda o cache da Homepage |
| Comentário do widget | — | a Homepage não chama `clearPageCache` deste módulo ao salvar a seção |
| Sitemap | `select id, updated_at where deleted_at is null` (todos) | gerado sob demanda |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Fragmento reutilizado em dois modais** (portal e Homepage) — o mesmo HTML precisa servir a dois containers.
- **`Hidden slugs` como blacklist** e padding de carrossel no widget.
- **Sitemap inclui inativos** por design (filtra só `deleted_at`).
- **Página sem qualquer filtro/ordenação pública**, apesar de existir taxonomia por tags.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-PARCEIROSINSTITUCIONAIS-37 | Index pagina 12 e ordena `sort_order, name`. | Ordem editorial controlada só pelo CMS. | `Controllers/ParceirosInstitucionaisPortalController.php:26`; `Models/Parceiro.php:167-175` |
| RN-PARCEIROSINSTITUCIONAIS-38 | Detalhe não filtra ativo. | Link direto funciona para desativado; modal da Homepage também. | `:38-50` |
| RN-PARCEIROSINSTITUCIONAIS-39 | O fragmento é cacheável quando não-AJAX. | Crawlers e navegação direta recebem HTML sem `<head>`; cache de 5 min. | `V2/Core/Middleware/PageCacheMiddleware.php:68-86` |
| RN-PARCEIROSINSTITUCIONAIS-40 | `summary` tem precedência sobre a descrição rica. | Editar só a descrição não muda o card se `summary` estiver preenchido. | `Views/portal/index.blade.php:74` |
| RN-PARCEIROSINSTITUCIONAIS-41 | Redes sociais exibidas vêm só de `metadata.social_media`, com `active` default `true`. | Colunas legadas não aparecem no portal. | `Views/portal/index.blade.php:76-102`; `Views/portal/detalhe.blade.php:44-71` |
| RN-PARCEIROSINSTITUCIONAIS-42 | Ícone social usa `icon` do JSON, com fallback por mapa de rede. | Item sem `icon` cai em `bi-link-45deg`. | `detalhe:46-66` |
| RN-PARCEIROSINSTITUCIONAIS-43 | Widget mostra todos e só oculta `hidden_slugs`; `visible_slugs` é inerte. | Novos parceiros aparecem automaticamente. | `SucesuAssociadosWidget.php:28-41`; `Schemas/SucesuAssociadosSchema.php:11-14` |
| RN-PARCEIROSINSTITUCIONAIS-44 | Widget repete a lista até 6 itens. | Com 1 parceiro, o carrossel mostra 6 slides iguais. | `SucesuAssociadosWidget.php:53-59` |
| RN-PARCEIROSINSTITUCIONAIS-45 | Sitemap inclui a página (0.5/weekly) e cada detalhe (0.4/monthly). | Inativos também são listados. | `V2/Services/SEO/Controllers/SeoPortalController.php:268-280` |
| RN-PARCEIROSINSTITUCIONAIS-46 | Busca global exige `entity_id` para montar URL; sem ele o resultado é descartado. | `publicResultUrl` retorna `null` e o item some. | `V2/Services/Search/Services/SearchService.php:83-84,44-49` |

### 3.3 Casos-limite

- **0 ativos**: mensagem "Em breve divulgaremos nossos parceiros."; a página continua com hero e SEO.
- **13+ ativos**: paginação `?page=N`; o cache key inclui a query, então cada página tem cache próprio.
- **Logo com variantes**: `<picture>` usa WebP com `media` por largura; sem variantes, `<img>` com `loading=lazy` conforme `$lazy`.
- **Parceiro sem `summary` e sem descrição**: `<p>` vazio.
- **Rede social com `active=false`**: não renderiza no portal (mas continua no JSON).
- **Homepage com 0 ativos**: `$parameters['items']` fica `[]`; o render `@if(!empty($data['items']))` não imprime a seção.
- **Homepage com 1 ativo**: 6 slides repetidos, `loop=true`.
- **AJAX lento**: spinner permanece até resposta/falha; sem timeout explícito.
- **Cache quente + parceiro desativado**: até 5 min de grade antiga e até 60 s no navegador.

### 3.4 Armadilhas de reimplementação

1. **Não devolver página completa em `detalhe`**: os dois modais injetam o retorno em `innerHTML`; um `<html>` aninhado quebra o layout.
2. **Não usar slug na URL pública**: a rota, a busca, o sitemap e o widget usam `id`.
3. **Não assumir que `resolvePartners` alimenta a Homepage atual**: ele é código sem chamador; o caminho vivo é o widget LSE (`HomepageSectionResolver` não é referenciado fora de si mesmo).
4. **Não contar com `limit` no widget**: diferente do integrador legado (`limit` default 12), o widget carrega todos.
5. **Não exibir logo pela coluna `logo`**: as views usam `galleryLogo`.
6. **Cuidado com `str_limit`**: helper do Laravel 5.5; não existe em versões novas.

### 3.5 Divergências e dívidas

- **`HomepageDataIntegrator::fetchPartners`** devolve `image_id => $parceiro->logo` (coluna legada) e só atua em seções `type=sucesu-brands` com `source_type=dynamic-partners` (`V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php:38-40,132-153`), enquanto o widget vivo usa `type=sucesu-associados`. Dois contratos para o mesmo dado.
- **`HomepageSectionResolver::resolvePartners`** não tem chamador e devolve itens sem `url`/`id` (`V2/Modules/Layouts/Homepage/Services/HomepageSectionResolver.php:488-511`).
- **`$logoUrl` do detalhe** é calculado e ignorado pela view (`Controllers/...PortalController.php:47-50`).
- **`pageDescription` fixo na view** ("Conheça os parceiros e apoiadores institucionais.") difere da description do `SEOService` ("Conheça os parceiros institucionais da {app.name}."); a do serviço vence no `<meta>` (`Views/portal/index.blade.php:7`; `Controllers/...PortalController.php:29`; `V2/layouts/portal/metadata/seo.blade.php:2-5`).
- **`trackAccess()` a cada render de metadata** pode gravar acesso mesmo em previews de CMS (`preview.blade.php` usa o layout do portal).
- **Sitemap inclui inativo**: divergência funcional entre "desativado" e "não indexável".

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `portal.index` | rota | `GET /parceiros-institucionais` | — | navegação, sitemap, Homepage (link) |
| `portal.detalhe` | rota | `GET /parceiros-institucionais/detalhe/{id}` | id UUID existente | modal do portal, modal da Homepage, busca |
| `Parceiro::scopeActive` | scope | `active()` | — | index, widget |
| `Parceiro::scopeOrdered` | scope | `ordered()` | — | index, widget |
| `Parceiro::getHtmlDescription` | método | `(): string` | — | index (fallback do resumo) |
| `SucesuAssociadosWidget::prepare` | método | `(array $parameters, Collection $anexos = null): array` | registrado como `sucesu-associados` | `LandingSectionsEditorRendererService` |
| `metadata.social_media[]` | estrutura | `{network, icon?, url, order?, active?}` | item com `network` e `url` | views da Homepage e portal |

### 4.2 Contrato de UI/JS

| Elemento | Seletor | Payload | Comportamento |
|---|---|---|---|
| Card | `.parceiro-card[data-id]` | UUID | clique abre `#parceiroModal` |
| Modal portal | `#parceiroModal`, `#parceiroModalBody` | HTML do detalhe | jQuery `$.get`, foco no primeiro focável |
| Ícone social do card | `.parceiro-social-icon` | `href`, `title`, `aria-label` | `stopPropagation` |
| Carrossel | `.associados-slider` | Swiper | `slidesPerView` 5/4/3/2 por breakpoint, loop, autoplay 3000 ms |
| Slide | `a.parceiro-home-trigger[data-url]` | URL do detalhe | abre `#parceiroHomeModal` |
| Modal Homepage | `#parceiroHomeModal`, `#parceiroHomeModalBody` | HTML do detalhe | `XMLHttpRequest`; erro mostra ícone + texto |

O render da Homepage inicializa o Swiper com retry de até 20 tentativas a cada 200 ms caso o global ainda não exista (`V2/Modules/Layouts/Homepage/Views/lse/render/associados.blade.php:81-132`).

### 4.3 Ordem e ciclo de vida

1. Homepage: `HomepagePortalController@index` lê `LayoutBaseConfig['landing_sections']`, passa pelo `HomepageDataIntegrator::integrate` e por `LandingSectionsEditorRendererService::prepareSections`, que chama `SucesuAssociadosWidget::prepare` (`V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php:44-48`).
2. O widget tem precedência sobre o integrador: mesmo que `integrate` preencha `items`, `prepare` sobrescreve.
3. O fragmento só existe após o `SEOServiceProvider` registrar os composers de metadata — irrelevante para o detalhe (sem layout).
4. O page cache é populado na primeira visita e invalidado por qualquer escrita CMS do módulo.

### 4.4 Receitas de uso

**Link para o detalhe em qualquer template:**

```blade
<a href="{{ route('v2.parceiros-institucionais.portal.detalhe', ['id' => $parceiro->id]) }}"
   data-url="{{ route('v2.parceiros-institucionais.portal.detalhe', ['id' => $parceiro->id]) }}">
   {{ $parceiro->name }}
</a>
```

**Carregar o fragmento em um modal próprio:**

```js
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
  .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
  .then(html => document.getElementById('meuCorpo').innerHTML = html);
```

**Renderizar a grade em outra página:**

```php
$parceiros = \V2\Modules\Conexoes\ParceirosInstitucionais\Models\Parceiro::with('galleryLogo')
    ->active()->ordered()->paginate(12);
return view('v2-parceirosinstitucionais::portal.index', compact('parceiros'));
```

**Ocultar um parceiro do carrossel da Homepage:**

```json
{ "parameters": { "title": "Parceiros", "hidden_slugs": ["acme-antiga"] } }
```

### 4.5 Anti-uso

1. **Incluir `portal/detalhe` dentro de uma view Blade** (server-side) esperando o layout: o arquivo começa em `<div class="text-center">` e não estende layout algum.
2. **Passar `hidden_slugs` com `id` em vez de `slug`**: o filtro é `whereNotIn('slug', ...)`; nada é ocultado.
3. **Esperar `limit` no widget**: o campo não é lido; use `hidden_slugs` ou ajuste o componente.

### 4.6 Custos e limites

- 12 itens por página; sem lazy-load de página.
- Imagens via `<picture>` com variantes WebP; sem dimensões explícitas (`width`/`height`), o que pode causar CLS.
- Page cache global por URL; qualquer escrita de parceiro limpa todo o cache de páginas.
- Sem throttle; dependente do cache para absorver tráfego.

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|---|
| página | GET | `/parceiros-institucionais` | `...portal.index` | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | `@index` | pública | grade + SEO + CollectionPage | `Routes/web.php:7-10` |
| fragmento | GET | `/parceiros-institucionais/detalhe/{id}` | `...portal.detalhe` | idem | `@detalhe` | pública | HTML do modal | `Routes/web.php:12-15` |

**SEO do index**: `<title>` = `Parceiros — {app.name}` (o `seoTitle` do serviço vence o `pageTitle` da view); `description`/`keywords` do serviço; OG/Twitter com `og:url` = URL atual; canonical = URL atual; JSON-LD `CollectionPage` com `name`, `description` e `url` relativa (`Views/portal/index.blade.php:9-17`).

**SEO do detalhe**: nenhum (fragmento).

## 6. Dados e modelo de domínio

Consome `v2_parceiros_institucionais` (`name`, `slug`, `summary`, `description`, `logo_id`, `metadata`, `is_active`, `sort_order`) e `v2_gallery_media` (via `galleryLogo`). Estrutura em [`dados.md`](dados.md).

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant U as Visitante
  participant P as PortalController
  participant DB as PostgreSQL
  participant S as StorageService
  U->>P: GET /parceiros-institucionais
  P->>DB: active()->ordered()->paginate(12)
  P-->>U: HTML + CollectionPage
  U->>P: GET /detalhe/{id} (AJAX)
  P->>DB: with(galleryLogo)->findOrFail
  P->>P: toHtml(description)
  P->>S: url(file_path)
  P-->>U: fragmento
```

`FL-PARCEIROSINSTITUCIONAIS-07` (Homepage): request → `LayoutBaseConfig` → `integrate` → `prepareSections` → `SucesuAssociadosWidget::prepare` (todos os ativos, blacklist, padding) → render Swiper → clique abre modal → `GET detalhe`.

## 8. UI CMS

Não se aplica; ver [`cms.md`](cms.md).

## 9. UI Portal

### 9.1 Página `/parceiros-institucionais`

- **Layout**: `v2-portal::app-layout` com `fullWidthMain=true`; header, footer, acessibilidade e colunas laterais desligados (`Views/portal/index.blade.php:18-25`).
- **Hierarquia**: hero (`h1` "Parceiros") → seção `team-section` → grade de cards com `h3` por parceiro.
- **Card**: logo 185×185 `object-fit: contain`, padding 20 px, fundo do placeholder `#f8f9fa`; hover eleva 4 px e adiciona sombra (`:145-194`).
- **Responsivo**: `col-md-6` (2 colunas a partir de 768 px); abaixo disso, 1 coluna.
- **Imagens**: `<picture>` com `<source>` WebP por largura; `alt` = `alt_text` da mídia ou nome; `loading=lazy` no fallback.
- **Modal**: `modal-lg` centralizado, `aria-modal=true`, `aria-labelledby` apontando para um `visually-hidden` "Detalhes do parceiro"; spinner inicial; botão fechar com `aria-label="Fechar"`.
- **A11y**: ícones sociais com `aria-label="Rede — abre em nova aba"`; o card em si não tem `role=button` nem `tabindex` (depende do Bootstrap para teclado).
- **Estado vazio**: ícone `bi-people`, `h2` "Nenhum parceiro cadastrado", texto "Em breve divulgaremos nossos parceiros."
- **Print**: sem CSS específico; o modal não imprime bem.

### 9.2 Fragmento de detalhe

- Logo 180×180 (ou placeholder `--color-bg-canvas`), nome em `h3.h4`, endereço com `bi-geo-alt` e link "Ver no mapa" (quando `google_maps_url`), divisor, descrição rica centralizada, ícones sociais 40×40.
- `rel="noopener noreferrer"` em todos os links externos; `aria-label` nos links sociais.
- Sem `h1`; o `h3` pressupõe que o container já tenha um heading (modal).
- `picture` com `lazy=false`: se houver variantes, o primeiro WebP recebe `fetchpriority="high"` e o `<img>` continua lazy (`V2/Services/Media/Views/components/picture.blade.php:5-20,55-72`).

### 9.3 Homepage — carrossel

- Seção `brand-section` com `aria-label="Parceiros Institucionais"`; título opcional.
- Swiper com 5 slides (≥1200 px), 4 (≥992), 3 (≥576), 2 (abaixo), `loop=true`, `autoplay` 3000 ms com `disableOnInteraction`, `centeredSlides`.
- Logo em `.associado-logo` com `max-height: 70px`; `alt` = nome.
- Slide é `<a data-url>` que abre `#parceiroHomeModal`; o modal tem o mesmo padrão de spinner/foco do portal.

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

| Item | Valor | Efeito | Fonte |
|---|---|---|---|
| `app.name` | `env('APP_NAME')` | title/description/keywords | `Controllers/...PortalController.php:28-30` |
| `filesystems.media_disk` | default | URL do logo via `StorageService` | `V2/Services/Storage/StorageService.php:33-43` |
| `page.cache` | Redis, 5 min | cache da página e do fragmento não-AJAX | `V2/Core/Middleware/PageCacheMiddleware.php:11-14` |
| `public.cache` | 60 s | cabeçalho HTTP | `V2/Core/Middleware/PublicCacheMiddleware.php:86-93` |
| flag de módulo | não se aplica às rotas públicas | portal continua mesmo com módulo desativado | `V2/Core/Providers/V2ServiceProvider.php:297-307` |
| permissão | pública | nenhuma verificação | `Routes/web.php:3-15` |

## 11. Observabilidade, erros e limites

- **Logs**: `[Parceiro] Erro ao renderizar descrição: …` no fallback do model (`Models/Parceiro.php:135`); `[PageCache] Redis indisponivel…` quando o cache falha (`V2/Core/Middleware/PageCacheMiddleware.php:63`).
- **HTTP**: `200` com `X-Cache-Status: HIT|MISS`; `404` de `findOrFail`; `500` de renderer; `503` em manutenção.
- **SEO/analytics**: `trackAccess()` no composer de metadata; `gtag.page_context = parceiros-institucionais` na view.
- **Limites**: 12/página; sem timeout de AJAX no cliente; sitemap sem paginação (uma query com todos).
- **Privacidade**: nenhum dado sensível; e-mails não são exibidos.

## 12. Seeds e dados iniciais

- `DemoContentSeeder` fornece 8 parceiros (6 com logo) para a página e o carrossel.
- `SucesuHomepageSeeder` cria a seção `sucesu-associados` com título "Parceiros Institucionais" (`V2/Seeds/BasicPages/SucesuHomepageSeeder.php:310-335`).
- `ParceirosInstitucionaisSeeder` cria uma **BasicPage** de mesmo slug com HTML estático, inalcançável pela rota (ver [`spec.md`](spec.md) §3.5).

## 13. Lacunas

- `LG-PARCEIROSINSTITUCIONAIS-16` — O comportamento do `prepareSections` para `sucesu-associados` (se respeita `parameters.title`) foi inferido da leitura do widget/render; não foi executado. Onde procurar: `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:180-220`. Impacto: baixo.
- `LG-PARCEIROSINSTITUCIONAIS-17` — Não há evidência de intenção de exibir inativos no sitemap; pode ser bug. Onde procurar: issues/histórico. Impacto: médio para SEO.
- `LG-PARCEIROSINSTITUCIONAIS-18` — Não se sabe se `HomepageSectionResolver` é usado por código fora de `V2/` (ex.: `public-v7/`). Onde procurar: busca global no monorepo. Impacto: baixo.
- `LG-PARCEIROSINSTITUCIONAIS-19` — Dimensões/CLS do componente `picture` não foram medidas. Impacto: baixo.

## 14. Checklist de paridade

- [ ] `PC-PARCEIROSINSTITUCIONAIS-50` — `/parceiros-institucionais` renderiza hero com `<h1>Parceiros</h1>` e breadcrumb "Home › Parceiros". Fonte: `Views/portal/index.blade.php:32-44`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-51` — Com 13 ativos, há 12 cards e link `?page=2`. Fonte: `Controllers/...PortalController.php:26`; `Views/portal/index.blade.php:110-114`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-52` — Clique no card dispara `GET /parceiros-institucionais/detalhe/{id}` e injeta o retorno. Fonte: `Views/portal/index.blade.php:199-214`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-53` — Clique em ícone social não abre o modal (`stopPropagation`). Fonte: `:89-101`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-54` — Nome do parceiro contém `<script>`: é escapado no card e na descrição rica o HTML é renderizado como rich text (por design). Fonte: `:73`; `detalhe:40`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-55` — Parceiro com `summary` e descrição longa exibe o `summary`. Fonte: `:74`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-56` — Sem `summary`, exibe os primeiros 250 caracteres do texto da descrição. Fonte: `:74`; `Models/Parceiro.php:126-138`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-57` — Fragmento não contém `<html>`/`<head>`. Fonte: `Views/portal/detalhe.blade.php:1-72`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-58` — Detalhe de parceiro desativado responde 200. Fonte: `Controllers/...PortalController.php:38-50`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-59` — Homepage com 2 ativos renderiza 6 slides (2 repetidos 3×). Fonte: `SucesuAssociadosWidget.php:53-59`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-60` — `hidden_slugs=['x']` remove o slide de slug `x` e mantém os demais. Fonte: `:30-39`; `Views/lse/forms/associados.blade.php:1-38`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-61` — Clique no slide da Homepage carrega o fragmento no `#parceiroHomeModalBody`. Fonte: `V2/Modules/Layouts/Homepage/Views/lse/render/associados.blade.php:116-146`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-62` — Sitemap tem `/parceiros-institucionais` com `priority 0.5` e cada parceiro com `priority 0.4`. Fonte: `V2/Services/SEO/Controllers/SeoPortalController.php:268-280`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-63` — Resultado de busca de parceiro tem `result_url` terminando em `/detalhe/{id}`. Fonte: `V2/Services/Search/Services/SearchService.php:83-84`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-64` — Segunda visita ao index recebe `X-Cache-Status: HIT`. Fonte: `V2/Core/Middleware/PageCacheMiddleware.php:35-40`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-65` — Modal com id inexistente mostra "Erro ao carregar detalhes do parceiro.". Fonte: `Views/portal/index.blade.php:215-221`.

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `V2/Modules/Conexoes/ParceirosInstitucionais/Controllers/ParceirosInstitucionaisPortalController.php` | 1-50 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/portal/index.blade.php` | 1-225 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/portal/detalhe.blade.php` | 1-72 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Models/Parceiro.php` | 96-175 |
| `V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuAssociadosWidget.php` | 1-64 |
| `V2/Modules/Layouts/Homepage/LseWidgets/Schemas/SucesuAssociadosSchema.php` | 1-17 |
| `V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php` | 20-45, 132-153 |
| `V2/Modules/Layouts/Homepage/Services/HomepageSectionResolver.php` | 488-511 |
| `V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php` | 40-60 |
| `V2/Modules/Layouts/Homepage/Views/lse/render/associados.blade.php` | 1-160 |
| `V2/Modules/Layouts/Homepage/Views/lse/forms/associados.blade.php` | 1-50 |
| `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php` | 128-200 |
| `V2/Services/Media/Views/components/picture.blade.php` | 1-72 |
| `V2/Services/SEO/Providers/SEOServiceProvider.php` | 19-51 |
| `V2/Services/SEO/Controllers/SeoPortalController.php` | 248-283 |
| `V2/Services/Search/Services/SearchService.php` | 20-90 |
| `V2/layouts/portal/metadata/seo.blade.php` | 1-30 |
| `V2/Core/Middleware/PageCacheMiddleware.php` | 1-92 |
| `V2/Core/Middleware/PublicCacheMiddleware.php` | 24-95 |
