---
id: MOD-LAYOUTS-HOMEPAGE-INTEGRADORES
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-LAYOUTS-HOMEPAGE]
fontes:
  - V2/Modules/Layouts/Homepage/Services/
  - V2/Modules/Layouts/Homepage/LseWidgets/
  - V2/Modules/Layouts/Homepage/Controllers/
  - V2/Modules/Layouts/Homepage/ModuleProvider.php
depende_de: [SVC-LANDINGSECTIONSEDITOR, MOD-PUBLICACOES-NOTICIAS, MOD-PUBLICACOES-AGENDA, MOD-CONEXOES-PARCEIROSINSTITUCIONAIS, SVC-GALLERY, SVC-STORAGE, SVC-SEO, MOD-SUPPORT-STRUCTURALLANDINGEDITOR]
consumidores: [MOD-LAYOUTS-HOMEPAGE]
criticidade: alta
dono: Homepage
---

# Homepage — integradores, widgets e caminhos de dados

> Três camadas disputam o mesmo problema — “de onde vêm os dados de uma seção?”:
> `HomepageDataIntegrator` (substitui `items` por consultas), `LandingSectionsEditorRendererService`
> (resolve mídias e chama `prepare` de cada widget) e o próprio widget (pode consultar banco outra
> vez). Este documento diz **qual caminho está vivo na requisição de `/`**, qual é legado e o que
> cada um dos 10 widgets `sucesu-*` resolve em runtime.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Converte uma seção abstrata do payload em HTML final, decidindo em runtime se os itens vêm do JSON salvo, da galeria, de Notícias, de Agenda ou de Parceiros. |
| Entrada | `type` + `parameters` de cada seção; coleção de `GalleryMedia`; banco de Notícias/Agenda/Parceiros. |
| Saída | `parameters` enriquecidos (`image`, `image_variants`, `icon`, `items`, `super_highlighted`, `months`, `upcoming_events`, `events_by_date`) e HTML por view de render. |
| Efeitos colaterais | Consultas de leitura; logs `warning` quando um `try/catch` existe; nenhum cache próprio. |
| Particularidade central | O `prepare` dos widgets é a única fonte de verdade para **mídia e dados de banco**; o integrador só é relevante para `sucesu-agenda`. |
| Como se integra | `WidgetRegistry::register('sucesu-*', widget)` no construtor de `ModuleProvider`; `LandingSectionsEditorRendererService::prepareSections` chama `getAttachmentIds` + `prepare`; o viewer renderiza `getRenderView()`. |
| Se removido, quebra | As seções não resolvem imagens nem dados e os 10 tipos desaparecem do editor. |
| Superfície pública | `HomepageDataIntegrator::integrate`; 10 widgets (cada um `getAttachmentIds`/`prepare`); 10 schemas; `HomepageSectionResolver::resolve` (sem consumidor). |

## 1. Efeito

### 1.1 O que cada widget resolve

| # | Slug / classe | Título no editor (ícone) | Schema | Configuração persistida | Fonte em runtime | Render | Fallback quando falta dado |
|---|---|---|---|---|---|---|---|
| 1 | `sucesu-hero` / `SucesuHeroWidget` | Hero SUCESU (`bi bi-star-fill`) | `SucesuHeroSchema` | `slides[]`: `title`, `subtitle`, `cta_text`, `cta_url`, `image_id` | `image_id` → `image` + `image_variants` (galeria) | `lse/render/hero` | sem `slides` → nada; `image` ausente → `<img src="">`; 1º slide vira `<h1>` |
| 2 | `sucesu-services` / `SucesuServicesWidget` | Serviços SUCESU (`bi bi-grid-3x3-gap-fill`) | `SucesuServicesSchema` | `items[]`: `title`, `description`, `url`, `icon_id` | `icon_id` → `icon` + `icon_variants` | `lse/render/services` | sem `items` → nada; item sem ícone → card só com texto |
| 3 | `sucesu-about` / `SucesuAboutWidget` | Sobre SUCESU (`bi bi-file-earmark-image`) | `SucesuAboutSchema` | textos, `image_id`, `counters[]` (`value/suffix/label/icon_id`) | `image_id` → `image`; `counters[].icon_id` → `icon` | `lse/render/about` | `parameters={}` → nada; sem imagem → `picture` vazio; contador sem ícone → só número |
| 4 | `sucesu-brands` / `SucesuBrandsWidget` | Marcas SUCESU (`bi bi-images`) | `SucesuBrandsSchema` | `title`, `items[]`: `name`, `image_id` | `image_id` → `image`; `Parceiro` **só** via integrador (`dynamic-partners`) | `lse/render/brands` | sem `items` → nada; logo ausente → slide vazio |
| 5 | `sucesu-events` / `SucesuEventsWidget` | Eventos SUCESU (`bi bi-calendar3`) | `SucesuEventsSchema` | `subtitle`, `title`, `items[]`: `title`, `details`, `url`, `image_id` | `image_id` → `image`; `Evento` **só** via integrador (`dynamic-events`) | `lse/render/events` | sem `items` → nada; sem imagem → sem bloco de imagem; `title/details` em HTML cru |
| 6 | `sucesu-regionais` / `SucesuRegionaisWidget` | Regionais SUCESU (`bi bi-geo-alt`) | `SucesuRegionaisSchema` | `subtitle`, `title`, `text1`, `states`, `text2`, `cta_text`, `cta_url`, `image_id` | `image_id` → `image` (CSS background) | `lse/render/regionais` | `parameters={}` → nada; sem imagem → `url('')` no background |
| 7 | `sucesu-news` / `SucesuNewsWidget` | Notícias SUCESU (`bi bi-newspaper`) | `SucesuNewsSchema` | `subtitle`, `title`, `description`, `items[]`: `title`, `excerpt`, `date`, `url`, `image_id` | `image_id` → `image`; `News` **só** via integrador (`dynamic-news`) | `lse/render/news` | sem `items` → nada; sem imagem → sem thumb; `date` precisa de `<dia> <mês>` |
| 8 | `sucesu-associados` / `SucesuAssociadosWidget` | Parceiros Institucionais (`bi bi-building`) | `SucesuAssociadosSchema` | `title`, `hidden_slugs[]` (blacklist; `visible_slugs` ignorado) | **sempre** `Parceiro::with('galleryLogo')->active()->ordered()` (sem limite) | `lse/render/associados` | sem parceiros → nada; <6 → repete a lista; sem logo → `picture` vazio |
| 9 | `sucesu-noticias` / `SucesuNoticiasWidget` | Notícias (Banco) (`bi bi-newspaper`) | `SucesuNoticiasSchema` | `title`, `limit`, `categories[]` | **sempre** `News::published()->recent()` + `coverImage`; super-destaque | `lse/render/noticias` | sem itens e sem super → nada; sem capa → `news-default.jpg`; `limit+1` sem super |
| 10 | `sucesu-agenda` / `SucesuAgendaWidget` | Agenda Executiva (`bi bi-calendar-event`) | `SucesuAgendaSchema` | `title`, `event_limit`, `months_ahead` | **sempre** `Evento::published()->active()->upcoming()->upcomingOrdered()` (até 20) | `lse/render/agenda` | banco fora → `months=[]` → nada; sem imagem → placeholder com ícone |

