---
id: MOD-INSTITUCIONAL-HISTORIA-PORTAL
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-INSTITUCIONAL-HISTORIA]
fontes:
  - V2/Modules/Institucional/Historia/
depende_de: [MOD-INSTITUCIONAL-HISTORIA, CORE-RUNTIME, SVC-SEO, SVC-MEDIA, SVC-GALLERY, SVC-CONTENTRICHEDITOR]
consumidores: [UI-PORTAL, SVC-SEO, CORE-RUNTIME]
criticidade: media
dono: InstitucionalA
---

# Historia — página pública `/historia`

> Anexo de [`spec.md`](spec.md). Descreve o caminho público: a query, o algoritmo de
> resolução de imagem, a alternância de layout, o HTML exato das seções, a origem de cada
> metadado e o cache de 5 minutos. A página é 100% estática por render (sem JS de
> interação) e não tem detalhe por marco.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Entrega em `GET /historia` uma linha do tempo em HTML com todos os marcos ativos, imagens da galeria e corpo renderizado do AST, sob o layout global do portal. |
| Entrada | `GET /historia` (sem parâmetros); cabeçalhos do visitante (User-Agent usado pelo `trackAccess` do SEO); estado de `v2_historia_milestones`, `v2_gallery_media`, `v2_portal_configs`. |
| Saída | HTML com hero/breadcrumb, intro, N `<section>` de marco, empty state opcional, CTA; `<title>`/description/keywords/OG/Twitter/canonical derivados; JSON-LD `Article`; `page_context=historia`. |
| Efeitos colaterais | Cache Redis `page:<sha1>` por 5 min; `Cache-Control: public, max-age=60, must-revalidate` + `Vary: Accept-Encoding`; rastreamento de acesso do `SEOService` (se não excluído). |
| Particularidade central | É a única página institucional cujo SEO vem de 3 chaves `historia_seo_*` em `v2_portal_configs`, mas cujo `pageTitle`/`pageDescription` literais no Blade são ignorados quando o `SEOService` está preenchido: o composer injeta `seoTitle = getTitle() ?: titulo_site` e o `??` do Blade curto-circuita a concatenação, sem duplicar `siteTitle`. |
| Como se integra | Rota nomeada `v2.historia.portal.index`; view `v2-historia::portal.index`; layout `v2-portal::app-layout`; componentes `v2-service-media::components.picture`; `SEOService` via injeção no controller. |
| Se removido, quebra | `/historia` passa a ser resolvida pelo catch-all `BasicPage` (404, salvo se existir linha com slug `historia`); o sitemap continua listando a URL adicionada por `addHistory` — link quebrado. |
| Superfície pública | `HistoriaPortalController@index`; view; nada em JS. |

## 1. Efeito

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

- Ler a trajetória em blocos alternados: nos índices ímpares (`$idx % 2 === 1`) o texto vem
  à esquerda e a imagem à direita; nos pares, o inverso; cada bloco ímpar recebe
  `section-bg` (`Views/portal/index.blade.php:69-123`).
- Ver a mensagem “Em breve / Nossa linha do tempo está sendo construída.” quando não há
  marco ativo (`:125-135`).
- Navegar para `/sobre-nos` pelo CTA final “Conheça nossa Diretoria” (`:147-151`).
- Compartilhar a URL; o `<head>` entrega OG/Twitter/canonical e `Article` JSON-LD.

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Cache de página gravado/recuperado (`page:<sha1(fullUrl)>`, TTL 5 min) | `PageCacheMiddleware` | `PageCacheMiddleware.php:13-14,31-59,88-91` |
| `Cache-Control: public, max-age=60, must-revalidate` e `Vary: Accept-Encoding` | `PublicCacheMiddleware` | `PublicCacheMiddleware.php:86-94` |
| Registro de acesso (view de SEO/bot) | `SEOService::trackAccess()` no composer de metadata | `SEOServiceProvider.php:31-36`; `SEOService.php:111-150` |
| Leitura de 3 chaves de `v2_portal_configs` | `index` | `HistoriaPortalController.php:77-87` |
| Uma query de marcos + uma de mídias | `index` + `resolveMilestoneImages` | `HistoriaPortalController.php:35-61,70` |
| Nenhuma escrita em banco a partir da página | — | `HistoriaPortalController.php:68-94` |

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

