# P5 — Composição do site: home por seções, páginas, destaques e widgets

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `revisado` — implementação fechada (S1.1–S6.2) e revisão profunda concluída em 2026-10-26 (`P5.FIX` + `P5.FIX2`); correções aplicadas, commit do mantenedor pendente. Ficha: `_to_revision/P5-composicao-site.md` |
| Depende de | P1, P4 |
| Habilita | P6, P7, P12 |
| Tamanho | **G** (12 sessões) |
| ADRs | Emenda à **ADR-016** (seções/blocos); ADR-017, ADR-028, ADR-026, ADR-025, ADR-022 |

## 1. Objetivo

Transformar a home e as páginas institucionais em **dado editável** — seções ordenadas, blocos
tipados, páginas estáticas com AST, banners/destaques e widgets reais — sem HTML/Blade no banco,
sem construtor livre e sem quebrar acessibilidade.

**Resultado observável:** o operador reordena/oculta uma seção da home, adiciona um bloco de
“últimas notícias” e um banner com vigência, cria uma página “Sobre” e vê tudo no portal sem
deploy; um bloco inválido é ignorado, nunca renderizado.

## 2. Estado atual (reusar)

- `src/layout/Engine/SlotRegistry.php`, `SlotProvider`, `SlotConfig`, `LayoutPreset`.
- `src/services/Layout/**`: perfis por escopo (`LayoutProfile`), placements com `inicio`/`fim`,
  admin surface `/cms/layout`, `LayoutSlot` (`main.left|right|header|footer|before|after`).
- `Core\Contracts\WidgetRegistry` + `InMemoryWidgetRegistry` + `WidgetSource` (id, title, icon,
  slots, embeddable, render) — **sem nenhuma fonte registrada hoje**.
- `<x-layout::slot>` e `<x-layout::widget>` já renderizam a pilha e degradam sem widget.
- `Portal/Home` é um hero estático + `quick-links`.
- `Media`/`Gallery` para imagens; `RichText` para AST; `Seo` para metadata.
- **Não existe:** seções de home, páginas estáticas, banners, fontes de widget.

## 3. Escopo fechado

### S1.1 — Emenda à ADR-016 e catálogo fechado de blocos

- Emenda curta (não uma arquitetura nova): página é `{version, sections:[{id,type,props,hidden}]}`;
  `hidden` preserva a posição; `props` é validado por schema; tipo desconhecido é ignorado;
  `version` desconhecida cai no default.
- Catálogo fechado inicial (12 tipos): `hero`, `quick-links`, `text`, `text-image`, `cards-links`,
  `cards-numbered`, `steps`, `faq`, `video`, `latest-news`, `highlights`, `banner`.
- Cada tipo declara: `schema` (campos/limites), `component` Blade concreto, `media` (ids),
  `a11y` (um `<h1>` por página; headings na ordem; sem autoplay/carrossel automático).
- **Proibido:** `html-free`, `bloco-sle`, `template`, Blade/CSS/HTML em `props`, `iframe` de
  terceiro, posição absoluta. Texto rico só via AST (ADR-028).

### S1.2 — Parser puro `ParseHomepage` e fallback default

- Parser puro `ParseHomepage` (sem banco), testado com: sem config → default; `hidden` preserva
  posição; tipo desconhecido ignorado; `props` inválidas ignoradas; versão errada → default.
- Default da home atual continua sendo o fallback (hero + quick-links) — sem regressão.

> Técnica: TECNICO §11 (teste determinístico do parser); skills `php-84-correct`.

### S2.1 — Editor da home: rotas, permissões e formulário de seções

> **D2:** edição profunda de blocos é exceção de **Home** e do módulo especial de **páginas
> estáticas**. Os demais módulos não recebem este editor; só declaram widgets e usam a ação
> “Configurar layout da área/módulo” (P1).

- Rotas CMS: `GET /cms/home` e `PUT /cms/home` (draft), com `home.editar`; o módulo `home` já
  existe e declara `permissions` `[VIEW, EDIT, APPROVE]` (publicar usa `home.aprovar`, na fatia
  seguinte).
- Tela: lista ordenada de seções; adicionar por tipo; editar em `<dialog>` nativo; mover
  cima/baixo e “ocultar” (preserva posição); remover. Alternativa de teclado para reordenar
  (WCAG 2.2 SC 2.5.7) — botões são o caminho principal; drag é opcional.