Fonte do registro: `V2/Modules/Layouts/Homepage/ModuleProvider.php:20-32`.

### 1.2 Efeitos sobre dados

| Efeito | Onde | Fonte |
|---|---|---|
| Resolve lotes de mídia uma única vez por `prepareSections` | `whereIn('id', ...)` em `v2_gallery_media` | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:167-178` |
| Injeta `image`/`icon` por caminho aninhado (`slides.0.image`) | `getAttachmentIds` + `prepare` | `LseWidgets/Widgets/SucesuHeroWidget.php:29-58` |
| Injeta variantes WebP em `<campo>_variants` | `array_set` no renderer | `LandingSectionsEditorRendererService.php:188-197` |
| Substitui `items` de news/events/brands quando `parameters.source_type` casa | `HomepageDataIntegrator` | `V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php:23-51` |
| Mescla `months`/`upcoming_events`/`events_by_date` em `sucesu-agenda` | `HomepageDataIntegrator` | `HomepageDataIntegrator.php:42-47` |
| Recalcula `items` de parceiros, notícias e agenda no widget | `prepare` | `SucesuAssociadosWidget.php:26-63`; `SucesuNoticiasWidget.php:43-75`; `SucesuAgendaWidget.php:32-118` |
| Converte `image_id` em `resolved_parameters` (caminho morto) | `HomepageSectionResolver` | `Services/HomepageSectionResolver.php:25-118` |

### 1.3 Matriz de integrações

| Integração | Direção | Ponto de contato | O que troca | Estado/limite |
|---|---|---|---|---|
| **Notícias** | consome | `SucesuNoticiasWidget::prepare`; `HomepageDataIntegrator::fetchNews` | notícias publicadas (`status`), categorias, capa/alt/URL | vivo; a home **não** escreve nem indexa notícias |
| **Agenda** | consome | `SucesuAgendaWidget::prepare`; `HomepageDataIntegrator::fetchAgendaData` | eventos futuros publicados/ativos, datas, local, organizador | vivo; sem escrita |
| **Parceiros institucionais** | consome | `SucesuAssociadosWidget::prepare`; `HomepageDataIntegrator::fetchPartners` | parceiros ativos, logo, URL de detalhe | vivo; sem escrita |
| **Unidade Regional** | — | nenhum | — | **não integra**: `sucesu-regionais` é texto + `states` + imagem manual; não consulta unidade regional nem aplica escopo |
| **Search** | — | nenhum | — | **não integra**: nenhum widget lê `v2_public_search_index` ou chama indexer; a indexação é dos módulos de origem |
| **SEO** | fornece/consome | `SEOService` (controller) + composer `metadata*` | title/description/keywords/JSON-LD; fallback de `PortalConfigsService`; `trackAccess()` | vivo; SEO não vem do payload |
| **Gallery** | consome | `getAttachmentIds` → `whereIn v2_gallery_media`; editor → `GallerySender` | `file_path`, `variants`, `alt_text` (via modelo) | vivo; resolução em lote no renderer |
| **LSE** | consome/estende | `WidgetRegistry`, `BaseWidget`, schemas, forms, renders, viewer/editor, 10 rotas | contrato de seção e HTML | vivo; o LSE não persiste a home |
| **SLE** | consome | `LayoutBaseConfig`, `BlockRenderer`, blocos `css_head`/`js_foot`/header/footer | payload, CSS/JS do tema, header/footer e banner | vivo; Swiper/main.js dependem do seed SLE |

### 1.4 Contrato observável

| Entrada | Resultado |
|---|---|
| `sucesu-hero` sem `slides` | nenhum HTML (`@if(!empty($data['slides']))`) |
| `sucesu-services` sem `items` | nenhum HTML |
| `sucesu-about` com `parameters={}` | nenhum HTML (`@if(!empty($data))`) |
| `sucesu-brands` sem `items` | nenhum HTML |
| `sucesu-events` sem `items` | nenhum HTML |
| `sucesu-regionais` com apenas `title` | HTML completo com imagem/CSS vazios |
| `sucesu-news` sem `items` | nenhum HTML |
| `sucesu-associados` sem parceiros ativos | nenhum HTML (nem mensagem) |
| `sucesu-noticias` sem notícias e sem super-destaque | nenhum HTML |
| `sucesu-agenda` com banco fora | calendário renderiza com `months` vazio → seção some (`@if(!empty($months))`) |
| `image_id` inexistente | `image` não é definido; render esconde a mídia, exceto hero |

### 1.5 O que os widgets NÃO fazem

- Não sanitizam HTML: `sanitize()` só roda no preview/render-section do LSE.
- Não paginam: retornam tudo que os scopes permitem (parceiros sem `take`).
- Não filtram por unidade regional: nenhum widget usa escopo regional.
- Não indexam busca: não há chamada a `PublicIndexerService`/`SearchSender`.
- Não geram SEO por seção: nenhum `script type=application/ld+json` nos renders.

## 2. Abordagem

### 2.1 Estratégia

```mermaid
flowchart TD
  Payload[payload.landing_sections] --> I[HomepageDataIntegrator::integrate]
  I -->|source_type=dynamic-events| EV[(Evento)]
  I -->|source_type=dynamic-news| NW[(News)]
  I -->|source_type=dynamic-partners| PA[(Parceiro)]
  I -->|type=sucesu-agenda| AG[(Evento 20)]
  I --> R1[Renderer::prepareSections]
  R1 -->|getAttachmentIds| G[(v2_gallery_media)]
  R1 -->|prepare| W1[widgets de mídia]
  R1 -->|prepare| W2[widgets de banco]
  W2 --> NW
  W2 --> EV
  W2 --> PA
  R1 --> V[viewer]
  V --> R2[Renderer::prepareSections 2ª vez]
  R2 --> HTML[render por tipo]