| Entrada | Resultado |
|---|---|
| `GET /historia` com marcos ativos | `200`, HTML com intro + N cartões; `<title>` = `historia_seo_title`/default, sem sufixo (o `??` curto-circuita); se a chave estiver vazia, cai para `titulo_site` (`?:`). |
| `GET /historia` sem marcos | `200`, bloco “Em breve”; o HTML ainda tem hero, intro e CTA. |
| Segunda requisição em <5 min | `200` com `X-Cache-Status: HIT` e o HTML anterior (mesmo que o CMS tenha mudado, se o cache não foi limpo). |
| Marco com `image_id` inexistente/não numérico | HTML sem `<picture>`; sem erro. |
| Marco com `description` vazio/JSON inválido | Bloco de texto vazio; sem erro. |
| `historia_seo_title` vazio (`''`) no banco | `seoTitle` cai para `titulo_site` (o composer usa `?:`); `titulo_site` `null`/ausente → Blade usa `pageTitle | siteTitle`; `titulo_site=''` → o curto-circuito do `??` mantém `<title>` vazio (borda conhecida do SVC-SEO). |
| Cache Redis indisponível | Página renderiza normalmente; warning `[PageCache] Redis indisponivel, servindo sem cache: …`. |

### 1.4 O que esta página NÃO faz

- Não tem paginação, filtro, busca ou âncora por marco.
- Não tem página de detalhe nem URL por marco (não há slug).
- Não renderiza imagem de destaque quando o id não resolve, nem dá fallback visual.
- Não lê `metadata['seo']` do marco: não existe SEO por marco.
- Não inclui o marco no índice de busca do portal.
- Não tem interação JS (sem carrossel, accordion ou lazy de seção).

## 2. Abordagem

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

Controller enxuto: coleta ativos ordenados, resolve mídias em lote, define 3 metadados e
devolve a view. A view é uma sequência de seções estáticas com um laço; o layout global
injeta `<head>` a partir do `SEOService`. O cache HTTP/Redis é aplicado por middleware
global, não pelo módulo.

```mermaid
flowchart LR
  R[GET /historia] --> M[MaintenanceMiddleware]
  M --> PC[page.cache]
  PC -->|HIT| H[HTML do cache]
  PC -->|MISS| C[HistoriaPortalController@index]
  C --> Q1[(v2_historia_milestones active ordered)]
  C --> Q2[(v2_gallery_media whereIn)]
  C --> Q3[(v2_portal_configs x3)]
  C --> V[Blade portal.index]
  V --> SEO[SEOService setters]
  SEO --> HEAD[metadata.seo composer]
  V --> PUB[public.cache headers]
```

### 2.2 Mecânica passo a passo

1. O roteador resolve `/historia` antes do fallback porque todas as rotas de módulo são
   registradas antes de `mapFallbackRoutes` (`V2ServiceProvider.php:96-140`).
2. `MaintenanceMiddleware` pode curto-circuitar com a página de manutenção.
3. `PageCacheMiddleware` só age em GET não-AJAX e fora de `cms*`, `api*`, `trabalhe-conosco*`,
   `associe-se*`, `fale-conosco*`, `_debugbar*`; portanto `/historia` **é** cacheada
   (`PageCacheMiddleware.php:16-23,68-85`).
4. `HistoriaPortalController@index` executa `Milestone::active()->ordered()->get()`
   (`HistoriaPortalController.php:70`).
5. `resolveMilestoneImages` filtra ids numéricos, busca `GalleryMedia` em uma query,
   indexa por id e injeta a relação `image` apenas quando encontrada (`:35-61`).
6. Lê `historia_seo_title|description|keywords` com `DB::table` e `??` para os defaults;
   chama `setTitle/setDescription/setKeywords` (`:73-91`).
7. Retorna `view('v2-historia::portal.index', compact('milestones'))` (`:93`).
8. O Blade estende `v2-portal::app-layout` com `pageTitle`, `pageDescription`, `gtag`,
   `schemas`, `fullWidthMain=true` e flags de header/footer desligadas (`:5-26`).