- Validação server-side por schema; erros em resumo + campo; sem JS obrigatório (formulários
  normais); JS só melhora a edição.

> Técnica: TECNICO §7.1/§7.2; skills `web-accessibility`, `css-moderno`.

### S2.2 — Publicação, preview autenticado e locales da home

- `POST /cms/home/publicar` (publica a versão) com `home.aprovar`; a tela de edição ganha a ação
  “publicar”.
- Preview autenticado: rota `home.preview` com a sessão do CMS e permissão, renderizando o draft
  no shell real; TTL curto em cache por usuário; **sem token público**. Nenhum HTML do draft é
  servido a anônimo.
- Publicação: grava a versão publicada (jsonb) + auditoria `home.published`; cache invalidado.
- 4 locales: `props` traduzíveis onde fizer sentido (título, lead, itens); locale vem da URL.

> Técnica: TECNICO §3.3/§6; skills `laravel-12`.

### S3.1 — Módulo `Paginas`: tabela, CMS e regra de slug

> **D2:** o módulo de páginas estáticas é o segundo e último ponto de edição profunda de blocos.
> Nenhum outro módulo ganha seções/blocos; o resto é widget + layout da área.

- Módulo token `paginas` (grupo `Portal`), permissions `[VIEW, EDIT, ARCHIVE]`.
- Tabela `pages`: `id`, `slug` (único case-insensitive), `title` jsonb (LocaleCast), `content`
  jsonb (AST), `seo` jsonb, `layout_scope` nullable, `status` (`rascunho|publicado|arquivado`),
  `published_at`, timestamps; `down()` → `backup_*`.
- Rotas CMS: lista, criar, editar (publicar/despublicar e arquivar na fatia seguinte).
- Slug reservado: não pode colidir com o primeiro segmento de rota existente (`cms`, `api`,
  `dados-abertos`, `noticias`, `lgpd`, ...); validação consulta o router. Slug case-insensitive
  único com índice parcial `WHERE status <> 'arquivado'`? Decisão: único sempre, para URL estável.

> Técnica: TECNICO §5; skills `postgresql`, `laravel-12`.

### S3.2 — Portal de páginas: fallback, render, sitemap e preview

- CMS: publicar/despublicar e arquivar (transições de `status`, auditoria `paginas.published`).
- Portal: `GET /{slug}` via **fallback** da área Portal (executa só quando nenhuma rota viva
  casou) — nunca sombreia rota de módulo. Locale prefixado também coberto.
- Render: AST via `RichText`; `<x-layout::portal>` com `title`, `description`, `canonical`,
  `robots`, `schemas`; breadcrumbs; sem `<h1>` duplicado.
- Sitemap: `SitemapSource` do módulo registra páginas publicadas; `hreflang`/canonical por locale;
  página arquivada → 404 + `X-Robots-Tag: noindex`.
- Preview autenticado (mesmo padrão da home), sem token público.

> Técnica: TECNICO §6/§7.1; skills `web-accessibility`, `html-moderno`.

### S4.1 — Módulo `Destaques`: tabela e validação de conteúdo

- Módulo token `destaques` (grupo `Portal`), permissions `[VIEW, EDIT, ARCHIVE]`.
- Tabela `destaques`: `id`, `title` jsonb, `image_id` (Media), `alt` (obrigatório), `link`
  (validado same-origin/`https`), `slot` (`main.before|main.after|main.header|main.footer`),
  `position`, `starts_at`, `ends_at`, `active`, timestamps.
- Regras de conteúdo: imagem via Media; `alt` não vazio; link sem `javascript:`.

> Técnica: TECNICO §5/§10; skills `postgresql`, `laravel-12`.

### S4.2 — Vigência, widget de banner e cache por slot

- Render da imagem via `<x-service-media::picture>` (AVIF/WebP, width/height); vigência com clock
  injetado; expirado não aparece; `active=false` preserva o registro; no máximo N ativos por slot
  (config); sem rotação automática.
- Registra uma `WidgetSource` `destaques.banner` (embeddable) para o `LayoutProfile`; o slot do
  perfil decide onde a pilha aparece. O editor da home pode usar o bloco `banner` para uma peça
  editorial específica.