```

### 2.2 Mecânica passo a passo

1. `HomepagePortalController::index` lê `landing_sections` (`:44-45`) e chama `integrate`
   (`HomepagePortalController.php:47`).
2. `integrate` percorre as seções por referência e, conforme `type` + `parameters.source_type`,
   substitui `items` (`HomepageDataIntegrator.php:25-48`).
3. O controller chama `prepareSections` (`HomepagePortalController.php:48`). O renderer:
   1. coleta IDs de anexo de cada widget via `getAttachmentIds` (`LandingSectionsEditorRendererService.php:133-165`);
   2. carrega mídias em uma query (`:167-178`);
   3. chama `prepare($params, $anexos)` (`:185-186`);
   4. injeta `*_variants` por caminho (`:188-197`).
4. `portal.index` inclui o viewer (`Views/portal/index.blade.php:34-36`), que **repete** os passos do
   renderer (`landing-sections-viewer.blade.php:12-13`).
5. O viewer resolve `WidgetRegistry::get($type)` e inclui `getRenderView()`
   (`landing-sections-viewer.blade.php:36-47`).
6. Cada render recebe `section` e `data = parameters` e decide se aparece.

### 2.3 Decisões de design

| Decisão | Por quê | Marca |
|---|---|---|
| Resolver mídia no renderer, não no Blade | evita N+1 de `GalleryMedia`; uma query por request | `[C]` `LandingSectionsEditorRendererService.php:167-178` |
| Deixar o widget consultar banco no `prepare` | mantém o render burro e o dado testável | `[C]` `SucesuNoticiasWidget.php:43-75` |
| Integrador separado do widget | tentativa de manter dados dinâmicos fora dos widgets | `[I]` `HomepageDataIntegrator.php:11-15` |
| Duplicar consultas por chamar `prepareSections` duas vezes | o viewer é reutilizável por outros módulos e não confia no chamador | `[I]` `landing-sections-viewer.blade.php:11-13` |
| `sucesu-news` e `sucesu-noticias` coexistirem | um é vitrine estática/dinâmica simples, o outro é vitrine editorial completa com super-destaque | `[C]` `SucesuNewsWidget.php` vs `SucesuNoticiasWidget.php` |

### 2.4 Trechos de código pivô

```php
// V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:133-165 — coleta de anexos por widget (com fallback legado)
foreach ($sections as $section) {
    $type = $section['type'] ?? '';
    $params = $section['parameters'] ?? [];

    if (isset($widgets[$type])) {
        $ids = $widgets[$type]->getAttachmentIds($params);
        $anexoIds = array_merge($anexoIds, $ids);
    } else {
        // Fallback legado: campos padrão
        if (!empty($params['background_image_id'])) {
            $anexoIds[] = $params['background_image_id'];
        }
        // ... image_id, steps, items, cards
    }
}
```

> Widgets de módulo controlam exatamente quais caminhos são resolvidos; `sucesu-associados` e
> `sucesu-noticias` retornam `[]` de propósito, porque buscam mídia por URL própria.

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuHeroWidget.php:29-38 — contrato de caminho aninhado
public function getAttachmentIds(array $parameters): array
{
    $ids = [];
    foreach ($parameters['slides'] ?? [] as $i => $slide) {
        if (!empty($slide['image_id'])) {
            $ids["slides.{$i}.image"] = $slide['image_id'];
        }
    }
    return $ids;
}
```