9. `app-layout` estende `base`, que inclui `metadata` → composer do `SEOService` injeta
   `seoTitle/seoDescription/seoKeywords`; a view `metadata.seo` usa `$seoTitle ?? …`
   (`SEOServiceProvider.php:38-56`; `metadata/seo.blade.php:1-31`).
10. `portal-content` renderiza header (SLE ou fallback), depois `@yield('main-section-area')`
    com as seções do módulo; ao final, `PublicCacheMiddleware` adiciona os headers HTTP.

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

- **Uma página, sem detalhe**: o conteúdo é narrativo e contínuo; a URL única simplifica
  SEO e evita N páginas pobres `[C]` (`Routes/web.php:6-9`; `SeoPortalController.php:339-350`).
- **Alternância por índice, não por campo**: economiza um campo de layout, mas amarra a
  ordem visual à posição `[C]` (`index.blade.php:70-72`).
- **SEO em `v2_portal_configs`**: permite editar sem deploy, ao custo de a página depender
  do banco; defaults garantem render mesmo sem linha `[C]`.
- **Resolução de imagem tolerante**: protege contra dados legados, ao custo de imagens
  silenciosamente ausentes `[C]`.
- **`fullWidthMain` + header/footer desligados**: a página controla o próprio hero e CTA,
  mas perde a navegação global do portal — reproduzir os flags exatos importa `[C]`.

### 2.4 Trechos de código pivô

```php
// V2/Modules/Institucional/Historia/Controllers/HistoriaPortalController.php:68-93 — pipeline do portal
$milestones = Milestone::active()->ordered()->get();
$milestones = $this->resolveMilestoneImages($milestones);

$defaultTitle = 'Nossa História — ' . config('app.name');
$defaultDesc  = 'Conheça a trajetória da SUCESU desde 1965. Marcos históricos que definiram o setor de TIC no Brasil.';
$defaultKw    = 'SUCESU, história, tecnologia, TIC, fundação, 1965, linha do tempo';

$seoTitle = \DB::table('v2_portal_configs')
    ->where('config_key', 'historia_seo_title')->value('config_value') ?? $defaultTitle;
// ... description, keywords ...
$this->seo->setTitle($seoTitle);
return view('v2-historia::portal.index', compact('milestones'));
```

Mostra a única dependência de banco além dos marcos e os defaults exatos. `value()` devolve
`null` quando não há linha — o `??` só cobre `null`, não `''`.

```blade
{{-- V2/Modules/Institucional/Historia/Views/portal/index.blade.php:68-98 — alternância texto/imagem --}}
@if($milestones->count() > 0)
    @foreach($milestones as $idx => $milestone)
        @php
            $isEven = $idx % 2 === 1;
            $bgClass = $isEven ? 'section-bg' : '';
            $media = $milestone->image;
        @endphp
        <section class="fix section-padding {{ $bgClass }}">
            ...
            @if($isEven)
                <div class="col-lg-6 order-lg-1"> ... texto ... </div>
                <div class="col-lg-6 order-lg-2">
                    @if($media)
                        @include('v2-service-media::components.picture', [
                            'originalPath' => $media->file_path,
                            'variants'     => $media->variants ?? [],
                            'alt'          => $media->alt_text ?: $milestone->title,
                            'class'        => 'milestone-img img-fluid',
                            'lazy'         => true,
                        ])
                    @endif
                </div>
```

O primeiro marco (índice 0) tem imagem à esquerda; “par/ímpar” no código é o oposto do
nome da variável (`$isEven` é verdadeiro quando o índice é ímpar).

```blade
{{-- V2/Modules/Institucional/Historia/Views/portal/index.blade.php:82-86 — conteúdo do marco --}}
<div class="milestone-period">{{ $milestone->period }}</div>
<h3>{{ $milestone->title }}</h3>
<div class="milestone-accent"></div>
{!! $milestone->getHtmlDescription() !!}
```

`period`/`title` são escapados; o HTML vem do renderer do CRE (não escapado). O contrato é:
o AST deve ser confiável, pois não há sanitização adicional no portal.

