---
id: UI-PORTAL-DESIGN-SYSTEM
tipo: ui
onda: 1
status: concluido
unidades: [UI-PORTAL]
fontes:
  - V2/layouts/portal/app-layout.blade.php
  - V2/layouts/portal/base.blade.php
  - V2/layouts/portal/base-struct/colors.blade.php
  - V2/layouts/portal/base-struct/fonts.blade.php
  - V2/layouts/portal/base-struct/normalize.blade.php
  - V2/layouts/portal/base-struct/global-banner.blade.php
  - V2/layouts/portal/base-struct/accessibility-skips.blade.php
  - V2/layouts/portal/metadata.blade.php
  - V2/layouts/portal/metadata/seo.blade.php
  - V2/layouts/portal/metadata/gtag.blade.php
  - V2/layouts/portal/metadata/metaheader.blade.php
  - V2/layouts/portal/metadata/metafooter.blade.php
  - V2/layouts/portal/header/header.blade.php
  - V2/layouts/portal/header/header-logo.blade.php
  - V2/layouts/portal/header/header-nav.blade.php
  - V2/layouts/portal/header/header-search-area.blade.php
  - V2/layouts/portal/footer/footer.blade.php
  - V2/layouts/portal/footer/footer-logo.blade.php
  - V2/layouts/portal/footer/footer-services-area.blade.php
  - V2/layouts/portal/footer/footer-public-contact.blade.php
  - V2/layouts/portal/footer/footer-social-network.blade.php
  - V2/layouts/portal/footer/footer-copyright.blade.php
  - V2/layouts/portal/footer/footer-cms-metadata.blade.php
  - V2/layouts/portal/footer/footer-transparencia-lai.blade.php
  - V2/layouts/portal/main-section/main-header.blade.php
  - V2/layouts/portal/main-section/main-left-area.blade.php
  - V2/layouts/portal/main-section/main-right-area.blade.php
  - V2/layouts/portal/main-section/main-footer.blade.php
  - V2/layouts/portal/main-section/main-content.blade.php
  - V2/layouts/portal/partials/portal-content.blade.php
  - V2/layouts/portal/partials/pagination.blade.php
  - V2/layouts/portal/print/print-only.blade.php
  - V2/layouts/portal/print/print-header.blade.php
  - V2/layouts/portal/print/print-footer.blade.php
  - V2/layouts/portal/search/index.blade.php
  - V2/layouts/portal/errors/403.blade.php
  - V2/layouts/portal/errors/404.blade.php
  - V2/layouts/portal/errors/419.blade.php
  - V2/layouts/portal/errors/500.blade.php
  - V2/layouts/portal/errors/503.blade.php
  - public/assets/css/portal/portal-base.css
  - public/assets/css/portal/portal-components.css
  - public/assets/js/portal-init.js
depende_de: [CORE-BOOT, SVC-SEO, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-SUPPORT-SUPPORTCONFIGS]
consumidores: [MOD-LAYOUTS-BASICPAGE, MOD-LAYOUTS-HOMEPAGE, MOD-PUBLICACOES-NOTICIAS, MOD-PUBLICACOES-AGENDA, MOD-CONEXOES-ASSOCIADOS, MOD-CONEXOES-PARCEIROSINSTITUCIONAIS, MOD-CONEXOES-UNIDADEREGIONAL, MOD-INSTITUCIONAL-SOBRENOS, SVC-SEARCH]
criticidade: alta
dono: UiPortal
---

# UI Portal — Design System e Layout Global