> A chave do array é o caminho a receber o `file_path`; o renderer acrescenta `_variants` a esse
> caminho (`slides.0.image_variants`). Um `image_id` na raiz não é resolvido.

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuAgendaWidget.php:32-45 — a consulta que roda 3 vezes por request
$eventLimit = isset($parameters['event_limit']) ? (int) $parameters['event_limit'] : 4;
$monthsAhead = isset($parameters['months_ahead']) ? (int) $parameters['months_ahead'] : 2;

try {
    $eventos = Evento::published()
        ->active()
        ->upcoming()
        ->upcomingOrdered()
        ->with('image')
        ->take(20)
        ->get();
} catch (\Exception $e) {
    Log::warning('[SucesuAgendaWidget] Falha ao buscar eventos: ' . $e->getMessage());
    $eventos = collect([]);
}
```

> O integrador faz a mesma consulta (`HomepageDataIntegrator.php:164-170`), e o renderer chama
> `prepare` duas vezes no portal. Resultado: 3 consultas idênticas de até 20 eventos.

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuAssociadosWidget.php:28-49 — blacklist e URL canônica
$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();

$items = $parceiros->map(function ($parceiro) {
    return [
        'id'    => $parceiro->id,
        // ...
        'url'   => route('v2.parceiros-institucionais.portal.detalhe', ['id' => $parceiro->id]),
    ];
})->toArray();
```

> O render usa `id`+`slug`+`url`; a URL é gerada pela **rota nomeada**, diferente do integrador, que
> monta `/noticia/` e `/agenda/` na mão.

### 2.5 Consultas por caminho vivo (request a `/`)

| Consulta | Integrador | Renderer (1ª) | Viewer (2ª) | Total |
|---|---|---|---|---|
| `LayoutBaseConfig` | — | — | — | 1 (controller) |
| `v2_gallery_media whereIn` | — | 1 | 1 | 2 quando há mídia; 0 sem `image_id`/`icon_id` |
| Notícias (`sucesu-noticias`) | — | 1 | 1 | 2 quando a seção existe |
| Eventos (`sucesu-agenda`) | 1 | 1 | 1 | 3 quando a seção existe |
| Parceiros (`sucesu-associados`) | — | 1 | 1 | 2 quando a seção existe |
| Notícias (`sucesu-news` + `source_type`) | 1 (se dinâmico) | 0 | 0 | 1 |
| Eventos (`sucesu-events` + `source_type`) | 1 (se dinâmico) | 0 | 0 | 1 |
| Parceiros (`sucesu-brands` + `source_type`) | 1 (se dinâmico) | 0 | 0 | 1 |

> Com o seed padrão (`sucesu-associados` + `sucesu-news` estático + `sucesu-events` estático), o
> total é **1 config + 2 galeria + 2 parceiros**; não há consulta de Notícias (o `sucesu-news` do
> seed é estático) nem de Agenda. Basta adicionar `sucesu-noticias` (+2) ou `sucesu-agenda` (+3)
> para o total subir.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Duas rotinas concorrentes de resolução**: `HomepageDataIntegrator::integrate` (array) e
  `HomepageSectionResolver::resolve` (Collection de model). A segunda replica a primeira com
  `source_type` no nível da seção e **não tem consumidor** (`HomepageSectionResolver.php:17-118`).
- **Widgets que ignoram `getAttachmentIds`**: `sucesu-associados` e `sucesu-noticias` retornam `[]`
  e resolvem imagens via modelo (`SucesuAssociadosWidget.php:21-24`;
  `SucesuNoticiasWidget.php:31-34`).