```blade
{{-- V2/Modules/Institucional/Historia/Views/portal/index.blade.php:5-26 — flags de layout e JSON-LD --}}
@extends('v2-portal::app-layout', [
    'pageTitle' => 'Nossa História — ' . config('app.name'),
    'pageDescription' => 'Conheça a trajetória da SUCESU desde 1965. ...',
    'gtag' => ['page_context' => 'historia'],
    'schemas' => [[
        '@context' => 'https://schema.org',
        '@type' => 'Article',
        'name' => 'Nossa História',
        'description' => 'Conheça a trajetória da SUCESU desde 1965.',
        'url' => route('v2.historia.portal.index', [], false)
    ]],
    'fullWidthMain' => true,
    'showHeader' => false,
    'showMainHeader' => false,
    'showMainFooter' => false,
    'showFooter' => false,
    'showAccessibilityBar' => false,
    'showMainLeftArea' => false,
    'showMainRightArea' => false,
])
```

Os `pageTitle`/`pageDescription` só valem se `$seoTitle`/`$seoDescription` forem `null`. O
composer usa `?:` (não `??`), então um serviço vazio cai para `titulo_site`; com `$seoTitle`
preenchido, o `??` de `metadata/seo.blade.php:2` curto-circuita e não acrescenta ` | siteTitle`.
O `schemas` é injetado como fallback **adicional** ao `seo->getSchemas()`,
gerando dois JSON-LD se o serviço também tiver schemas (`metadata/seo.blade.php:36-49`).

### 2.5 Algoritmos, consultas e custos

- `Milestone::active()->ordered()->get()` → `WHERE is_active = true AND deleted_at IS NULL
  ORDER BY sort_order ASC, created_at ASC` (usa `idx_v2_hist_milestones_active`/`_order`).
- 1 query `whereIn(id, [...])` para todas as mídias; custo O(1) queries por página.
- 3 queries `value()` em `v2_portal_configs` (uma por chave), sem cache de aplicação.
- Render: N seções; `picture` pode gerar várias tags por imagem.
- Cache: 5 min no Redis (chave por URL completa) + 60 s de cache HTTP.
- Sem N+1 e sem paginação: a página cresce linearmente com o número de marcos.

## 3. Particularidades

### 3.1 O que só existe aqui

- **`historia_seo_*`** como fonte de SEO da página (3 chaves globais do módulo).
- **`Article` JSON-LD** fixo com `name: 'Nossa História'` e URL relativa `route(..., false)`.
- **`gtag.page_context = 'historia'`**.
- **Alternância de layout pelo índice** com `section-bg` nos ímpares.
- **Página inteira elegível a cache de 5 min**, ao contrário de `/fale-conosco`.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-HISTORIA-PORTAL-01` | Apenas ativos, ordenados por `sort_order, created_at`. | Desativado/arquivado não aparece. | `Milestone.php:85-97` |
| `RN-HISTORIA-PORTAL-02` | SEO: `historia_seo_*` do banco vence; default do controller se ausente; se o serviço ficar vazio, o composer usa `titulo_site`; `pageTitle` do Blade nunca vence no fluxo normal. | Título exibido é o `seoTitle` do composer (banco/default/`titulo_site`), **sem** ` | siteTitle`; a concatenação do Blade só executa quando `$seoTitle` é `null`. | `HistoriaPortalController.php:77-91`; `SEOServiceProvider.php:47`; `metadata/seo.blade.php:2` |
| `RN-HISTORIA-PORTAL-03` | `image_id` só resolve se numérico e presente em `v2_gallery_media`. | Sem imagem para legado; sem erro. | `HistoriaPortalController.php:37-58` |
| `RN-HISTORIA-PORTAL-04` | `alt` = `alt_text` da mídia ou `title` do marco. | ALT nunca vazio quando há título. | `index.blade.php:93,105` |
| `RN-HISTORIA-PORTAL-05` | HTML do corpo é `{!! !!}` do renderer. | AST malicioso vira HTML; não há sanitização no módulo. | `index.blade.php:85,116` |
| `RN-HISTORIA-PORTAL-06` | Cache Redis por URL completa, TTL 5 min; `X-Cache-Status` HIT/MISS. | Conteúdo pode ficar até 5 min desatualizado se a limpeza falhar. | `PageCacheMiddleware.php:13-14,35-58,88-91` |
| `RN-HISTORIA-PORTAL-07` | Headers HTTP públicos de 60 s com `must-revalidate`. | Navegador/CDN revalidam após 60 s. | `PublicCacheMiddleware.php:86-94` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **`historia_seo_title = ''`** (gravado pelo form quando o campo é limpo) **não** produz
  `<title>` vazio: o controller grava `''`, o composer faz `getTitle() ?: titulo_site` e o
  `??` do Blade só concatena `pageTitle | siteTitle` se `$seoTitle` for `null`. Com
  `titulo_site` nulo, o título vira `Nossa História — {app.name} | Portal Oficial`; com
  `titulo_site=''`, o curto-circuito mantém `<title>` vazio (mesma borda de `SVC-SEO`).
- **`route(..., false)`** no JSON-LD gera `/historia` relativo; o `@context` é `https://schema.org`.
- **Dois JSON-LD**: se o `SEOService` receber schemas (não é o caso aqui) e o Blade passar
  `schemas`, ambos são impressos.