- Cache por slot com invalidação na escrita; se o módulo for desativado, o slot fica vazio.

> Técnica: TECNICO §3.3/§6; skills `web-accessibility`, `web-performance`.

### S5.1 — Fontes de widget reais nos módulos donos

> **D2/D6:** aqui entram os widgets; a posição é do layout da área/módulo (P1), não de um editor
> de blocos por módulo. O fallback global é definido pelo Suporte.

- Implementar `WidgetSource` concretas, cada uma no módulo dono:
  - `noticias.ultimas` (publicadas, ordem determinística, limite do schema);
  - `noticias.mais-lidas` (P4 métricas; fallback por `published_at` se métrica ausente);
  - `dadosabertos.ultimos` (datasets publicados);
  - `destaques.banner` (S4.2);
  - `home.quick-links` (lê a porta Core de navegação do P1; nunca importa `Menus`).
- Cada fonte: `id` namespaceado, `title` i18n, `icon` Lucide, `slots`, `embeddable`, `render()`
  sem consulta N+1 (uma query por widget; `whereIn` para mídia), sem HTML/CSS no banco.
- Registro no `ModuleProvider::register()`, com o módulo ativo resolvendo a fonte no
  `WidgetRegistry` (o comportamento de módulo inativo fica na fatia seguinte).

> Técnica: TECNICO §4.2/§11; skills `laravel-12`, `web-performance`.

### S5.2 — `/cms/layout` real, módulo inativo e acessibilidade por widget

- Módulo inativo → `WidgetRegistry` não resolve; slot vazio; log estruturado (sem 500).
- `LayoutProfile` admin (`/cms/layout`) passa a listar as fontes reais; teste de “widget de outro
  módulo” (ex.: notícias na coluna de uma página) sem import entre módulos.
- A11y por widget: headings corretos, listas semânticas, imagens com `alt`, alvos, contraste;
  axe por tipo.

> Técnica: TECNICO §7.1; skills `web-accessibility`.

### S6.1 — E2E, axe e orçamento de performance

- E2E: reordenar seção → portal; ocultar → some sem perder posição; bloco inválido ignorado;
  página criada → URL + sitemap; banner com vigência; widget no slot; preview autenticado.
- axe desktop/mobile; sniffCSS; performance budget (sem N+1); corrigir as regressões achadas.

> Técnica: TECNICO §11; skills `playwright-ts-admin`, `sniff-css`.

### S6.2 — Documentação, `make check` 2× e evidência

- `make check` 2× + `E2E=1`.
- Atualizar `INVENTARIO.md` (landing/páginas/banners/widgets) e a emenda à ADR-016.
- Registrar a evidência no `kd` e o estado da fase.

## 4. Fora de escopo

- Construtor visual livre, posição absoluta, drag com pixel, HTML/Blade no banco.
- Todos os módulos institucionais (A Instituição, A Cidade, Secretarias, etc.): entram por demanda
  sobre esta base, não nesta fase.
- Agendamento de publicação da home (além de banner); versionamento editorial completo.
- Widgets de módulos que ainda não existem (agenda, ouvidoria, licitações) — cada um na sua fase.

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P5.S1.1** | emenda ADR-016 + catálogo fechado (12 tipos, schema/componente/a11y) + proibições | emenda registrada; 12 tipos com schema; schema rejeita `html`/`template`/`iframe` | P1, P4 |
| **P5.S1.2** | parser `ParseHomepage` + default da home | parser (hidden, inválido, versão); home atual sem regressão | S1.1 |
| **P5.S2.1** | editor da home: rotas, permissões, formulário de seções | reorder/hide; teclado; erro por campo; sem JS obrigatório | S1.2 |
| **P5.S2.2** | preview autenticado + publicar + auditoria + locales | preview só autenticado; versão publicada; auditoria; axe | S2.1 |
| **P5.S3.1** | módulo `Paginas` + tabela/model + CMS + regra de slug | slug não sombreia rota; unicidade; CRUD | S1.2 |
| **P5.S3.2** | fallback do portal + render/SEO + sitemap + preview | 404/arquivada noindex; canonical; sitemap | S3.1 |
| **P5.S4.1** | módulo `Destaques` + tabela + validação de conteúdo | alt obrigatório; link sem `javascript:`; CRUD | S1.2 |
| **P5.S4.2** | vigência + widget `destaques.banner` + cache | banner expira; slot vazio; cache; limite por slot | S4.1 |
| **P5.S5.1** | fontes de widget nos módulos donos + registro | fonte resolvida com módulo ativo; N+1 zero | S2.2–S4.2 |
| **P5.S5.2** | `/cms/layout` real + módulo inativo + a11y por widget | widget de outro módulo sem import; módulo off → vazio; axe por tipo | S5.1 |
| **P5.S6.1** | E2E, axe desktop/mobile, sniffCSS, performance | E2E dos fluxos; axe zero A/AA; budget sem N+1 | S2.2–S5.2 |
| **P5.S6.2** | docs + 2× check + `E2E=1` + evidência | `INVENTARIO`/ADR-016 atualizados; evidência no `kd` | S6.1 |