- **Widgets que sempre consultam banco**: `sucesu-associados`, `sucesu-noticias`, `sucesu-agenda`
  não têm contrapartida estática.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-HOMEPAGE-31` | `prepare` recebe `$anexos` keyed por id, mas só os widgets de mídia o usam | widgets de banco ignoram o parâmetro | `SucesuNoticiasWidget.php:43`; `BaseWidget.php:161-179` |
| `RN-HOMEPAGE-32` | `image` já existente em `parameters` não é re-resolvido pelos widgets | muda o resultado de um payload semeado vs editado | `SucesuHeroWidget.php:52-55` (sobrescreve se `image_id`), `HomepageSectionResolver.php:270-284` (preserva) |
| `RN-HOMEPAGE-33` | O integrador roda **antes** do `prepare`; o widget pode sobrescrever o que o integrador fez | `sucesu-agenda` é recalculado; `items` dinâmicos ficam | `HomepagePortalController.php:47-48` |
| `RN-HOMEPAGE-34` | O renderer injeta `<campo>_variants` apenas para caminhos retornados por `getAttachmentIds` | itens dinâmicos não têm variantes WebP | `LandingSectionsEditorRendererService.php:188-197` |
| `RN-HOMEPAGE-35` | `sucesu-associados` não tem `limit`; o min 6 duplica a lista inteira | carrossel infinito com poucos parceiros | `SucesuAssociadosWidget.php:41-59` |
| `RN-HOMEPAGE-36` | `sucesu-noticias` usa `cover_image_url` (StorageService) e `url` de rota nomeada | URLs absolutas/relativas consistentes com o módulo de Notícias | `SucesuNoticiasWidget.php:83-103`; `News.php:201-213,362-369` |
| `RN-HOMEPAGE-37` | `sucesu-agenda` considera `first_day_of_week` com 0=domingo | calendário renderiza a coluna correta | `SucesuAgendaWidget.php:108` |
| `RN-HOMEPAGE-38` | `sucesu-agenda` busca até 20 eventos, independentemente de `event_limit` | `event_limit` limita só a lista lateral | `SucesuAgendaWidget.php:39-45,185-187` |
| `RN-HOMEPAGE-39` | O renderer não ordena seções; `prepare` preserva a ordem | mesma regra do payload | `LandingSectionsEditorRendererService.php:128-205` |
| `RN-HOMEPAGE-40` | O portal não aplica `sanitizeContent`; só preview/render-section aplicam | HTML persiste e é executado | `landing-sections-viewer.blade.php:1-14`; `LandingSectionsEditorController.php:149-153,221-223` |

### 3.3 Caminhos vivos × legado

| Caminho | Quem chama | Estado | Evidência |
|---|---|---|---|
| `HomepagePortalController@index` → `integrate` → `prepareSections` → `portal.index` → viewer → `prepareSections` → widget `prepare`/render | rota `GET /` | **vivo** | `Routes/web.php:9`; `HomepagePortalController.php:42-65`; `landing-sections-viewer.blade.php:12-47` |
| Ramo `sucesu-agenda` do integrador | sempre no portal | **vivo** | `HomepageDataIntegrator.php:42-47` |
| Ramos `dynamic-events`/`dynamic-news`/`dynamic-partners` | só se `parameters.source_type` existir | **vivo mas inalcançável pela UI** | `HomepageDataIntegrator.php:29-40`; forms LSE não têm `source_type` |
| `HomepageSectionResolver::resolve` | nenhum código de produção | **morto** (testes) | `HomepageSectionResolver.php:25`; `tests/Unit/.../HomepageSectionResolverTest.php` |
| `HomepageSection` + `v2_homepage_sections` | nenhum código de produção | **morto** | `Models/HomepageSection.php`; migration; `DashboardStatsService.php:670` |
| `Views/portal/sections/*` | nenhum `@include` | **morto** (cópia antiga dos renders) | `Views/portal/index.blade.php:33-36` inclui só o viewer |
| `Views/cms/forms/*` | nenhuma view/rota | **morto** | referenciam `v2.homepage.update`/`v2.homepage.index`, inexistentes |
| `Views/cms/index.blade.php` | nenhuma rota | **morto** | `Routes/cms.php:5-9` |
| `HomepageCmsController@refreshCache` | botão do editor | **vivo** | `cms/editor.blade.php:35-42` |
| `/cms/v2/landing-sections/*` (LSE) | JS do editor | **vivo** (SVC) | `V2/Services/LandingSectionsEditor/Routes/cms.php:9-25` |
| `BasicPage/Views/portal/homepage.blade.php` | nenhuma rota | **morto para `/`** | `BasicPage/Routes/web.php:8`; `BasicPagePortalController::homepage` sem rota |
| `Core/Controllers/HomePortalController` | rota comentada | **morto** | `V2/Core/Routes/web.php:10-11` |

### 3.4 Armadilhas de reimplementação

1. **Implementar só o integrador** e achar que cobre os widgets: o seed e o editor não produzem
   `source_type`, então `sucesu-noticias`/`sucesu-associados`/`sucesu-agenda` continuam consultando
   banco.
2. **Remover o integrador** para “evitar duplicação”: `sucesu-agenda` perde os campos de calendário
   no primeiro `prepare`? Na prática o widget os recria; mas o contrato do controller (`integrate`
   antes de `prepare`) é observável em testes e em qualquer consumidor que reuse `integrate`.
3. **Unificar `sucesu-news` com `sucesu-noticias`**: filtros e ordenações são diferentes
   (`status`+`created_at` vs `published_at`).
4. **Usar `HomepageSectionResolver` como referência de formato**: ele espera `section_key` e
   `source_type` no nível da seção, que não existem no payload real.
5. **Resolver `image_id` na view** (como faz o BasicPage legado com `GalleryMedia::where('file_path')`):
   gera N+1 e ignora variantes.

### 3.5 Divergências

| Divergência | Evidência |
|---|---|
| Integrador monta `/noticia/{slug}` e `/agenda/{slug}` à mão; widgets usam rotas nomeadas | `HomepageDataIntegrator.php:89 e :123` vs `SucesuNoticiasWidget.php:101`; `SucesuAgendaWidget.php:73` |
| Integrador busca notícias ordenando por `published_at`; widget por `created_at` | `HomepageDataIntegrator.php:102-104` vs `SucesuNoticiasWidget.php:49` |
| Integrador não filtra `is_super_highlighted`; widget separa o super-destaque | `HomepageDataIntegrator.php:116-125` vs `SucesuNoticiasWidget.php:57-72` |
| Resolver mantém `image` literal; widgets sobrescrevem | `HomepageSectionResolver.php:270-284` vs `SucesuHeroWidget.php:52-55` |
| `visible_slugs` no schema/form sem efeito | `SucesuAssociadosSchema.php:11-12`; `SucesuAssociadosWidget.php:28-33` |
| README diz 9 widgets | `README.md:15-27` vs `ModuleProvider.php:22-31` |

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Precondição | Consumidores |
|---|---|---|---|
| `HomepageDataIntegrator::integrate` | `(array $sections): array` | seções LSE | `HomepagePortalController` |
| `WidgetContract::getAttachmentIds` | `(array $parameters): array<string,string>` | — | renderer |
| `WidgetContract::prepare` | `(array $parameters, ?Collection $anexos): array` | — | renderer |
| `WidgetRegistry::register/get/all/has` | estático | boot | `ModuleProvider`, renderer, LSE |
| `Sucesu*Widget::__construct` | define `icon`, `title`, `schemaClass`, `formView`, `renderView` | — | registry |
| `HomepageSectionResolver::resolve` | `(Collection): Collection` | model hidratado | **nenhum** |

### 4.2 Contrato de UI/JS

- Os renders usam os componentes `v2-service-media::components.picture` (mídia responsiva) e
  `v2_storage_url()` (background/CSS) — nunca `Storage::url`.
- `sucesu-associados` inicializa Swiper com retry de 20×200 ms e um modal Bootstrap que busca a URL
  do parceiro por `XMLHttpRequest` e injeta o HTML (`lse/render/associados.blade.php:80-176`).
- `sucesu-agenda` usa tabs Bootstrap (`data-bs-toggle="tab"`) e links `/agenda?date=YYYY-MM-DD`
  (`lse/render/agenda.blade.php:31-104`).
- Cada seção é embrulhada pelo viewer em `<div data-section-id="…" class="lse-render-section-block">`
  (`landing-sections-viewer.blade.php:41`).

### 4.3 Ordem e ciclo de vida

1. Registry populado no boot do provider (`ModuleProvider.php:20-32`).
2. `prepareSections` sempre chama `registerLegacyWidgets()` antes de renderizar
   (`LandingSectionsEditorRendererService.php:81-85`), mas só adiciona tipos ausentes.
3. Integrador → renderer → (viewer → renderer) → renders.
4. Sem teardown: nada é cacheado em memória entre requests além de `WidgetRegistry::$widgets`
   (estático por processo PHP-FPM).

### 4.4 Receitas de uso

**1. Adicionar um widget de banco (ex.: depoimentos) sem tocar no LSE:**

```php
class SucesuDepoimentosWidget extends \V2\Services\LandingSectionsEditor\Widgets\BaseWidget
{
    public function __construct()
    {
        $this->title = 'Depoimentos';
        $this->icon = 'bi bi-chat-quote';
        $this->schemaClass = \Meu\Modulo\Schemas\DepoimentosSchema::class;
        $this->formView = 'meu-modulo::lse.forms.depoimentos';
        $this->renderView = 'meu-modulo::lse.render.depoimentos';
    }

    public function prepare(array $parameters, \Illuminate\Support\Collection $anexos = null): array
    {
        $parameters['items'] = \Meu\Modulo\Models\Depoimento::published()->latest()->take(3)->get()->toArray();
        return $parameters;
    }
}
\V2\Services\LandingSectionsEditor\Services\WidgetRegistry::register('sucesu-depoimentos', new SucesuDepoimentosWidget());
```

**2. Testar a resolução de mídia de um widget isoladamente:**

```php
$widget = new \V2\Modules\Layouts\Homepage\LseWidgets\Widgets\SucesuHeroWidget();
$ids = $widget->getAttachmentIds(['slides' => [['image_id' => '42']]]);
// ['slides.0.image' => '42']
$params = $widget->prepare(['slides' => [['image_id' => '42']]], collect(['42' => (object)['file_path' => 'sucesu/hero/1.jpg']]));
// slides[0]['image'] === 'sucesu/hero/1.jpg'
```

**3. Consumir os dados de uma seção no portal sem renderizar:**

```php
$sections = app(\V2\Modules\Layouts\Homepage\Services\HomepageDataIntegrator::class)
    ->integrate($config->payload);
$news = collect($sections)->firstWhere('type', 'sucesu-noticias');
```

### 4.5 Anti-uso

- **Chamar `prepare` manualmente antes de `prepareSections`**: você perde a coleção de anexos em
  lote e cria N+1 em `v2_gallery_media`.
- **Adicionar `take()` em `sucesu-associados`**: o render espera o carrossel com todos os parceiros
  ativos e a duplicação até 6; limitar muda a paridade visual.
- **Contar com `visible_slugs`**: só `hidden_slugs` filtra.
- **Esperar sanitização**: nenhum render do portal passa por `strip_tags`.

### 4.6 Custos e limites

- Consultas por request: ver §2.5.
- `sucesu-associados` carrega **todos** os parceiros ativos com `galleryLogo` (relação 1:1).
- `sucesu-agenda` carrega até 20 eventos com `image`; monta meses em PHP puro (O(20×meses)).
- `sucesu-noticias` carrega `limit+1` com `coverImage`.
- Todas são absorvidas pelo cache de página de 5 min; não há cache por widget.

## 5. Rotas e contratos HTTP

Nenhum widget expõe rota. O contrato HTTP é o do LSE:

| Método | URI | Uso pelo editor | Efeito |
|---|---|---|---|
| GET | `/cms/v2/landing-sections/form` | busca o form do widget | renderiza `getFormView()` com `$data` |
| POST | `/cms/v2/landing-sections/validate` | aplica `getValidationRules()` | `{success:true}` ou 422 com `errors` |
| POST | `/cms/v2/landing-sections/preview/render-section` | troca uma seção no iframe | sanitiza + prepara + renderiza `getRenderView()` |
| GET/POST/DELETE | `/cms/v2/landing-sections/media*` | mídia do form | delega ao Gallery |

Fonte: `V2/Services/LandingSectionsEditor/Routes/cms.php:9-25`;
`V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:67-242`.

## 6. Dados e modelo de domínio

- Entrada dos widgets: array `parameters` (§3.3 de [`dados.md`](dados.md)).
- Saída: o mesmo array, potencialmente com `image`, `image_variants`, `icon`, `icon_variants`,
  `items`, `super_highlighted`, `months`, `upcoming_events`, `events_by_date`.
- Dependências de dados: `v2_gallery_media`, `v2_news`, `v2_news_categories`, `v2_eventos`,
  `v2_parceiros_institucionais` (scopes citados na tabela §3.2).
- `News::published()` filtra `status`; `Evento::published()` filtra `published_at <= now()`;
  `Parceiro::active()->ordered()` filtra `is_active` e ordena por `sort_order,name`
  (`News.php:391-394`; `Evento.php:251-264`; `Parceiro.php:167-175`).

## 7. Fluxos e estados

- `FL-HOMEPAGE-06` — **resolução de uma seção de mídia**: `getAttachmentIds` → `whereIn` →
  `prepare` → `image` → `array_set(<campo>_variants)` → render.
- `FL-HOMEPAGE-07` — **resolução de uma seção de banco**: `prepare` → query com scopes → mapeamento
  para `items`/`super_highlighted`/`months` → render.
- `FL-HOMEPAGE-08` — **falha**: `try/catch` só existe no integrador e em `SucesuAgendaWidget`;
  Notícias/Parceiros deixam a exceção subir.

## 8. UI CMS

- O form de cada widget é carregado por AJAX e os campos são `parameters[...]`; o widget só entra no
  dropdown se estiver registrado. Detalhes em [`cms.md`](cms.md).

## 9. UI Portal

- Cada render tem seu próprio `aria-label` de seção, guardas de vazio e fallback de imagem.
  Detalhes em [`portal.md`](portal.md).

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

- Nenhum widget lê config própria; todos herdam a permissão `Homepage` (via editor) e a flag
  `Homepage` (rotas CMS).
- O render público não tem permissão nem flag: desligar o módulo **não** desliga `/` (a rota está em
  `Routes/web.php`, fora de `module.active`).

## 11. Observabilidade, erros e limites

| Situação | Log | Fonte |
|---|---|---|
| Falha no integrador (eventos/notícias/parceiros/agenda) | `warning` + retorno vazio | `HomepageDataIntegrator.php:67-69,111-113,140-142,171-177` |
| Falha no widget de agenda | `warning` + coleção vazia | `SucesuAgendaWidget.php:46-49` |
| Falha em notícias/parceiros | nenhum log; exceção sobe | `SucesuNoticiasWidget.php:49`; `SucesuAssociadosWidget.php:35` |
| Falha ao carregar mídias | `error` no renderer | `LandingSectionsEditorRendererService.php:170-177` |

Limites: sem paginação; sem `limit` em parceiros; sem cache por widget; sem retry.

## 12. Seeds e dados iniciais

- O seed padrão cobre 8 dos 10 widgets (não cria `sucesu-noticias` nem `sucesu-agenda`);
  `sucesu-associados` entra só com `title` (`SucesuHomepageSeeder.php:320-327`).
- `sucesu-brands` do seed é **estático** (`items` com `image_id`), embora exista caminho
  `dynamic-partners` no integrador.

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| `LG-HOMEPAGE-11` | Não há registro histórico de uso dos ramos `dynamic-*`; podem ser código morto | logs de produção/busca por `source_type` | reimplementação pode omiti-los |
| `LG-HOMEPAGE-12` | Não há teste de integração para o duplo `prepareSections` | `tests/Feature/V2/Modules/Layouts/Homepage/` | custo/regressão invisível |
| `LG-HOMEPAGE-13` | Não existe contrato de escopo regional nos widgets | `RegionalScopeService` (não referenciado aqui) | home sempre nacional |
| `LG-HOMEPAGE-14` | `SucesuAgendaWidget` e integrador divergem na origem dos campos de calendário | ambos os arquivos | duplicação de manutenção |
| `LG-HOMEPAGE-15` | Não há definição de comportamento esperado quando a galeria retorna mídia de outro módulo | Gallery `allowShared` | imagens de outros módulos podem ser exibidas |

## 14. Checklist de paridade

- [ ] `PC-HOMEPAGE-31` — `WidgetRegistry` contém exatamente os 10 slugs `sucesu-*` após o boot. Fonte: `ModuleProvider.php:22-31`.
- [ ] `PC-HOMEPAGE-32` — `sucesu-hero` resolve `image_id` de todos os slides em uma única query. Fonte: `SucesuHeroWidget.php:29-58`; renderer `:167-197`.
- [ ] `PC-HOMEPAGE-33` — `sucesu-services`, `sucesu-brands`, `sucesu-news` e `sucesu-events` resolvem `items.{i}.image`/`icon` e escondem mídia ausente. Fonte: widgets + renders.
- [ ] `PC-HOMEPAGE-34` — `sucesu-about` resolve `image` e `counters.{i}.icon` no mesmo `prepare`. Fonte: `SucesuAboutWidget.php:29-64`.
- [ ] `PC-HOMEPAGE-35` — `sucesu-regionais` usa `v2_storage_url` em CSS e, com apenas `title`, renderiza o bloco com imagem/CSS vazios; com `parameters={}` não renderiza. Fonte: `lse/render/regionais.blade.php:1-29`.
- [ ] `PC-HOMEPAGE-36` — `sucesu-associados` busca `Parceiro::active()->ordered()`, aplica `hidden_slugs` e garante ≥6 itens por repetição. Fonte: `SucesuAssociadosWidget.php:26-63`.
- [ ] `PC-HOMEPAGE-37` — `sucesu-noticias` filtra por `status='published'`, ordena por `created_at` e separa o super-destaque. Fonte: `SucesuNoticiasWidget.php:43-75`.
- [ ] `PC-HOMEPAGE-38` — `sucesu-agenda` busca eventos futuros publicados e ativos, limita a lista a `event_limit` e mostra `months_ahead` meses. Fonte: `SucesuAgendaWidget.php:32-118`.
- [ ] `PC-HOMEPAGE-39` — `sucesu-agenda` gera 3 consultas de eventos por request a `/` (integrador + 2× widget). Fonte: `HomepageDataIntegrator.php:42-47`; `HomepagePortalController.php:47-48`; viewer `:12-13`.
- [ ] `PC-HOMEPAGE-40` — `parameters.source_type` é o único gatilho dos ramos `dynamic-*` e não é produzido por nenhum form LSE. Fonte: `HomepageDataIntegrator.php:29-40`; `lse/forms/*`.
- [ ] `PC-HOMEPAGE-41` — `HomepageSectionResolver::resolve` não é chamado em nenhum fluxo de produção (apenas testes unitários). Fonte: `Services/HomepageSectionResolver.php:17-118`; `tests/Unit/V2/Modules/Layouts/Homepage/Services/HomepageSectionResolverTest.php`.
- [ ] `PC-HOMEPAGE-42` — `getAttachmentIds` de `sucesu-noticias`/`sucesu-associados` devolve `[]`. Fonte: `SucesuNoticiasWidget.php:31-34`; `SucesuAssociadosWidget.php:21-24`.
- [ ] `PC-HOMEPAGE-43` — Falha de banco em `sucesu-noticias`/`sucesu-associados` não é capturada e derruba a home. Fonte: `SucesuNoticiasWidget.php:49`; `SucesuAssociadosWidget.php:35`.
- [ ] `PC-HOMEPAGE-44` — Nenhum render imprime JSON-LD nem altera SEO. Fonte: `lse/render/*.blade.php`.

## 15. Evidências consultadas

- `V2/Modules/Layouts/Homepage/LseWidgets/Widgets/*.php` (10 arquivos)
- `V2/Modules/Layouts/Homepage/LseWidgets/Schemas/*.php` (10 arquivos)
- `V2/Modules/Layouts/Homepage/Views/lse/render/*.blade.php` (10 arquivos)
- `V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php:1-246`
- `V2/Modules/Layouts/Homepage/Services/HomepageSectionResolver.php:1-512`
- `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:128-273`
- `V2/Services/LandingSectionsEditor/Views/components/landing-sections-viewer.blade.php:1-49`
- `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:67-242`
- `V2/Modules/Publicacoes/Noticias/Models/News.php:195-235,357-427`
- `V2/Modules/Publicacoes/Agenda/Models/Evento.php:245-291`
- `V2/Modules/Conexoes/ParceirosInstitucionais/Models/Parceiro.php:165-175`
- `V2/Modules/Layouts/BasicPage/Views/portal/homepage.blade.php:34-45`
- `V2/Core/Routes/web.php:10-11`