- **Meta `title` igual ao `<title>`**: `metadata/seo.blade.php:3` usa `$seoTitle` sem sufixo.
- **Marco sem imagem**: o cartão continua com a coluna vazia (não há placeholder), gerando
  buraco visual em telas largas.
- **Marco com imagem mas `file_path` vazio**: `picture` recebe path vazio; pode gerar
  `<img src="">` ou nada, dependendo do componente `SVC-MEDIA` (lacuna).
- **Mais de 20 marcos**: não há paginação; a página fica muito longa e o cache cresce.
- **Cache após edição**: `clearPageCache` apaga `page:*`; se o Redis falhar, a página pública
  continua antiga por até 5 min.

### 3.4 Armadilhas de reimplementação

- **Não usar `$pageTitle` como fonte de SEO**: no fluxo normal o `SEOProvider` preenche
  `seoTitle` e o literal do Blade é ignorado.
- **Não remover os defaults do controller** esperando que o Blade supra: sem linha e sem
  defaults, o `<title>` cai no ramo `pageTitle | siteTitle` (com `Portal Oficial`), não fica
  vazio.
- **Não trocar `{!! !!}` por escape** sem mudar o editor: o corpo é HTML gerado, escapá-lo
  mostraria marcação.
- **Não esquecer o `order-lg-*`**: a alternância visual depende das classes de ordem, não só
  da posição no DOM.
- **Não aplicar cache público ao `/historia` sem o `page.cache`**: os dois middlewares têm
  papéis diferentes (Redis vs headers).

### 3.5 Divergências documentação × código e dívidas

- **Flags de layout no Blade** (`showHeader=false` etc.) são repassadas ao `app-layout` e
  chegam ao `portal-content` por herança de escopo; não há validação de que o portal real
  use todas (ex.: `showFooter` é consumido adiante no partial).
- **`$isEven` nomeado ao contrário** (`$idx % 2 === 1`).
- **Hero e CTA fixos no Blade**, sem vínculo com `LayoutBaseConfig`/SLE — diferente de
  `/fale-conosco`, que lê dados do SLE.
- **`\DB` inline** e três queries por chave em vez de uma leitura agrupada.
- **Nenhum teste** de cache/invalidação da página no módulo.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `GET /historia` | rota | `v2.historia.portal.index` | módulo com rota web registrada | visitantes, sitemap |
| `HistoriaPortalController@index` | ação | `(): View` | `SEOService` resolvível | rota |
| `v2-historia::portal.index` | view | `$milestones` (Collection) | cada item com `image` opcional | controller |
| `Milestone::getHtmlDescription` | método | `(): string` | AST válido para conteúdo | view |
| `picture` | componente | `originalPath`, `variants`, `alt`, `class`, `lazy` | mídia com `file_path` | view |

### 4.2 Contrato de UI/JS

- Não há JS do módulo. O layout carrega `assets/js/masks.js` (defer) e o botão “voltar ao
  topo”, sem relação com esta página.
- Classes de contrato visual: `.milestone-card`, `.milestone-img`, `.milestone-period`,
  `.milestone-accent`, `.milestone-content`, `.breadcrumb-wrapper`, `.section-title`,
  `.video-tour-section` (`index.blade.php:30-40,43-54,57-65,138-156`).