## 6. Contratos e dados

- **Documento:** `home.sections` jsonb `{version, sections[]}`; `pages.content` AST jsonb.
- **Tabelas:** `pages`, `destaques` (módulos).
- **Portas:** `WidgetSource`/`WidgetRegistry` (já existem); `Navigation` (P1).
- **Rotas:** `home.cms.*`, `home.preview`, `paginas.cms.*`, `paginas.portal.show` (fallback),
  `destaques.cms.*`.
- **Permissões:** `home.ver/editar/aprovar`, `paginas.ver/editar/arquivar`,
  `destaques.ver/editar/arquivar`.
- **Eventos:** `home.published`, `paginas.published`, `destaques.updated`; auditoria nas mutações.

## 7. DoD

- [ ] Home e páginas estáticas usam blocos; **nenhum outro módulo** ganha editor de seções — só layout de widgets.
- [ ] Home, páginas e destaques são dado editável; nenhum HTML/Blade/CSS no banco.
- [ ] Bloco inválido/tipo desconhecido é ignorado; `hidden` preserva posição; versão errada → default.
- [ ] Página nunca sombreia rota viva; arquivada → 404 noindex; sitemap/canonical corretos.
- [ ] Banner com vigência e `alt`; sem rotação automática; módulo off → slot vazio.
- [ ] Widget de outro módulo via registry, sem import; N+1 zero; uma query por widget.
- [ ] Preview só autenticado; nenhum draft público.
- [ ] Axe zero A/AA; teclado no reorder; `make check` 2× + `E2E=1`; cobertura ≥90%.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P5-composicao-site.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Virar construtor de site complexo | catálogo fechado, sem posição absoluta, modo simples primeiro |
| HTML/Blade no banco | schema por tipo + renderer Blade em código + teste que rejeita `html`/`template` |
| Página sombrear rota | fallback da área Portal, validação de slug contra router, teste com rotas vivas |
| Widget pesado/N+1 | uma query por widget, `whereIn` de mídia, cache curto, teste de query count |
| Draft vazar | preview autenticado, cache por usuário/TTL, nenhuma rota pública de draft |
| A11y quebrada por bloco | contrato por componente + axe por tipo + revisão de headings |
| Cache stale de home/banner | invalidação na escrita + teste de dois requests |

## 9. Retomada

```bash
kd prime
kd ask "home seções landing páginas banners widgets layout"
kd task list --ready
```

Ler: `wiki/plan/16-front-builder-widgets-banners.md` → `src/services/Layout/**` →
`src/core/Contracts/Ports/WidgetSource.php` → `src/layout/Components/slot.blade.php` →
`src/modules/Portal/Home/Layout/Views/index.blade.php` → `lotep-v2/support-structurallandingeditor/09-APROVEITAMENTO-V7.md`
(portar contrato, rejeitar HTML/Blade no banco).

Referências irmãs (UX/desenho, não código): `../sucesu-cms/V2/Modules/Support/StructuralLandingEditor/**`
e `../lotep/V2/Services/LandingSectionsEditor/**` (seções ordenadas/schema/preview);
`../sucesu-cms/V2/Modules/Layouts/BasicPage/**` e `Layouts/Homepage/**` (página como dado e
fallback). Rejeitar Blade/HTML/template vindo do banco e endpoint de render sem permissão.