> **Alvo de reimplementação.** Este documento descreve o **DOM/estado/comportamento** do
> legado (Bootstrap 5, jQuery, AdminLTE). A reconstrução usa Blade + Tailwind 4 +
> daisyUI 5 + Lucide, com o shell `Layout\Cms`/`Layout\Portal` e JS mínimo vendorizado.
> Preserve ids, eventos, aria, mensagens e contratos; troque classes CSS e bibliotecas.
> Ver [`09_reimplementacao/pilha-alvo-public-v7.md`](../09_reimplementacao/pilha-alvo-public-v7.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Toda página pública herda `html[lang=pt-br]`, CSS/JS vendorizados, tokens de tema injetados de `v2_portal_configs`, barra de acessibilidade/VLibras, header, conteúdo em `<main id="main-content">`, rodapé institucional e motor de impressão. |
| Entrada | `@extends('v2-portal::app-layout', [...])`, `@section('main-section-area')`, `@push` nos stacks `head-styles/styles/scripts/footer-scripts`, variáveis compartilhadas por três ViewComposers. |
| Saída | HTML completo do portal (head com SEO, JSON-LD, GA), markup do chrome (barra, header, banner, footer, LAI), clone de impressão em `#print-engine`. |
| Efeitos colaterais | 1 registro de visita SEO por view composta (até 8 por pageview) no buffer `seo_access_buffer`; flag `manifest.json`/`sw.js` requisitados pelo browser; scripts globais do admin injetados sem escape. |
| Particularidade central | O chrome não é composto por componentes de módulo: é um único `V2/layouts/portal/base.blade.php` com includes condicionados por flags da tabela `v2_portal_configs` e por payloads de `v2_layout_base_configs`; o `/` do projeto é `homepage` do módulo Homepage, não uma view do layout. |
| Como se integra | Namespace `v2-portal::` registrado em `V2/Core/Providers/V2ServiceProvider.php:41`; layouts `base` → `app-layout` → `partials.portal-content`. |
| Se removido, quebra | 35 blades que fazem `@extends('v2-portal::app-layout')`, as 5 páginas de erro, a busca global e o editor estrutural (SLE) que invoca parciais via `sle_component()`. |

## 1. Efeito

### 1.1 Garantias para qualquer página

- `<!DOCTYPE html>`, `<html lang="pt-br" dir="ltr">`, `<meta charset="utf-8">`, viewport e `csrf-token` (`V2/layouts/portal/base.blade.php:1-6`). [C]
- Manifesto PWA em `url('manifest.json')`, `theme-color` derivado de `$portalTheme['primary_color']` e metas `application-name=SUCESU` (`V2/layouts/portal/base.blade.php:8-15`). [C]
- Favicon/`apple-touch-icon` vindos de `layoutConfigs['logos']->payload['favicon']` via `v2_storage_url()`; fallback literal `url('favicon.ico')` (`V2/layouts/portal/base.blade.php:17-33`). [C]
- Bloco SEO completo (`metadata`) com `<title>`, description, keywords, Open Graph, Twitter, canonical, tokens globais e JSON-LD (`V2/layouts/portal/base.blade.php:35-42`; `V2/layouts/portal/metadata/seo.blade.php:1-50`). [C]
- CSS vendorizado local + tokens inline: Bootstrap 5.3.3, Bootstrap Icons, Nunito Sans local, `colors`, `fonts`, `normalize`, depois `public/assets/css/portal/portal-base.css`, `portal-nav.css`, `portal-a11y.css`, `public/assets/css/portal/portal-components.css` (`V2/layouts/portal/base.blade.php:44-66`). [C]
- Skip links eMAG sempre no body, antes de qualquer conteúdo (`V2/layouts/portal/base.blade.php:97-98`; `V2/layouts/portal/base-struct/accessibility-skips.blade.php:1-12`). [C]
- Conteúdo do site dentro de `<div id="app" class="d-print-none">` (`V2/layouts/portal/base.blade.php:105-107`), garantindo que a impressão use somente o motor `#print-engine`. [C]
- jQuery 3.6 síncrono, Bootstrap bundle `defer`, `public/assets/js/portal-init.js` `defer` (`V2/layouts/portal/base.blade.php:117-123`). [C]
- `manifest.json` e `sw.js` são referenciados literalmente, sem checagem de existência (`V2/layouts/portal/base.blade.php:9`, `:152-158`). [C]

### 1.2 O que muda por página

Variáveis aceitas via array de `@extends` (o merge de `get_defined_vars()` do Blade faz as demais chegarem ao layout):

| Variável | Default | Efeito |
|---|---|---|
| `pageTitle` | `null` | Entra no `<title>` quando não há `seoTitle` e em `meta[property=og:title]` como fallback (`V2/layouts/portal/metadata/seo.blade.php:2,10`). |
| `title` | — | `<h1 class="h2">` e breadcrumb de `main-header` (`V2/layouts/portal/main-section/main-header.blade.php:11,17`). |
| `description` | — | Parágrafo `lead` do `main-header` (`V2/layouts/portal/main-section/main-header.blade.php:20-22`). |
| `pageDescription` | `null` | Fallback de `meta description`/OG (`V2/layouts/portal/metadata/seo.blade.php:4,11`). |
| `fullWidthMain` | `false` | Troca container/grid por `<main id="main-content">` direto com `@yield('main-section-area')` (`V2/layouts/portal/partials/portal-content.blade.php:31-36`). |
| `showHeader` | `true` | Inclui `header.header`; `false` remove também o banner global (`V2/layouts/portal/partials/portal-content.blade.php:21-29`). |
| `showFooter` | `true` | Inclui `footer.footer` (`V2/layouts/portal/partials/portal-content.blade.php:117-125`). |
| `showMainHeader` | `true` | Inclui breadcrumb/título (`V2/layouts/portal/partials/portal-content.blade.php:41-52`). |
| `showMainLeftArea` / `showMainRightArea` | `false` | Criam as colunas `<aside>` e mudam a largura da `<main>` (`V2/layouts/portal/partials/portal-content.blade.php:57-88`). |
| `showMainFooter` | `true` | Inclui botões Voltar/Imprimir e “Atualizado em” (`V2/layouts/portal/partials/portal-content.blade.php:91-104`). |
| `showFooterLai` | `true` | Pula o painel LAI no rodapé (`V2/layouts/portal/partials/portal-content.blade.php:121-123`; `V2/layouts/portal/footer/footer.blade.php:25`). |
| `custom-header`, `custom-footer` | — | Seções que substituem completamente header/footer padrão (`V2/layouts/portal/partials/portal-content.blade.php:22-24`, `118-120`). |
| `main-header-area`, `main-left-area`, `main-right-area`, `main-footer-area` | — | Seções que substituem os parciais correspondentes (`V2/layouts/portal/partials/portal-content.blade.php:44-48,59-66,75-84,94-98`). |
| `head-styles` (`@section`/`@push`), `styles` (`@push`) | vazio | CSS blocking no `<head>` (`V2/layouts/portal/base.blade.php:73-74,91`). |
| `scripts`, `footer-scripts` (`@push`) | vazio | JS no fim do body (`V2/layouts/portal/metadata/metafooter.blade.php:1-3`). |

Variáveis de layout sem efeito observado: `showAccessibilityBar` (presente em 27 blades, incluindo previews de CMS; o layout lê apenas a flag `accessibility_bar_enabled`) e `gtag`/`coverImage` (ver §3.3). [C]

### 1.3 O que o layout NÃO faz

- Não carrega JS de módulo automaticamente: módulos devem `@push('scripts')` ou `@push('footer-scripts')` (`V2/layouts/portal/metadata/metafooter.blade.php:2-3`). [C]
- Não gera breadcrumb além de “Início” + página atual: níveis intermediários vêm de `@yield('breadcrumbs')` (`V2/layouts/portal/main-section/main-header.blade.php:10`). [C]
- Não define cache HTTP, autenticação, CSRF de rotas nem tratamento de 404: a seleção de view de erro é do `App\Exceptions\Handler` (`app/Exceptions/Handler.php:31-49`). [C]
- Não normaliza `<main>`: se a página usar `main-content` dentro do grid, o id duplica (ver §3.3). [C]
- Não calcula canonical por entidade: `SEOService::getCanonical()` nunca é chamado com `setCanonical()` e o canonical é sempre `url()->current()`. [C]
- Não faz fallback de menu: sem `links_header` o header mostra apenas “Início” (`V2/layouts/portal/header/header-nav.blade.php:10-14`). [C]
- Não adiciona classes de tema ao `<body>`; o tema é 100% variáveis CSS no `:root` (`V2/layouts/portal/base-struct/colors.blade.php:9-32`). [C]

## 2. Abordagem

### 2.1 Hierarquia de composição

```mermaid
flowchart TD
  page["página do módulo<br/>@extends('v2-portal::app-layout')"] --> app["app-layout<br/>@section('layout-content')"]
  app --> content["partials/portal-content"]
  content --> bar["header/header-accessibility<br/>(flag accessibility_bar_enabled)"]
  content --> hdr["header/header (ou bloco SLE 'header')"]
  hdr --> logo["header/header-logo"]
  hdr --> nav["header/header-nav"]
  hdr --> search["header/header-search-area"]
  content --> banner["base-struct/global-banner"]
  content --> main["<main id=main-content><br/>@yield('main-section-area')"]
  main --> mh["main-section/main-header"]
  main --> left["main-section/main-left-area"]
  main --> right["main-section/main-right-area"]
  main --> mf["main-section/main-footer"]
  content --> ftr["footer/footer"]
  ftr --> flogo["footer/footer-logo"]
  ftr --> fserv["footer/footer-services-area"]
  ftr --> fcont["footer/footer-public-contact"]
  ftr --> fsoc["footer/footer-social-network"]
  ftr --> flai["footer/footer-transparencia-lai"]
  ftr --> fcopy["footer/footer-copyright"]
  ftr --> fmeta["footer/footer-cms-metadata"]
  app --> base["base: head + skip links + #app + widget + print-engine + scripts"]
```

Todo `@include` usa o namespace `v2-portal::`, registrado em `V2ServiceProvider::boot()` (`V2/Core/Providers/V2ServiceProvider.php:38-42`). [C]

### 2.2 Ordem de carregamento

1. `charset`, `viewport`, `csrf-token`, manifesto/PWA, `theme-color`, favicon (`V2/layouts/portal/base.blade.php:3-33`).
2. `metadata` → `gtag` (se houver GA) → `seo` → `@stack('json-ld')` (`V2/layouts/portal/metadata.blade.php:1-10`).
3. Bootstrap CSS, Bootstrap Icons, Nunito Sans, preconnects (`V2/layouts/portal/base.blade.php:44-55`).
4. Três `<style>` inline: `colors` → `fonts` → `normalize` (`V2/layouts/portal/base.blade.php:57-59`).
5. Quatro CSS do portal (`V2/layouts/portal/base.blade.php:61-66`).
6. `seoHeaderScripts` cru do admin (`V2/layouts/portal/metadata/metaheader.blade.php:1`).
7. `@yield('head-styles')` e `@stack('head-styles')` (`V2/layouts/portal/base.blade.php:73-74`).
8. Blocos SLE `css_head` e `js_head` (`V2/layouts/portal/base.blade.php:76-90`).
9. `@stack('styles')` (`V2/layouts/portal/base.blade.php:91`).
10. Body: skip links → VLibras (flag) → `#app` → widget (flag) → `#print-engine` (`V2/layouts/portal/base.blade.php:96-115`).
11. jQuery síncrono → Bootstrap bundle `defer` → `public/assets/js/portal-init.js` `defer` (`V2/layouts/portal/base.blade.php:117-123`).
12. Blocos SLE `js_foot` e `css_foot` (`V2/layouts/portal/base.blade.php:125-139`).
13. `<style>` do widget de acessibilidade, se qualquer das três flags estiver ligada (`V2/layouts/portal/base.blade.php:141-144`).
14. `seoFooterScripts` + `@stack('scripts')` + `@stack('footer-scripts')` (`V2/layouts/portal/metadata/metafooter.blade.php:1-3`).
15. Overlay da guia de leitura e registro do service worker (`V2/layouts/portal/base.blade.php:149-158`).

Pushes feitos por parciais incluídos **antes** de `base` (todo o `layout-content` é renderizado quando `app-layout` executa, e `@extends` é compilado para o rodapé da view) chegam aos stacks do `<head>`. Já um parcial incluído dentro do body de `base` **depois** do `<head>` não pode usar `@push('head-styles')` — por isso `V2/layouts/portal/header/accessibility/styles.blade.php:1` declara que emite `<style>` inline. [C]

### 2.3 Trechos de código pivô

```blade
{{-- V2/layouts/portal/base.blade.php:44-68 — ordem CSS: vendor, tokens inline, design system, injeção do admin --}}
<link rel="stylesheet" href="{{ asset('assets/vendor/bootstrap5/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ asset('assets/vendor/bootstrap-icons/bootstrap-icons.css') }}">
<link rel="stylesheet" href="{{ asset('assets/vendor/nunito-sans/nunito-sans.css') }}">

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://www.vlibras.gov.br">

{{-- Design System Variables --}}
@include('v2-portal::base-struct.colors')
@include('v2-portal::base-struct.fonts')
@include('v2-portal::base-struct.normalize')

<link rel="stylesheet" href="{{ asset('assets/css/portal/portal-base.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/portal/portal-nav.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/portal/portal-a11y.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/portal/portal-components.css') }}">

{{-- Injeção Global do Admin --}}
@include('v2-portal::metadata.metaheader')
```

O `theme-color`, os tokens e os quatro CSS ficam no `<head>`, mas `portal-a11y.css`/`public/assets/css/portal/portal-components.css` são carregados **depois** dos `<style>` inline; qualquer regra inline do tema perde para essas folhas na mesma especificidade. [C]

```blade
{{-- V2/layouts/portal/base.blade.php:117-123 — jQuery precisa estar parseado antes dos plugins SLE --}}
{{-- jQuery 3.6 (SEMPRE síncrono: plugins SLE como nice-select, meanmenu, counterup, waypoints, magnific, viewport DEPENDEM de jQuery no momento do parse) --}}
<script src="{{ asset('assets/vendor/jquery/jquery-3.6.0.min.js') }}"></script>
{{-- Bootstrap 5 JS Bundle (não requer jQuery, pode carregar com defer) --}}
<script defer src="{{ asset('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') }}"></script>

{{-- Portal Init — JavaScript global consolidado (defer) --}}
<script defer src="{{ asset('assets/js/portal-init.js') }}"></script>
```

O comentário é o contrato: remover o jQuery síncrono ou movê-lo para `defer` quebra plugins do editor estrutural. [C]

```blade
{{-- V2/layouts/portal/partials/portal-content.blade.php:13-29 — precedência SLE × fallback gov --}}
@if($useSleBlocks && $sleBlockRenderer->hasBlocks('header'))
    @foreach($sleBlockRenderer->getBlocks('header') as $sleBlock)
        <div class="sle-block" data-area="header">
            {!! $sleBlockRenderer->renderBlock($sleBlock->content) !!}
        </div>
    @endforeach
@else
    @if($showHeader ?? true)
        @hasSection('custom-header')
            @yield('custom-header')
        @else
            @include('v2-portal::header.header')
        @endif
    @endif
    @include('v2-portal::base-struct.global-banner')
@endif
```

Quando existe bloco SLE ativo na área `header`, o header padrão **e** o banner global desaparecem — o banner só existe no ramo de fallback. [C]

```blade
{{-- V2/layouts/portal/base-struct/colors.blade.php:1-18 — tokens de marca e superfícies --}}
@php
    $_hex = $portalTheme['primary_color'] ?? '#1075ad';
    $_rgb = $portalTheme['primary_rgb'] ?? (function($h) {
        $h = ltrim($h, '#');
        return hexdec(substr($h,0,2)) . ',' . hexdec(substr($h,2,2)) . ',' . hexdec(substr($h,4,2));
    })($_hex);
@endphp
    :root {
        /* Grupo A: Marca */
        --tenant-primary: {{ $_hex }};
        --tenant-secondary: {{ $portalTheme['secondary_color'] ?? '#0a4c8c' }};
        --tenant-accent: {{ $portalTheme['accent_color'] ?? '#fece50' }};
        --tenant-links: {{ $portalTheme['link_color'] ?? '#1075ad' }};

        /* Grupo B: Superfícies */
        --color-bg-main: {{ $portalTheme['bg_main'] ?? '#ffffff' }};
        --color-bg-canvas: {{ $portalTheme['bg_canvas'] ?? '#f8f9fa' }};
```

`--bs-primary` e `--bs-primary-rgb` são sobrescritos dentro do mesmo `:root` (`V2/layouts/portal/base-struct/colors.blade.php:29-31`), então `.btn-primary`, `.text-primary`, `.bg-primary` seguem o tema sem recompilar Bootstrap. [C]

```blade
{{-- V2/layouts/portal/metadata/seo.blade.php:1-21 — origem de title/OG e fallback da imagem --}}
<title>{{ $seoTitle ?? ($pageTitle ?? '') . (isset($pageTitle) ? " | " : "") . $siteTitle }}</title>
<meta name="title" content="{{ $seoTitle ?? ($pageTitle ?? $siteTitle) }}">
<meta name="description" content="{{ $seoDescription ?? ($pageDescription ?? '') }}">
<meta name="keywords" content="{{ $seoKeywords ?? '' }}">

{{-- Open Graph / Facebook --}}
<meta property="og:type" content="website">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:title" content="{{ $seoTitle ?? ($pageTitle ?? $siteTitle) }}">
<meta property="og:description" content="{{ $seoDescription ?? ($pageDescription ?? '') }}">
@php
    $rawImage = isset($seo) ? $seo->getCoverImage() : ($coverImage ?? null);
    $finalCoverImage = '';
    if ($rawImage) {
        $finalCoverImage = (strpos($rawImage, 'http') === 0) ? $rawImage : url($rawImage);
    }
```

Como `$seo` é sempre injetado pelo composer nessa view (`V2/Services/SEO/Providers/SEOServiceProvider.php:23-56`), o parâmetro `coverImage` passado pela página nunca é consultado: a imagem vem de `SEOService::getCoverImage()` (capa da entidade ou `logo_header`). [C]

```blade
{{-- V2/layouts/portal/metadata.blade.php:1-10 — composição do bloco de metadados --}}
@include('v2-portal::metadata.gtag', ['gtag' => $gtag ?? []])
@include('v2-portal::metadata.seo', [
    'pageTitle' => $pageTitle ?? null,
    'pageDescription' => $pageDescription ?? null,
    'coverImage' => $coverImage ?? null,
    'schemas' => $schemas ?? []
])

{{-- Injeção de JSON-LD estruturado --}}
@stack('json-ld')
```

`gtag` é repassado, mas o parcial de GA só lê `$seoGtagId` e `$seo->getGtag()` (`V2/layouts/portal/metadata/gtag.blade.php:1-20`); o array é morto. `@stack('json-ld')` também nunca recebe push. [C]

```php
<?php
// V2/Modules/Support/SupportConfigs/ModuleProvider.php:20-66 — fonte real de portalTheme/portalConfigs
\Illuminate\Support\Facades\View::composer('v2-portal::*', function ($view) {
    static $portalTheme = null;
    static $portalConfigs = null;

    if ($portalTheme === null) {
        try {
            $allConfigs = DB::table('v2_portal_configs')
                ->whereNull('deleted_at')
                ->get()
                ->pluck('config_value', 'config_key');

            $portalTheme = [
                'primary_color'    => $allConfigs['theme_primary_color'] ?? '#1075ad',
                'secondary_color'  => $allConfigs['theme_secondary_color'] ?? '#0a4c8c',
                // ...
            ];
            $portalConfigs = $allConfigs->toArray();
        } catch (\Exception $e) {
            // defaults idênticos + $portalConfigs = []
        }
    }
    $view->with('portalTheme', $portalTheme);
    $view->with('portalConfigs', $portalConfigs);
});
```

O trecho omite as demais chaves de `$portalTheme` e o `Log::debug` do `catch`. Os valores chegam como **string** do banco; as flags são avaliadas com `!empty()` (`'0'` é falso, `'1'` é verdadeiro) em `V2/layouts/portal/base.blade.php:101,110,142` e `V2/layouts/portal/partials/portal-content.blade.php:7`. [C]

### 2.4 Origem das variáveis compartilhadas

Três composers incidem em `v2-portal::*`, nesta ordem efetiva de registro: `SEOServiceProvider` (config/app.php:171) → composer do `V2ServiceProvider` (`V2/Core/Providers/V2ServiceProvider.php:48-73`) → composers instanciados no loop de módulos (`V2/Core/Providers/V2ServiceProvider.php:106-120`), incluindo `LayoutBaseComposer` (`MOD-SUPPORT-STRUCTURALLANDINGEDITOR`) e o composer de `SupportConfigs`.

| Variável | Quem injeta | Fonte | Fallback |
|---|---|---|---|
| `siteTitle` | core e SEO (o core roda depois e vence) | `v2_portal_configs.titulo_site` | `config('app.name', 'Portal Oficial')` (`V2/Core/Providers/V2ServiceProvider.php:59-69`) |
| `portalConfigs` | core (`[]`), sobrescrito por SupportConfigs | `v2_portal_configs` (sem defaults) | `[]` se a tabela falhar (`SupportConfigs/ModuleProvider.php:20-66`) |
| `portalTheme` | SupportConfigs | chaves `theme_*` | dez defaults literais (`SupportConfigs/ModuleProvider.php:30-40`) |
| `layoutConfigs` | `LayoutBaseComposer` | `v2_layout_base_configs` keyBy(`key`) | `collect()` (`ViewComposers/LayoutBaseComposer.php:10-22`) |
| `logos`, `path` | core | sempre vazios | `[]` / `''` (`V2/Core/Providers/V2ServiceProvider.php:71-72`) — locais `$logos` dos parciais sombreiam |
| `faviconUrl` | core | `config('v2.portal.favicon_url')` | literal `/V2/assets/images/favicon.png` — nunca usado: `V2/layouts/portal/base.blade.php:18` recalcula |
| `seo`, `seoTitle`, `seoDescription`, `seoKeywords`, `seoGtagId`, `seoGlobalTokens`, `seoHeaderScripts`, `seoFooterScripts` | SEO | `SEOService` + `v2_portal_configs` | `null`/`''` (`V2/Services/SEO/Providers/SEOServiceProvider.php:41-55`) |

### 2.5 Tema e tokens

| Token | Origem | Default | Consumidores |
|---|---|---|---|
| `--tenant-primary` | `theme_primary_color` | `#1075ad` | nav, botões, paginação, links, rodapé |
| `--tenant-secondary` | `theme_secondary_color` | `#0a4c8c` | hover da logo fallback (`V2/layouts/portal/header/header-logo.blade.php:49`) |
| `--tenant-accent` | `theme_accent_color` | `#fece50` | foco, hover da barra, destaque (`V2/layouts/portal/base-struct/colors.blade.php:59-61`) |
| `--tenant-links` | `theme_link_color` | `#1075ad` | `a` global (`V2/layouts/portal/base-struct/colors.blade.php:39-46`) |
| `--color-bg-main` | `theme_bg_main` | `#ffffff` | body, cards, dropdown (`V2/layouts/portal/base-struct/colors.blade.php:17,35`) |
| `--color-bg-canvas` | `theme_bg_canvas` | `#f8f9fa` | hover, faixas, banner |
| `--color-text-main/-muted`, `--color-success/warning/danger/info`, `--color-a11y-highlight` | fixos no código | — | garantia de contraste, independente do tema (`V2/layouts/portal/base-struct/colors.blade.php:21-27`) |
| `--bs-primary`, `--bs-primary-rgb` | derivados de `theme_primary_color` | `#1075ad` / `16,117,173` | classes utilitárias Bootstrap |
| `--font-sans` | `theme_font_family` | stack Nunito Sans | body (`V2/layouts/portal/base-struct/fonts.blade.php:3-8`) |
| `--font-size-base` | `theme_font_size_base` + `px` | `16px` | `html { font-size }` (`V2/layouts/portal/base-struct/fonts.blade.php:4,8`) |
| `--theme-radius` | `theme_border_radius` | `4px` | `.btn/.card/.form-control/.alert/...` com `!important` (`V2/layouts/portal/base-struct/normalize.blade.php:4,22-32`) |
| `--theme-shadow` | `theme_shadow_intensity` | `none` | `.card/.dropdown-menu/.modal-content/.shadow-sm` com `!important` (`V2/layouts/portal/base-struct/normalize.blade.php:5,36-41`) |

A escala tipográfica é fixa em `rem` (h1 2.5rem … h6 1rem, display 3.5–5rem) e `h1..h6` recebem `font-weight: 800` (`V2/layouts/portal/base-struct/fonts.blade.php:19-37`). `V2/layouts/portal/base-struct/normalize.blade.php:13-17` força `body { background-color: var(--color-bg-main) }` e `a` é reescrito com `filter: brightness(90%)` no hover (`V2/layouts/portal/base-struct/colors.blade.php:39-46`). [C]

### 2.6 Metadata e SEO — origem de cada tag

| Tag | Origem | Fallback quando não há config/DB |
|---|---|---|
| `<title>` | `$seoTitle` (`SEOService::getTitle()` ← `setTitle()`/`bindModel()` no controller) | Se `$seoTitle` é nulo: `$pageTitle . ' \| ' . $siteTitle`; sem `pageTitle`: `$siteTitle` (`V2/layouts/portal/metadata/seo.blade.php:2`) |
| `meta[name=title]` | idem | idem, sem o separador ` \| ` (`V2/layouts/portal/metadata/seo.blade.php:3`) |
| `meta[name=description]` | `$seoDescription` (`setDescription`/`metadata.seo.description`) | `$pageDescription`; senão `''` |
| `meta[name=keywords]` | `$seoKeywords` (`setKeywords`) | `''` |
| `og:type` | literal `website` | — |
| `og:url`, `twitter:url` | `url()->current()` | — |
| `og:title`, `twitter:title` | `$seoTitle ?? $pageTitle ?? $siteTitle` — sem o sufixo do site que o `<title>` acrescenta | — |
| `og:description`, `twitter:description` | mesmos fallbacks de `seoDescription`/`pageDescription` | — |
| `og:image`, `twitter:image` | `SEOService::getCoverImage()`: capa definida por `setCoverImage()`; senão `logo_header` de `v2_layout_base_configs`, devolvida como `/storage/<path>` (`V2/Services/SEO/Services/SEOService.php:375-397`) | `''` (tag presente e vazia) |
| `link[rel=canonical]` | `getCanonical()` | **sempre** `url()->current()`, pois nenhum `setCanonical()` é chamado (`V2/layouts/portal/metadata/seo.blade.php:31`) |
| tokens de verificação | `global_meta_tokens` cru | `''` |
| JSON-LD | `getSchemas()` (do `metadata.seo.json_ld` ou `addSchema()`) + `$schemas` do `@extends` | nada é impresso |
| gtag | `google_analytics_id` | script não é impresso |
| scripts de header/footer | `header_scripts` / `footer_scripts` crus | `''` |
| `theme-color` | `$portalTheme['primary_color']` | `#1075ad` (`V2/layouts/portal/base.blade.php:10` — não usa `--tenant-primary`) |
| favicon/apple-touch | `layoutConfigs['logos']->payload['favicon']` | `url('favicon.ico')` |

Quando `v2_portal_configs` não existe, `PortalConfigsService::getAll()` devolve apenas os defaults (`V2/Modules/Support/SupportConfigs/Services/PortalConfigsService.php:37-52,199-222`) e o `SupportConfigs` devolve `portalConfigs=[]`; nesse estado: `seoTitle/Description/Keywords` ficam nulos, GA e scripts não são impressos, `siteTitle` cai em `config('app.name')`, e **as três feature flags ficam desligadas** (sem barra, sem widget, sem VLibras). [C]

### 2.7 Header, navegação e busca

- Estrutura: `<header role="banner">` → `.navbar` → logo + `#offcanvasNavbar` (nav) à esquerda; busca + `.navbar-toggler` à direita (`V2/layouts/portal/header/header.blade.php:1-36`). [C]
- Desktop: CSS transforma o offcanvas em elemento estático (`min-width: 992px`) e esconde `.offcanvas-backdrop` (`V2/layouts/portal/header/header.blade.php:39-62`). [C]
- Menu: item fixo “Início” com `aria-current="page"` quando `Request::is('/')`; depois `links_header` em até 3 níveis (`V2/layouts/portal/header/header-nav.blade.php:8-52`). Nível 0 sem `children` vira link simples; com `children`, vira `role="button"` + `aria-haspopup` + `aria-expanded="false"` e `href` continua preenchido (o JS faz `preventDefault`). [C]
- Interação: `public/assets/js/portal-init.js:88-160` fecha todos (`.is-open`, `aria-expanded=false`) e reabre um; `Escape` fecha, clique fora fecha, scroll em `<992px` fecha. Os atributos `data-dropdown`/`data-submenu` do HTML não são lidos pelo JS. [C]
- Busca: elemento `<search role="search">` com botão `.js-toggle-search` (`aria-expanded`, `aria-controls="search-dropdown"`) e `<form action="{{ url('busca') }}" method="GET">` com `name="q"`, `id="portal-search"`, `required` (`V2/layouts/portal/header/header-search-area.blade.php:5-38`). [C]
- O dropdown nasce com a classe `d-none` (`display:none !important` do Bootstrap) e o JS tenta abrir com `style.display='block'` sem `!important` (`public/assets/js/portal-init.js:171-181`; `public/assets/css/portal/portal-components.css:60-70`) — na prática permanece oculto. Ver §3.3. [C]
- Atalho: `a[href="#portal-search"]` da barra de acessibilidade é interceptado para abrir a busca e focar o input (`public/assets/js/portal-init.js:223-233`). [C]

### 2.8 Rodapé, transparência e LAI

- `<footer id="footer" role="contentinfo">` com borda superior na cor primária (`V2/layouts/portal/footer/footer.blade.php:1`). [C]
- Linha de logos: `logo_principal` (180×60), `sefaz` e `governo` (135×45), `membro_1`/`membro_2` (120×40); todos com `filter: brightness(0) invert(1)` e `loading="lazy"` (`V2/layouts/portal/footer/footer-logo.blade.php:20-40`). [C]
- Colunas de links: `links_footer` → `[{title, children:[{title,url}]}]`, renderizadas em `.footer-grid` (`V2/layouts/portal/footer/footer-services-area.blade.php:1-25`). [C]
- Contato: `endereco` (com `nl2br`), `map_link` (target `_blank` + aviso “abre em nova aba”), `telefone_1`, `telefone_2`, `email` (`mailto:`), `horario`; `zipcode` e `po_box` são salvos mas não renderizados (`V2/Modules/Support/StructuralLandingEditor/Controllers/LayoutBaseCmsController.php:185-198`; `V2/layouts/portal/footer/footer-public-contact.blade.php:9-48`). [C]
- Redes: `social_media` ordenado por `order`, só itens `active===true` e `url` não vazia; ícone FontAwesome convertido para Bootstrap Icons por mapa fixo (`V2/layouts/portal/footer/footer-social-network.blade.php:1-45`). [C]
- Copyright: ano corrente + `siteTitle` (`V2/layouts/portal/footer/footer-copyright.blade.php:3`). [C]
- Badge do CMS: `v2_version(true)` + link para a Máxima (`V2/layouts/portal/footer/footer-cms-metadata.blade.php:3-6`). [C]
- LAI: `lai_items` ordenado por `order` e filtrado por `active`; se vazio, usa quatro itens fixos (Receitas, Despesas, Licitações, e-SIC/Ouvidoria) (`V2/layouts/portal/footer/footer-transparencia-lai.blade.php:8-38`). O toggle `lai_enabled` é lido como `(bool) $payload` (`V2/layouts/portal/footer/footer.blade.php:25-30`; `V2/layouts/portal/footer/footer-transparencia-lai.blade.php:12-13`) — payload real é `['enabled' => bool]` (`V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:269-271`), então o cast de array é sempre `true`. Ver §3.3. [C]

### 2.9 Banner global

- Renderizado só no fallback gov, logo após o header (`V2/layouts/portal/partials/portal-content.blade.php:28`). [C]
- Exige `global_banner.banner_image`; aceita string ou `{path}` e converte via `v2_storage_url()` (`V2/layouts/portal/base-struct/global-banner.blade.php:1-25`). [C]
- Janela de exibição: `Carbon::parse(start_date)` ≤ agora ≤ `Carbon::parse(end_date)`; ambos opcionais (`V2/layouts/portal/base-struct/global-banner.blade.php:10-18`). [C]
- Link destino padrão `#`; `alt` usa `banner_image_alt`, depois `alt_text`, depois `Comunicado Global` (`:20-23`). [C]
- Fechar não persiste nada: apenas anima opacidade/altura e `display:none` (`public/assets/js/portal-init.js:239-251`); recarregar traz o banner de volta. [C]
- `<aside>` com `aria-labelledby="banner-comm-title"`, `<h2 class="visually-hidden">Comunicado Importante da Administração</h2>`, imagem `loading="lazy"` e botão “Fechar comunicado” (`V2/layouts/portal/base-struct/global-banner.blade.php:34-70`). [C]

### 2.10 Impressão (APE)

- `#print-engine` é `d-none d-print-block`; em `@media print`, `body > *:not(#print-engine)` é ocultado (`V2/layouts/portal/print/print-only.blade.php:13,54-58`). [C]
- Estrutura `<table class="print-layout">`: `<thead>` repete header, `<tbody>` recebe o clone de `#main-content`, `<tfoot>` reserva o rodapé (`V2/layouts/portal/print/print-only.blade.php:15-46`). [C]
- O clone é feito em `beforeprint` e pelo botão `.js-btn-print`; `afterprint` limpa `#print-canvas` (`public/assets/js/portal-init.js:15-27,263-271`). [C]
- `@page { margin: 1.8cm 1.5cm 1.5cm; size: A4 }`, fonte 11pt, links sublinhados, `page-break-inside: avoid` para tabelas/figuras/pre/quotes e `page-break-after: avoid` em headings (`V2/layouts/portal/print/print-only.blade.php:60-123`). [C]
- Header de impressão usa `logo_header` (alt “Logomarca {siteTitle}”) ou letra “G” (`V2/layouts/portal/print/print-header.blade.php:20-31`). Rodapé mostra `cms_brand_name` (inexistente em `portalTheme`, logo “PORTAL”), URL atual, timestamp e QR SVG de 55px; em falha, o texto “QR” (`V2/layouts/portal/print/print-footer.blade.php:9-35`). [C]

### 2.11 Estados de erro

`App\Exceptions\Handler::renderHttpException()` escolhe `v2-portal::errors.<status>` quando o path não começa por `/cms` e a view existe; caso contrário delega ao framework (`sucesu-cms/app/Exceptions/Handler.php:31-49`). [C]

| Status | View | Flags | Mensagem principal |
|---|---|---|---|
| 403 | `errors/403` | header/footer sim; main-header/asides não | “Acesso Negado.” / “Você não tem permissão para acessar este recurso ou página.” |
| 404 | `errors/404` | header/footer sim; main-header, left e right sim | “Desculpe, a página que você está procurando não existe ou foi movida.” |
| 419 | `errors/419` | header/footer sim | “Sua sessão expirou por inatividade.” |
| 500 | `errors/500` | header/footer sim | “Ocorreu um erro inesperado em nosso servidor.” |
| 503 | `errors/503` | header/footer sim | “Portal em Manutenção.” |

Todas usam `@component('v2-portal::main-section.main-content')` e terminam em “Voltar ao Início”/“Fazer Login” (403), “Voltar para o Início”/“Falar com a Ouvidoria” (404), “Atualizar Página” (419), “Tentar Novamente”/“Relatar Falha” (500), “Verificar Disponibilidade” (503). [C] O 419 raramente é alcançado: `TokenMismatchException` é convertido em `redirect()->back()` com flash “Sessão terminada! Por favor realize o login novamente.” antes do render da view (`app/Exceptions/Handler.php:67-74`). [C]

## 3. Particularidades

### 3.1 Invariantes

- `#main-content` deve existir exatamente uma vez por página — é alvo dos skip links (`V2/layouts/portal/base-struct/accessibility-skips.blade.php:4`), do botão “Voltar ao topo” (`public/assets/js/portal-init.js:43-47`) e da clonagem de impressão (`public/assets/js/portal-init.js:15-22`). [C]
- O namespace `v2-portal` é registrado por `loadViewsFrom(base_path('V2/layouts/portal'), 'v2-portal')` (`V2/Core/Providers/V2ServiceProvider.php:40-42`); mover a pasta quebra todas as views. [C]
- `layoutConfigs` precisa ser uma **Collection** (parciais chamam `->has()`); o core injeta `[]` e só o `LayoutBaseComposer` substitui por `collect()`. Sem o módulo SLE, o primeiro parcial que chama `$layoutConfigs->has('logos')` gera fatal (`V2/layouts/portal/header/header-logo.blade.php:5`). [C]
- `siteTitle` tem duas definições concorrentes; a do core vence porque é registrada depois (`V2/Core/Providers/V2ServiceProvider.php:48-69` × `V2/Services/SEO/Providers/SEOServiceProvider.php:23-56`; ordem em `config/app.php:171,185`). [C]
- O tema aplica `!important` em `body`, `.btn`, `.card`, `.shadow-sm` (`V2/layouts/portal/base-struct/colors.blade.php:34-37`; `V2/layouts/portal/base-struct/normalize.blade.php:22-41`), então CSS de módulo sem `!important` não sobrepõe cor de fundo, raio ou sombra. [C]
- Os únicos pontos de extensão de head/body são os stacks `head-styles`, `styles`, `scripts`, `footer-scripts`; `json-ld` existe mas não é alimentado. [C]

### 3.2 Armadilhas de reimplementação

1. **`coverImage` e `gtag` do `@extends` não têm efeito.** `V2/layouts/portal/metadata/seo.blade.php:13` sempre prefere `$seo`, e `V2/layouts/portal/metadata/gtag.blade.php:15` lê `$seo->getGtag()`. Para definir capa use `SEOService::setCoverImage()`; para OG extra não há API pública. [C]
2. **GA é bloqueado pela CSP.** `google_analytics_id` imprime `https://www.googletagmanager.com/gtag/js`, mas a política só permite `www.vlibras.gov.br` e `cdn.jsdelivr.net` (`V2/Core/Middleware/CspMiddleware.php:27-30,66`). O script é baixado? Não: `script-src` o bloqueia. [C]
3. **Busca do header não abre** por causa de `d-none` + `!important` (§2.7). Reimplementar trocando classe (`classList.toggle('d-none')`) ou usando `hidden`. [C]
4. **LAI não desliga.** `(bool) ['enabled' => false]` é `true` (`V2/layouts/portal/footer/footer.blade.php:28`; `V2/layouts/portal/footer/footer-transparencia-lai.blade.php:13`). O cast correto é `$payload['enabled'] ?? true`. [C]
5. **`theme_primary_color` de 3 dígitos quebra `--bs-primary-rgb`.** O closure faz `substr($h,0,2)`, `substr($h,2,2)`, `substr($h,4,2)` (`V2/layouts/portal/base-struct/colors.blade.php:4-7`): `#fff` gera `255,15,0` (substr de 2 em 2 sobre 3 dígitos). [C]
6. **`siteTitle` vazio no banco.** `titulo_site` default é `''` (`V2/Modules/Support/SupportConfigs/Services/PortalConfigsService.php:212`); o core usa `!empty()`, então cai em `config('app.name')`, mas o header fallback mostra o `<span>` com string vazia se `app.name` também for vazio. [I]
7. **Erros duplicam `<main id="main-content">`.** `portal-content` já emite `<main id="main-content">` no grid (`V2/layouts/portal/partials/portal-content.blade.php:71`) e as views de erro incluem `main-content`, que emite outro `<main>` com o mesmo id (`V2/layouts/portal/main-section/main-content.blade.php:5`). Isso viola HTML e a11y (landmark duplicado, skip link ambíguo). [C]
8. **`sle_component()` recebe `siteTitle='Portal'`** porque usa `config('v2_portal_configs.titulo_site', 'Portal')`, namespace de config que não existe (`V2/Modules/Support/StructuralLandingEditor/Services/ComponentResolver.php:47-50`; `V2/Modules/Support/StructuralLandingEditor/Services/BlockRenderer.php:94`). Headers/footers SLE montados com `sle_component` exibem “Portal”, não o título real. [C]
9. **`$seoTitle` suprime o `pageTitle`.** Quando o controller define título, o `<title>` contém **apenas** esse valor (sem “ | site”); quando não define, contém `pageTitle | siteTitle`. Páginas que só passam `pageTitle` em um portal com `titulo_site` preenchido perdem o `pageTitle` (ex.: busca global, `SearchController` não chama `setTitle()`). [C]
10. **Banner não respeita fuso/limites de data inválidos.** `Carbon::parse()` lança exceção para string inválida e derruba a página (não há try/catch) (`V2/layouts/portal/base-struct/global-banner.blade.php:10-11`). [C]

### 3.3 Divergências e dívidas

| Item | Evidência | Efeito |
|---|---|---|
| `config('v2.portal.favicon_url')` é lido mas não existe em `config/`; o valor é sobrescrito no próprio `base` | `V2/Core/Providers/V2ServiceProvider.php:50`; `V2/layouts/portal/base.blade.php:18-33` | favicon só vem de `logos.favicon` ou `favicon.ico` |
| `showAccessibilityBar` passado por 12 páginas | `MOD-LAYOUTS-BASICPAGE/Views/portal/acessibilidade.blade.php:5` | ignorado; quem manda é `accessibility_bar_enabled` |
| `stickyAccessibilityBar` existe no CMS e em duas páginas | `V2/Modules/Layouts/BasicPage/Views/portal/acessibilidade.blade.php:6`; `SobreNos/.../index.blade.php:14` | funciona apenas porque a variável chega ao include via escopo do `app-layout`; não é repassada explicitamente em `V2/layouts/portal/partials/portal-content.blade.php:8-10` |
| `json-ld` stack | `V2/layouts/portal/metadata.blade.php:10` | nunca alimentado; JSON-LD real vem de `$seoSchemas`/`$schemas` |
| `footer_scripts` cru antes dos stacks | `V2/layouts/portal/metadata/metafooter.blade.php:1` | script do admin roda antes do `public/assets/js/portal-init.js`? Não: `portal-init` já executou por ser `defer`; ordem relativa depende do parser |
| `metaheader` cru no `<head>` | `V2/layouts/portal/base.blade.php:68` | HTML/JS do admin injetado sem escape nem nonce próprio (o CSP adiciona nonce a `<script>` inline) |
| `print-footer` `cms_brand_name` | `V2/layouts/portal/print/print-footer.blade.php:10` | chave não existe em `portalTheme` → sempre “PORTAL” |
| `main-footer` “Atualizado em” | `V2/layouts/portal/main-section/main-footer.blade.php:16-18` | usa `date('d/m/Y')` (hoje), não a data real de atualização da entidade |
| `getCoverImage()` devolve `/storage/...` direto | `V2/Services/SEO/Services/SEOService.php:383-393` | ignora `v2_storage_url()`/CDN e pode divergir do resto do portal |
| GA ausente da CSP | `V2/Core/Middleware/CspMiddleware.php:27-30` | analytics silenciosamente bloqueado em produção |
| `header-search` `d-none` | `V2/layouts/portal/header/header-search-area.blade.php:19-20`; `public/assets/js/portal-init.js:171-181` | dropdown não abre |
| LAI cast de array | `V2/layouts/portal/footer/footer.blade.php:25-30`; `V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:269-271` | toggle não desliga |
| `layoutConfigs` array × Collection | `V2/Core/Providers/V2ServiceProvider.php:71`; `V2/layouts/portal/header/header-logo.blade.php:5` | dependência dura do módulo SLE |

### 3.4 Código morto ou duplicado dentro do escopo

- `V2/layouts/portal/header/accessibility/scripts.blade.php` (271 linhas) replica o bloco 8 de `public/assets/js/portal-init.js` e **não é incluído por ninguém**; o cabeçalho do próprio `base` diz “JS consolidado em public/assets/js/portal-init.js” (`V2/layouts/portal/base.blade.php:141-143`). Manter apenas `public/assets/js/portal-init.js`. [C]
- `V2/layouts/portal/metadata/metaheader.blade.php` tem 1 linha e só imprime `$seoHeaderScripts`. [C]
- `$path`, `logos` e `faviconUrl` compartilhados pelo core não são lidos diretamente nos parciais deste layout; os parciais definem variáveis locais de mesmo nome (`V2/layouts/portal/base.blade.php:18-24`; `V2/layouts/portal/header/header-logo.blade.php:2-25`). [C]
- `.hidden` usado em `#accessibility-placeholder` não está definido em nenhum CSS do portal; o elemento é um `div` vazio de altura zero. [C]
- `@stack('footer-scripts')` não tem produtor em `V2/`. [C]

## 4. Integrabilidade

### 4.1 Contrato de página mínima

| Item | Obrigatório | Tipo | Efeito |
|---|---|---|---|
| `@extends('v2-portal::app-layout', [...])` | sim | layout | herda head, chrome, scripts |
| `@section('main-section-area')` | sim | seção | conteúdo dentro de `<main id="main-content">` |
| `pageTitle` | recomendado | string | `<title>`/OG quando não há `seoTitle` |
| `title` | recomendado | string | `<h1>` + breadcrumb (`main-header`) |
| `pageDescription` | opcional | string | description/OG e parágrafo do header |
| `showMainLeftArea`/`showMainRightArea` | opcional | bool | asides fixos de navegação/links úteis |
| `fullWidthMain` | opcional | bool | página controla o próprio container |
| `@push('head-styles')`/`@push('scripts')` | opcional | CSS/JS | incluídos no head/fim do body |

```blade
{{-- receita mínima: qualquer módulo do portal --}}
@extends('v2-portal::app-layout', [
    'pageTitle'       => 'Minha Página',
    'title'           => 'Minha Página',
    'pageDescription' => 'Resumo exibido no header e nas metatags.',
    'showMainLeftArea' => false,
    'showMainRightArea'=> false,
])

@section('main-section-area')
    <h1 class="h2 fw-800">Minha Página</h1>
    <p>Conteúdo…</p>
@endsection

@push('head-styles')
<style>.minha-classe { color: var(--tenant-primary); }</style>
@endpush

@push('scripts')
<script defer src="{{ asset('assets/js/meu-modulo.js') }}"></script>
@endpush
```

Resultado esperado: `<title>Minha Página | <siteTitle></title>` apenas se `seoTitle` for nulo; caso o controller tenha chamado `$this->seo->setTitle(...)`, `<title>` será exatamente esse valor.

### 4.2 Receitas de uso

1. **Página de erro customizada**: estenda `app-layout`, defina as flags de chrome e use o componente `main-content` com slot (padrão das 5 views). [C]
2. **Página controlada (hero próprio)**: passe `fullWidthMain => true`, `showHeader => false`, `showFooter => false` e desenhe hero/grid no `main-section-area` (padrão de `MOD-PUBLICACOES-NOTICIAS`). [C]
3. **Definir SEO por entidade**: no controller, `$this->seo->setTitle(...)`, `setDescription(...)`, `setCoverImage(...)`, `addSchema([...])` antes de retornar a view; use `bindModel($entity)` para ler `metadata['seo']` (`V2/Services/SEO/Services/SEOService.php:29-56,293-369`). [C]
4. **Trocar logo/favicon**: gravar payloads em `v2_layout_base_configs` chave `logos` (`logo_header`, `logo_header_contraste`, `favicon`, `logo_principal`, …); não há config de arquivo para isso. [C]

### 4.3 Anti-uso

- **`@extends('v2-portal::base')` direto sem `@section('layout-content')`**: o body fica vazio (nada inclui header/conteúdo) e não há `#main-content`; use `app-layout`. [C]
- **Contar com `coverImage`/`gtag` no array de `@extends`**: não produzem tag alguma (§3.2). [C]
- **Sobrescrever `#main-content` ou renderizar `main-content` dentro do grid**: ids duplicados e dois landmarks `main` (§3.2 item 7). [C]
- **Obter dados de contato por config de arquivo**: o rodapé só lê `v2_layout_base_configs` (`V2/layouts/portal/footer/footer-public-contact.blade.php:1-5`). [C]

### 4.4 Consumidores e risco

| Consumidor | Dependência | Risco se o layout mudar |
|---|---|---|
| 35 blades `@extends('v2-portal::app-layout')` | flags/seções acima | quebra total da página |
| `SVC-SEARCH` (`SearchController`) | `search/index` + rota `v2.portal.home` | 500 se o módulo Homepage estiver desativado (`route()` inexistente) |
| 5 views de erro | `main-content` + `layoutConfigs` | erro dentro do erro |
| SLE (`sle_component`) | 11 parciais mapeadas em `V2/Modules/Support/StructuralLandingEditor/Services/ComponentResolver.php:17-31` | HTML sem estilo/tema correto |

## 5. Catálogo de arquivos

| Arquivo | Papel | Variáveis principais | Observação |
|---|---|---|---|
| `V2/layouts/portal/app-layout.blade.php` | casca de página | `pageTitle/pageDescription/coverImage/gtag/schemas` | ponto único de `@extends('v2-portal::base')`; injeta `masks.js` |
| `V2/layouts/portal/base.blade.php` | documento HTML | `$portalTheme`, `$portalConfigs`, `$layoutConfigs`, `siteTitle` | ordem de CSS/JS e flags de feature |
| `V2/layouts/portal/base-struct/colors.blade.php` | tokens | `portalTheme` | sobrescreve `--bs-*`; alto contraste |
| `V2/layouts/portal/base-struct/fonts.blade.php` | tipografia | `portalTheme` | escala rem fixa |
| `V2/layouts/portal/base-struct/normalize.blade.php` | reset/utilitários/a11y CSS | `portalTheme` | `.cms-grid`, OpenDyslexic, guia de leitura |
| `V2/layouts/portal/base-struct/global-banner.blade.php` | comunicado | `layoutConfigs['global_banner']` | janela de datas; sem persistência de fechamento |
| `V2/layouts/portal/metadata.blade.php` | composição SEO | `pageTitle`, `pageDescription`, `schemas` | inclui `gtag` e `seo` |
| `V2/layouts/portal/metadata/seo.blade.php` | metatags | `seo*`, `siteTitle` | canonical = URL atual |
| `V2/layouts/portal/metadata/gtag.blade.php` | GA | `seoGtagId`, `seo` | bloqueado pela CSP |
| `V2/layouts/portal/metadata/metaheader.blade.php` | scripts do admin no head | `seoHeaderScripts` | cru |
| `V2/layouts/portal/metadata/metafooter.blade.php` | scripts do admin + stacks | `seoFooterScripts` | — |
| `V2/layouts/portal/header/header.blade.php` | chrome de navegação | nenhuma própria | offcanvas/desktop via CSS |
| `V2/layouts/portal/header/header-logo.blade.php` | logo | `layoutConfigs['logos']`, `siteTitle` | troca em alto contraste |
| `V2/layouts/portal/header/header-nav.blade.php` | menu | `layoutConfigs['links_header']` | 3 níveis |
| `V2/layouts/portal/header/header-search-area.blade.php` | busca | nenhuma | ação `GET /busca` |
| `V2/layouts/portal/footer/footer.blade.php` | rodapé | `showFooterLai` | orquestra 7 parciais |
| `V2/layouts/portal/footer/footer-logo.blade.php` | logos institucionais | `layoutConfigs['logos']` | filtro branco |
| `V2/layouts/portal/footer/footer-services-area.blade.php` | colunas de links | `layoutConfigs['links_footer']` | reusado no 404 |
| `V2/layouts/portal/footer/footer-public-contact.blade.php` | contato | `layoutConfigs['contato']` | address semântico |
| `V2/layouts/portal/footer/footer-social-network.blade.php` | redes | `layoutConfigs['social_media']` | mapa FA→BI |
| `V2/layouts/portal/footer/footer-copyright.blade.php` | copyright | `siteTitle` | ano corrente |
| `V2/layouts/portal/footer/footer-cms-metadata.blade.php` | versão | — | `v2_version(true)` |
| `V2/layouts/portal/footer/footer-transparencia-lai.blade.php` | LAI | `lai_items`, `lai_enabled` | 4 itens default |
| `V2/layouts/portal/main-section/main-header.blade.php` | breadcrumb + h1 | `title`, `description` | yields `breadcrumbs`, `header-actions` |
| `V2/layouts/portal/main-section/main-left-area.blade.php` | aside esquerdo | — | links fixos |
| `V2/layouts/portal/main-section/main-right-area.blade.php` | aside direito | — | links fixos |
| `V2/layouts/portal/main-section/main-footer.blade.php` | ações da página | — | Voltar/Imprimir + data de hoje |
| `V2/layouts/portal/main-section/main-content.blade.php` | envelope `<main>` | `$slot`, `id`, `class` | usar via `@component` |
| `V2/layouts/portal/partials/portal-content.blade.php` | montagem do chrome | flags de página | precedência SLE |
| `V2/layouts/portal/partials/pagination.blade.php` | paginação | `$paginator`, `$elements` | markup só com `hasPages()` |
| `V2/layouts/portal/print/print-only.blade.php` | motor de impressão | — | oculta todo o body exceto `#print-engine` |
| `V2/layouts/portal/print/print-header.blade.php` | cabeçalho impresso | `siteTitle`, `layoutConfigs` | fallback letra “G” |
| `V2/layouts/portal/print/print-footer.blade.php` | rodapé impresso | `portalTheme` | QR SVG / fallback |
| `V2/layouts/portal/search/index.blade.php` | resultados | `term`, `results` | PostgreSQL FTS |
| `V2/layouts/portal/errors/403.blade.php` | 403 | — | login em `acesso-restrito` |
| `V2/layouts/portal/errors/404.blade.php` | 404 | — | inclui links do rodapé |
| `V2/layouts/portal/errors/419.blade.php` | 419 | — | reload |
| `V2/layouts/portal/errors/500.blade.php` | 500 | — | reload/ouvidoria |
| `V2/layouts/portal/errors/503.blade.php` | 503 | — | verificar disponibilidade |

## 6. Acessibilidade e responsividade

O detalhamento está em `04-acessibilidade.md`. Resumo do que o layout garante: `lang="pt-br"`, viewport sem restrição de zoom, skip links com `accesskey` 1/2/3, foco visível global (`V2/layouts/portal/base-struct/normalize.blade.php:316-322`), barra de acessibilidade e widget opcionais, marcação semântica (`header/banner`, `nav/main-nav`, `main`, `aside`, `footer/contentinfo`), `prefers-reduced-motion` no banner (`V2/layouts/portal/base-struct/global-banner.blade.php:78-85`) e impressão acessível (conteúdo clonado, marca d'água decorativa). Breakpoints usados: `992px` (header offcanvas), `768px` (widget), `576px` (barra/rodapé). [C]

## 7. Assets e build

| Asset | Origem | Observação |
|---|---|---|
| `assets/vendor/bootstrap5/css/bootstrap.min.css` + `js/bootstrap.bundle.min.js` | vendor local | 5.3.3 (`V2/layouts/portal/base.blade.php:44,120`) |
| `assets/vendor/bootstrap-icons/bootstrap-icons.css` | vendor local | 1.11.3 (`V2/layouts/portal/base.blade.php:47`) |
| `assets/vendor/nunito-sans/nunito-sans.css` | vendor local | sem Google Fonts; o `preconnect` é inócuo |
| `assets/css/portal/portal-{base,nav,a11y,components}.css` | projeto | carregados depois dos tokens inline |
| `assets/js/portal-init.js` | projeto | `defer`; consolida print, topo, nav, busca, banner, a11y |
| `assets/js/masks.js` | projeto | `defer` via `@push('scripts')` do `app-layout` |
| `assets/fontes/opendyslexic/*.woff2` | projeto | `@font-face` em `V2/layouts/portal/base-struct/normalize.blade.php:139-153` |
| `manifest.json`, `sw.js`, `favicon.ico` | raiz pública | referenciados sem verificação |

CSP global (`V2/Core/Middleware/CspMiddleware.php:60-75`) adiciona nonce a todo `<script>` inline sem `src` e permite apenas `self`, `www.vlibras.gov.br` e `cdn.jsdelivr.net`; `style-src` e `script-src-attr` usam `'unsafe-inline'`. [C]

## 8. Lacunas

- `LG-UI-PORTAL-01` — Formatos exatos dos payloads de `links_header`/`links_footer` são gerados por builders JS fora desta unidade; os consumidores comprovam `title/url/children` (`V2/layouts/portal/header/header-nav.blade.php:16-50`; `V2/layouts/portal/footer/footer-services-area.blade.php:10-20`) e os builders confirmam 3 níveis (`scripts/links-header.blade.php:217`). Falta ler o JS completo para limites (máx. de itens, validação de URL). [L]
- `LG-UI-PORTAL-02` — Não há teste de browser que prove o comportamento do dropdown de busca; a conclusão de que `d-none !important` o mantém oculto é baseada nas regras de CSS (Bootstrap 5.3.3 + inline sem `!important`). Verificar em `public/assets/vendor/bootstrap5/css/bootstrap.min.css` (classe `.d-none`). [I]
- `LG-UI-PORTAL-03` — Quem popula `$title`/`$description` em `BasicPagePortalController@runtime` não foi localizado dentro desta unidade; a página runtime não passa essas chaves. [L]
- `LG-UI-PORTAL-04` — Comportamento do `qrcode` (`app('qrcode')`) depende do provider do pacote; a auto-descoberta está em `V2/layouts/portal/print/print-footer.blade.php:18-30`, mas a versão do pacote não foi verificada aqui. [L]
- `LG-UI-PORTAL-05` — `REFERENCE/*.html` não possui o chrome deste layout (usa assets próprios); qualquer paridade visual deve ser comparada com o HTML servido, não com a referência. [C]

## 9. Checklist de paridade

- [ ] `PC-UI-PORTAL-01` — `GET /` responde 200 com `lang="pt-br"`, `meta[name=viewport]`, `meta[name=csrf-token]` (`V2/layouts/portal/base.blade.php:2-6`).
- [ ] `PC-UI-PORTAL-02` — Com `titulo_site` preenchido e controller sem `setTitle`, `<title>` contém `pageTitle | titulo_site`; com `setTitle`, contém exatamente o título definido (`V2/layouts/portal/metadata/seo.blade.php:2`; `V2/Services/SEO/Providers/SEOServiceProvider.php:47`).
- [ ] `PC-UI-PORTAL-03` — Alterar `theme_primary_color` no banco muda `--tenant-primary` e a cor de `.btn-primary` sem deploy (`V2/layouts/portal/base-struct/colors.blade.php:11,30-31`).
- [ ] `PC-UI-PORTAL-04` — `links_header` com 3 níveis renderiza `.lotep-dropdown`/`.lotep-submenu`, `aria-expanded=false` e abre por clique (`V2/layouts/portal/header/header-nav.blade.php:18-50`; `public/assets/js/portal-init.js:104-141`).
- [ ] `PC-UI-PORTAL-05` — Rodapé exibe `logo_principal`, colunas de `links_footer` e redes ativas ordenadas (`V2/layouts/portal/footer/footer-logo.blade.php:20-21`; `V2/layouts/portal/footer/footer-social-network.blade.php:36-42`).
- [ ] `PC-UI-PORTAL-06` — Banner fora da janela `start_date..end_date` não é renderizado (`V2/layouts/portal/base-struct/global-banner.blade.php:14-20`).
- [ ] `PC-UI-PORTAL-07` — `beforeprint` copia `#main-content` para `#print-canvas` e o CSS de impressão oculta todo o resto (`public/assets/js/portal-init.js:15-22`; `V2/layouts/portal/print/print-only.blade.php:54-58`).
- [ ] `PC-UI-PORTAL-08` — `/pagina-inexistente` renderiza `errors/404` com header/footer e mensagem exata (`app/Exceptions/Handler.php:40-48`; `V2/layouts/portal/errors/404.blade.php:19`).
- [ ] `PC-UI-PORTAL-09` — Com `v2_portal_configs` vazia, as flags desligam barra/widget/VLibras e `siteTitle` cai em `config('app.name')` (`SupportConfigs/ModuleProvider.php:19-66`; `V2/layouts/portal/base.blade.php:101,110,142`).
- [ ] `PC-UI-PORTAL-10` — Área `header` com bloco SLE ativo suprime header padrão e banner (`V2/layouts/portal/partials/portal-content.blade.php:13-29`).

## 10. Evidências consultadas

- `V2/layouts/portal/**` (50 arquivos; leitura integral dos 39 listados em `fontes` e dos 11 de acessibilidade usados como referência).
- `V2/Core/Providers/V2ServiceProvider.php:29-148`; `V2/Modules/Support/SupportConfigs/ModuleProvider.php:1-100`; `V2/Modules/Support/SupportConfigs/Services/PortalConfigsService.php:20-249`.
- `V2/Modules/Support/StructuralLandingEditor/ViewComposers/LayoutBaseComposer.php:1-23`; `.../Services/BlockRenderer.php:20-160`; `.../Services/ComponentResolver.php:1-60`; `.../Controllers/LayoutBaseCmsController.php:180-265`; `.../Controllers/SleCmsController.php:252-300`.
- `V2/Services/SEO/Providers/SEOServiceProvider.php:1-60`; `V2/Services/SEO/Services/SEOService.php:20-200,364-408`.
- `app/Exceptions/Handler.php:1-89`; `config/app.php:165-190`; `V2/Core/Middleware/CspMiddleware.php:1-110`.
- `public/assets/js/portal-init.js:1-280`; `public/assets/css/portal/portal-base.css:1-30`; `public/assets/css/portal/portal-components.css:50-80`.
- `V2/Core/Controllers/SearchController.php:1-50`; `V2/Services/Search/Services/SearchService.php:20-90`.
- `V2/Modules/Layouts/BasicPage/Views/portal/acessibilidade.blade.php:1-20`; `V2/Modules/Publicacoes/Noticias/Views/portal/index.blade.php:1-30`.

## 11. Checklist de extração (A–M)

| Item | Resposta |
|---|---|
| A1 | Toda página pública recebe chrome completo + SEO + tema (§1.1). |
| A2 | `@extends` com flags/seções; precondição: namespace `v2-portal` registrado e SLE presente para `layoutConfigs`. |
| A3 | HTML, metatags, JSON-LD, GA (bloqueado), clone de impressão. |
| A4 | Buffer `seo_access_buffer` por view composta; requisições a `manifest.json`/`sw.js`; logs de `PortalConfigsService` em debug quando a tabela falta. |
| A5 | Página válida → 200 com chrome; erro HTTP mapeado → view de erro; status sem view → render padrão. |
| A6 | Não autentica, não faz cache HTTP, não gera breadcrumb completo, não injeta JS de módulo. |
| A7 | Público: chrome completo; CMS: não usa este layout (usa `v2-cms`); API: não usa. |
| B1 | Composição `base → app-layout → portal-content → parciais` (§2.1). |
| B2 | Ordem de 15 passos (§2.2) com arquivos/linhas. |
| B3 | Flags em vez de componentes; tema por variáveis CSS; decisões de vendorização local. |
| B4 | 7 trechos citados (§2.3). |
| B5 | N+1 evitado por `static` no composer de tema; `LayoutBaseConfig::all()` uma vez por request; 8 chamadas de `trackAccess` por pageview (custo de escrita em buffer). |
| B6 | Estado do tema vem do banco a cada request (static por request); sem cache explícito; alteração no CMS reflete no próximo request. |
| B7 | Composer captura exceções de tabela; parciais não capturam (Carbon, `has()`), gerando 500. |
| C1 | Fonte única do chrome público, tokens e ordem de assets. |
| C2 | Diverge do `v2-cms` (Bootstrap/AdminLTE) e não reutiliza componentes do CMS. |
| C3 | Invariantes §3.1. |
| C4 | Tabela ausente, logo ausente, título vazio, LAI desligado (não funciona), banner com data inválida. |
| C5 | Duplicação de `<main>` nos erros; `sle_component` sem título real; canonical com query string. |
| C6 | Ordem de jQuery, `!important` do tema, timing dos stacks, CSP. |
| C7 | `V2/layouts/portal/header/accessibility/scripts.blade.php` morto; `coverImage`/`gtag`/`json-ld`/`faviconUrl` mortos; `getCoverImage` fora do StorageService. |
| D1 | Superfície: views `v2-portal::*`, variáveis de composer, helpers `v2_storage_url`/`v2_version`. |
| D2 | Registro de views em `V2/Core/Providers/V2ServiceProvider.php:40-42`; composers em `:48-73` e nos `ModuleProvider` dos módulos Support. |
| D3 | Ordem: core composer → SupportConfigs → SLE; `layoutConfigs` precisa ser Collection. |
| D4 | Contrato Blade/seções/stacks e flags (§1.2); JS por seletores `.js-*` (§2.7). |
| D5 | Receitas §4.2. |
| D6 | Anti-uso §4.3. |
| D7 | Sem paginação própria no layout; busca limita 20 resultados; impressão A4; sem timeout específico. |
| D8 | Consumidores e risco §4.4. |
| D9 | Extensão por stacks; novos parciais em `v2-portal::`; SLE via `sle_component`. |
| E1 | Não se aplica: o layout não declara rotas; a única rota citada é `GET /busca` (`V2/Core/Routes/web.php:14`). |
| E2 | Não se aplica (sem controller próprio). |
| E3 | Não se aplica. |
| E4 | Erros HTTP mapeados por status (§2.11). |
| E5 | CSRF apenas via `meta[name=csrf-token]` (`V2/layouts/portal/base.blade.php:6`); o layout não autentica. |
| E6 | Não se aplica. |
| E7 | Não se aplica. |
| F1 | Não se aplica: layout não persiste. |
| F2 | Não se aplica. |
| F3 | Não se aplica. |
| F4 | Não se aplica. |
| F5 | `v2_portal_configs` e `v2_layout_base_configs` só são lidas aqui. |
| F6 | Duas queries por request (configs/tema e layout base), cacheadas em `static`/instância. |
| G1–G6 | Não se aplica: telas de CMS estão em `UI-CMS`. |
| H1 | Dados exibidos vêm de configs do portal e do controller da página (§1.2, §2.5–2.8). |
| H2 | `header/banner` → `<main>` com asides → `footer`; headings do header usam `<h1 class=h2>`; LAI usa `<h2>` no rodapé. |
| H3 | Origem de cada metatag em §2.6. |
| H4 | Logos com `alt` e dimensões; banner `loading=lazy`; capa OG com fallback de logo. |
| H5 | Breakpoints §6; impressão §2.10. |
| H6 | Sem cache HTTP no layout; cache de fragmentos não é usado. |
| I1–I8 | Acessibilidade detalhada em `04-acessibilidade.md`; aqui só a integração. |
| J1 | Chaves em §2.4/§2.6 e no índice `config.csv`. |
| J2 | Não se aplica (nenhuma permissão no layout público). |
| J3 | Não se aplica. |
| J4 | Flags `accessibility_bar_enabled`, `accessibility_widget_enabled`, `vlibras_enabled` (§2.4). |
| J5 | Não se aplica. |
| K1 | `Log::debug` em `V2/Modules/Support/SupportConfigs/Services/PortalConfigsService.php:47` e `SupportConfigs/ModuleProvider.php:47`; `Log::warning` em `SEOService::trackAccess` (`V2/Services/SEO/Services/SEOService.php:158`). |
| K2 | Nenhuma auditoria disparada pelo layout; o CMS audita gravações de configs. |
| K3 | 403/404/419/500/503 (§2.11). |
| K4 | Busca limita 20 (`V2/Services/Search/Services/SearchService.php:39`); sem throttle no layout. |
| K5 | `header_scripts`/`footer_scripts` são HTML cru do admin — superfície de injeção. |
| L1–L3 | Seeds de configs em `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:33-42` e migration `V2/Modules/Support/SupportConfigs/Migrations/2026_06_30_000005_create_v2_portal_configs_table.php:56-64`. |
| M1 | `PC-UI-PORTAL-01..10`. |
| M2 | `LG-UI-PORTAL-01..05`. |
| M3 | §10. |
| M4 | Lint e `coverage.py --strict` executados; índice parcial emitido. |