- `picture` exige `alt`; o módulo sempre passa um valor (`alt_text` ou `title`).
- Não há IDs de DOM além de `#main-content` (do layout).

### 4.3 Ordem e ciclo de vida

1. Rotas de módulo são registradas antes do fallback (`/historia` não colide com
   `/{slug}`).
2. Middlewares globais: manutenção → page cache → controller → public cache.
3. `SEOService` é singleton; os setters rodam antes da renderização do `<head>`.
4. O cache é populado somente em resposta 2xx; erros/redirects não são cacheados
   (`PageCacheMiddleware.php:42-59`).
5. Invalidação ocorre no CMS via `KEYS page:*`; não há invalidação por evento de model.

### 4.4 Receitas de uso

```php
// Reutilizar a lista de marcos em outro renderer (ex.: feed ou landing)
$milestones = \V2\Modules\Institucional\Historia\Models\Milestone::active()->ordered()->get();
foreach ($milestones as $m) {
    echo $m->title . ' — ' . $m->getHtmlDescription();
}
```

```blade
{{-- Link canônico para a página (usar a rota nomeada, não string) --}}
<a href="{{ route('v2.historia.portal.index') }}">Nossa História</a>
```

```http
GET /historia HTTP/1.1
Host: exemplo.org

HTTP/1.1 200 OK
X-Cache-Status: MISS
Cache-Control: public, max-age=60, must-revalidate
Vary: Accept-Encoding
```

### 4.5 Anti-uso

- **Criar `/historia/{slug}`** como detalhe: o módulo não tem slug nem SEO por marco; a
  rota cairia no catch-all `BasicPage` e mudaria o contrato do sitemap.
- **Confiar no `pageTitle` do Blade** para o título: ele é ignorado quando o SEO está setado.
- **Remover o `page.cache`** para “sempre ver atualizado”: muda o custo e o comportamento
  observável (`X-Cache-Status`); a invalidação correta é no CMS.

### 4.6 Custos e limites da integração

- 1 + 1 + 3 queries por render (marcos, mídias, SEO) — barato, mas sem cache de objeto.
- Cache Redis de 5 min por URL; `KEYS page:*` é O(n) a cada mutação do CMS.
- Sem limite de marcos na página; sem paginação/lazy de seção.
- Imagens: `picture` responsivo pode multiplicar requisições; `loading=lazy` no fallback.

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Efeito |
|---|---|---|---|---|---|---|
| grupo | — | `/historia` | — | `web`; `MaintenanceMiddleware`; `page.cache`; `public.cache` | — | aplica manutenção/cache |
| endpoint | GET | `/historia` | `v2.historia.portal.index` | idem | `HistoriaPortalController@index` | HTML da linha do tempo |

Sem query string, sem parâmetros de rota, sem throttle, sem auth. CSRF não se aplica (GET).

## 6. Dados e modelo de domínio

Leitura de `v2_historia_milestones` (ver [`dados.md`](dados.md)), `v2_gallery_media`
(`id`, `file_path`, `variants`, `alt_text`) e `v2_portal_configs`
(`historia_seo_title|description|keywords`). Nenhuma escrita.

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant V as Visitante
  participant MW as page.cache/public.cache
  participant C as HistoriaPortalController
  participant DB as Banco
  participant SEO as SEOService
  V->>MW: GET /historia
  alt cache HIT
    MW-->>V: HTML + X-Cache-Status HIT
  else MISS
    MW->>C: index()
    C->>DB: marcos ativos + mídias + 3 configs
    C->>SEO: setTitle/setDescription/setKeywords
    C-->>MW: View
    MW->>MW: grava page:<sha1> (5 min)
    MW-->>V: HTML + X-Cache-Status MISS
  end
```

## 8. UI CMS

Não se aplica; ver [`cms.md`](cms.md). O CMS só influencia esta página pela ordem, status,
imagem e chaves de SEO.

## 9. UI Portal

### 9.1 Estrutura e hierarquia

1. `<div class="breadcrumb-wrapper">` com fundo `assets/sucesu/img/hero/breadcrumb.png`,
   breadcrumb `Home › Nossa História` e `<h1>Nossa História</h1>`
   (`index.blade.php:43-54`).
2. `<section>` de intro com `sub-title` “Trajetória”, `<h2>` (duas linhas fixas) e parágrafo
   fixo (`:57-65`).
3. Para cada marco: `<section class="fix section-padding [section-bg]">` → `.milestone-card`
   → `.row.g-5.align-items-center` com texto e imagem (`:69-123`).
4. Empty state (`:125-135`).
5. CTA `.video-tour-section` com fundo de hero, sub-título “Há mais de 50 anos conectando
   pessoas e ideias”, `<h2>` fixo e botão para `/sobre-nos` (`:138-156`).

### 9.2 SEO (origem de cada valor)

| Metadado | Origem | Fallback |
|---|---|---|
| `<title>` | `seoTitle` = `$seo->getTitle() ?: titulo_site` (composer; serviço ← `historia_seo_title`) | `pageTitle`/`siteTitle` **somente** se `$seoTitle` for `null`; com `$seoTitle` não nulo o `??` curto-circuita e não duplica `siteTitle` (se `titulo_site=''`, o curto-circuito mantém vazio) |
| `meta title`/`og:title`/`twitter:title` | idem | idem |
| `meta description`/`og:description`/`twitter:description` | `historia_seo_description` | `descricao` global |
| `meta keywords` | `historia_seo_keywords` | `palavras_chaves` global |
| `og:image`/`twitter:image` | `seo->getCoverImage()` (não setado) | `$coverImage` (não setado) → vazio |
| canonical | `seo->getCanonical()` (não setado) | `url()->current()` |
| JSON-LD | `schemas` do Blade (`Article`) + `seo->getSchemas()` | — |
| `og:type`/`twitter:card` | fixos (`website`, `summary_large_image`) | — |

Fonte: `metadata/seo.blade.php:1-50`; `SEOServiceProvider.php:38-56`; `index.blade.php:5-17`.

### 9.3 Mídia

- Componente `picture` com `variants` (WebP), `originalPath`, `class="milestone-img img-fluid"`,
  `alt` = `alt_text ?: title`, `lazy=true`.
- CSS: `.milestone-img { width:100%; max-height:420px; object-fit:cover; border-radius:12px }`
  (`index.blade.php:33`).
- Sem `width`/`height` explícitos → risco de CLS.
- Sem fallback quando `image_id` não resolve.

### 9.4 Responsividade e print

- `col-lg-6` para texto/imagem; abaixo de `lg` empilha.
- `@media(max-width:991px)` adiciona margem inferior ao card e à imagem (`:39`).
- Não há CSS de print específico no módulo; o layout global tem partials de print.

### 9.5 Cache e invalidação

- Redis: chave `page:<sha1(fullUrl)>`, TTL 5 min, `X-Cache-Status`.
- HTTP: `Cache-Control: public, max-age=60, must-revalidate`, `Vary: Accept-Encoding`.
- Invalidação: apenas `MilestoneCmsController::clearPageCache()` em mutações do CMS.
- Alteração direta no banco (seeder/SQL) **não** invalida o cache.

### 9.6 Acessibilidade

- `<h1>` único no hero; `<h2>` na intro e no CTA; `<h3>` por marco — hierarquia correta.
- Imagens têm `alt` derivado (`alt_text` ou título); imagem decorativa não é suportada
  (sempre há `alt`).
- Breadcrumb é uma `<ul>` sem `aria-label`/`nav`; ícone `bi-chevron-right` sem `aria-hidden`.
- CTA é um link com texto; contraste depende do tema (`--tenant-primary` / branco).

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

- Rota pública, sem permissão.
- Três chaves de banco (`historia_seo_*`), grupo `MODULOS`; sem env.
- `config('app.name')` nos defaults de título.
- Flag de módulo não afeta a rota web: `module.active:Historia` só é aplicado a `Routes/cms.php`
  (`V2ServiceProvider.php:215-219,299-307`). Desativar o módulo **não** derruba `/historia`.

## 11. Observabilidade, erros e limites

- `Log::warning('[PageCache] Redis indisponivel, servindo sem cache: …')` quando o cache falha.
- `Log::error('[Milestone] Erro ao renderizar descrição: …')` se o renderer lançar.
- `SEOService::trackAccess()` registra visita (view/bot) — exclui `cms/*`, `api/*`, assets;
  `/historia` **é** rastreada.
- Erros possíveis: `404` se a rota não existir (módulo removido) ou `500` se
  `SEOService`/view falharem; `503` em manutenção.
- Limites: sem paginação; sem throttle; sem limite de tamanho de HTML cacheado.

## 12. Seeds e dados iniciais

O conteúdo inicial vem de `HistoriaMilestonesSeeder` (unidade `DATA-SEEDS-BASICPAGES`):
5 marcos com imagens da galeria e AST JSON. Não há seed de `historia_seo_*` nem de página
`v2_basic_pages` para o slug `historia`. Ver [`spec.md`](spec.md) §12.

## 13. Lacunas

- `LG-HISTORIA-PORTAL-01` — Origem do `siteTitle`/`descricao` globais: vêm de
  `v2_portal_configs` via `PortalConfigsService`; a composição derivada do código
  (`seoTitle ?: titulo_site`, senão `pageTitle | siteTitle`) não foi exercitada em runtime
  (pertence a `SVC-SEO`).
- `LG-HISTORIA-PORTAL-02` — Comportamento do `picture` com `file_path` vazio/`null` não foi
  verificado no componente; impacto visual.
- `LG-HISTORIA-PORTAL-03` — Se `trackAccess` grava em tabela própria e por quanto tempo
  retém IP/UA — pertence a `SVC-SEO`.
- `LG-HISTORIA-PORTAL-04` — Não há teste de cache/invalidação nem de HIT/MISS para esta rota.

## 14. Checklist de paridade

- [ ] `PC-HISTORIA-PORTAL-01` — `GET /historia` retorna 200 com hero, intro, N cartões e CTA.
- [ ] `PC-HISTORIA-PORTAL-02` — Só ativos, ordenados por `sort_order, created_at`; alternância
      texto/imagem com `section-bg` nos índices ímpares.
- [ ] `PC-HISTORIA-PORTAL-03` — Empty state com “Em breve” e “Nossa linha do tempo está sendo construída.”.
- [ ] `PC-HISTORIA-PORTAL-04` — SEO lido de `historia_seo_*`; defaults exatos quando ausente;
      `pageTitle` do Blade não prevalece.
- [ ] `PC-HISTORIA-PORTAL-05` — JSON-LD `Article` com `name`, `description` e `url` relativa;
      `gtag.page_context=historia`.
- [ ] `PC-HISTORIA-PORTAL-06` — `picture` com variantes, `alt = alt_text ?: title`, `lazy=true`.
- [ ] `PC-HISTORIA-PORTAL-07` — Cache Redis 5 min com `X-Cache-Status` e headers HTTP
      `public, max-age=60, must-revalidate` + `Vary: Accept-Encoding`.
- [ ] `PC-HISTORIA-PORTAL-08` — Marco sem imagem renderiza cartão sem `<picture>` e sem erro.
- [ ] `PC-HISTORIA-PORTAL-09` — `historia_seo_title=''` cai para `titulo_site`; com
      `titulo_site` nulo, `<title>` = `pageTitle | siteTitle`, sem duplicar o sufixo; com
      `titulo_site=''`, `<title>` fica vazio (borda do SVC-SEO).
- [ ] `PC-HISTORIA-PORTAL-10` — `/historia` listada no sitemap com `priority 0.5`, `monthly`.

## 15. Evidências consultadas

- `V2/Modules/Institucional/Historia/Views/portal/index.blade.php:1-157`.
- `V2/Modules/Institucional/Historia/Controllers/HistoriaPortalController.php:1-95`.
- `V2/Modules/Institucional/Historia/Models/Milestone.php:65-97`.
- `V2/Core/Middleware/PageCacheMiddleware.php:1-91`; `V2/Core/Middleware/PublicCacheMiddleware.php:1-95`.
- `V2/Core/Providers/V2ServiceProvider.php:96-140,203-224,293-340`.
- `V2/Services/SEO/Providers/SEOServiceProvider.php:1-70`;
  `V2/layouts/portal/metadata/seo.blade.php:1-50`;
  `V2/Services/SEO/Controllers/SeoPortalController.php:325-355,35-55`.
- `V2/Services/Media/Views/components/picture.blade.php:1-40`.
