---
id: MOD-INSTITUCIONAL-FALECONOSCO-PORTAL
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-INSTITUCIONAL-FALECONOSCO]
fontes:
  - V2/Modules/Institucional/FaleConosco/
depende_de: [MOD-INSTITUCIONAL-FALECONOSCO, CORE-RUNTIME, SVC-SEO, MOD-SUPPORT-STRUCTURALLANDINGEDITOR]
consumidores: [UI-PORTAL, SVC-SEO]
criticidade: alta
dono: InstitucionalA
---

# FaleConosco — página pública `/fale-conosco`

> Anexo de [`spec.md`](spec.md). A página tem duas metades: o formulário (POST com validação
> e captcha opcional) e um cartão de informações que **não** pertence ao módulo — vem do
> Editor Estrutural (`v2_layout_base_configs`, chave `contato`). A rota é excluída de todos
> os caches e do índice de busca; os e-mails são tentados de forma silenciosa.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Exibe o formulário de contato e os dados institucionais, grava a mensagem e tenta notificar o destinatário configurado e o visitante. |
| Entrada | `GET /fale-conosco`; `POST /fale-conosco` com `name`, `email`, `phone`, `subject`, `message`, `g-recaptcha-response` (opcional). |
| Saída | HTML `v2-faleconosco::portal.index`; `302` + flash; linha em `v2_mensagens_contato`; tentativas em `v2_mail_logs`. |
| Efeitos colaterais | E-mails SMTP síncronos; logs de erro de mail; `ContactPage` JSON-LD; `page_context=fale-conosco`; nenhum cache. |
| Particularidade central | Os dados de exibição vêm do SLE e são mesclados com o grupo `CONTATO` de `v2_portal_configs`; o formulário não renderiza reCAPTCHA, mas o controller tenta validá-lo se o campo chegar. |
| Como se integra | View `v2-faleconosco::portal.index`; layout `v2-portal::app-layout`; `LayoutBaseConfig` (SLE); `SEOService`; `MailService`; máscara `assets/js/masks.js`. |
| Se removido, quebra | `/fale-conosco` cai no `BasicPage` semeado (HTML antigo com form `action="#"`), que não grava mensagens; o link do rodapé/menu continua. |
| Superfície pública | `FaleConoscoPortalController@index`, `@store`; view; nada em JS próprio. |

## 1. Efeito

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

- Ler os canais institucionais (telefone, e-mail, endereço, CEP, caixa postal, horário,
  mapa) (`Views/portal/index.blade.php:134-215`).
- Preencher e enviar a mensagem, com repopulação em caso de erro (`old()` em todos os
  campos) (`:57-120`).
- Receber confirmação `Mensagem enviada com sucesso! Entraremos em contato em breve.`
  (`FaleConoscoPortalController.php:161-163`).
- Receber auto-resposta por e-mail **se** `contato_auto_reply_body` estiver configurado.

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| INSERT em `v2_mensagens_contato` | `store` | `FaleConoscoPortalController.php:105-114` |
| Notificação interna `[Contato] {subject}` | `store` | `:116-135` |
| Auto-resposta `Recebemos sua mensagem` (ou assunto configurado) | `store` | `:137-159` |
| `v2_mail_logs` para cada tentativa | `MailService::logMail` | `MailService.php:360-380` |
| Nenhum cache/headers públicos | middlewares de cache excluem a rota | `PageCacheMiddleware.php:16-23`; `PublicCacheMiddleware.php:31-36` |
| Visita rastreada pelo SEO | `SEOService::trackAccess` | `SEOServiceProvider.php:31-36` |

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

| Entrada | Resultado |
|---|---|
| `GET /fale-conosco` | `200`; formulário + cartão; sem `X-Cache-Status`; sem `Cache-Control` público. |
| `POST` válido | `302` → `/fale-conosco` + flash de sucesso; mensagem persistida. |
| `POST` inválido | `302` back + erros como chaves cruas (`validation.required`, `validation.email`, `validation.regex`, `validation.max.string`); campos repopulados. |
| `POST` com `g-recaptcha-response` inválido | `302` back + `Falha na verificação do reCAPTCHA. Tente novamente.` |
| `POST` sem o campo de captcha | prossegue (captcha não obrigatório). |
| `POST` sem CSRF | `419`. |
| `POST` com SMTP quebrado | `302` + flash de sucesso; `v2_mail_logs.status='failed'`. |
| `GET /fale-conosco` com página em manutenção | `503` (MaintenanceMiddleware). |
| `GET /fale-conosco` com módulo desativado | `200` (a flag só vale para o CMS). |

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

- Não renderiza o widget nem o script do reCAPTCHA (só o `BasicPage` semeado tem).
- Não envia anexos, cópia ou confirmação visual de envio de e-mail.
- Não limita tentativas por IP (sem throttle/honeypot).
- Não usa AJAX: submit tradicional com redirect (PRG).
- Não tem página de sucesso própria (volta ao formulário).
- Não exibe os e-mails configurados em `contato_*` (apenas o e-mail do SLE).
- Não tem SEO editável por tela.

## 2. Abordagem

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

```mermaid
flowchart LR
  G[GET /fale-conosco] --> S1[SEO setters]
  S1 --> S2[LayoutBaseConfig contato]
  S2 --> S3[v2_portal_configs grupo CONTATO]
  S3 --> V[view: form + infos]
  P[POST /fale-conosco] --> V1[validate]
  V1 --> RC{tem g-recaptcha-response?}
  RC -->|sim| GV[siteverify cURL]
  RC -->|nao| DB
  GV -->|falha| ERR[flash + back]
  GV -->|ok| DB[(INSERT)]
  DB --> M1[mail admin]
  DB --> M2[mail auto-resposta]
  M1 --> OK[flash sucesso + redirect]
  M2 --> OK
```

### 2.2 Mecânica passo a passo

1. `GET /fale-conosco` passa por manutenção e pelos middlewares de cache, que **não**
   cacheiam a rota.
2. `index` define SEO fixo: `Fale Conosco — {app.name}`, descrição `Entre em contato
   conosco. Envie sua mensagem, dúvida ou sugestão.`, keywords `contato, fale conosco,
   suporte, {app.name}` (`FaleConoscoPortalController.php:36-38`).
3. Lê `LayoutBaseConfig::where('key','contato')->first()`; se existir, usa `payload`;
   senão `[]` (`:41-42`).
4. Mapeia 7 chaves do payload para nomes da view (`phone`, `email`, `address`,
   `business_hours`, `map_embed_url`, `zipcode`, `po_box`) (`:45-53`).
5. Lê `v2_portal_configs` do grupo `CONTATO` com `pluck('config_value','config_key')` e
   faz `array_merge($displayData, $formConfigs)`; chaves do grupo sobrescrevem as do SLE em
   caso de colisão (`:56-61`).
6. Renderiza a view com `$configs` (`:63`).
7. `POST`: valida os 5 campos; o captcha só roda se o campo existir (`:72-103`).
8. Cria a mensagem, salva, tenta notificação e auto-resposta, flash e redirect (`:105-163`).

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

- **Dados de contato no SLE**: uma fonte para header/footer e página; o módulo só mapeia
  nomes `[C]` (`:40-53`; `Routes/cms.php:13-15`).
- **Merge com `CONTATO`**: permite sobrescrever exibição por chave de config, embora não
  haja tela nem uso previsto `[C]` (`:56-61`).
- **Captcha condicional**: permite página sem widget; segurança fica opcional `[C]`.
- **E-mails best-effort**: não bloqueiam a gravação nem a resposta ao usuário `[C]`.
- **Sem página de sucesso**: PRG simples; o flash é a confirmação `[C]` (`:161-163`).

### 2.4 Trechos de código pivô

```php
// V2/Modules/Institucional/FaleConosco/Controllers/FaleConoscoPortalController.php:40-61 — dados de exibição e merge
$contatoConfig = LayoutBaseConfig::where('key', 'contato')->first();
$payload = $contatoConfig ? $contatoConfig->payload : [];

$displayData = [
    'phone'          => $payload['telefone_1'] ?? '',
    'email'          => $payload['email'] ?? '',
    'address'        => $payload['endereco'] ?? '',
    'business_hours' => $payload['horario'] ?? '',
    'map_embed_url'  => $payload['map_link'] ?? '',
    'zipcode'        => $payload['zipcode'] ?? '',
    'po_box'         => $payload['po_box'] ?? '',
];

$formConfigs = DB::table('v2_portal_configs')
    ->where('config_group', 'CONTATO')
    ->pluck('config_value', 'config_key')->toArray();

$configs = array_merge($displayData, $formConfigs);
```

`telefone_2` é salvo pelo SLE, mas **não** mapeado. `map_link` é usado como `src` de iframe
— o campo do SLE se chama “Link do Google Maps”, então o iframe aponta para a página do
Maps, não para um embed (comportamento a preservar ou corrigir conscientemente).

```blade
{{-- V2/Modules/Institucional/FaleConosco/Views/portal/index.blade.php:201-215 — mapa --}}
@if(!empty($configs['map_embed_url']))
    <div class="card border-0 shadow-sm">
        <div class="card-body p-0">
            <div class="ratio ratio-16x9">
                <iframe src="{{ $configs['map_embed_url'] }}"
                        style="border:0;"
                        allowfullscreen=""
                        loading="lazy"
                        referrerpolicy="no-referrer-when-downgrade"
                        title="Mapa de localização">
                </iframe>
            </div>
        </div>
    </div>
@endif
```

O `title` garante nome acessível; `referrerpolicy` e `loading=lazy` são os únicos controles
de privacidade/performance.

```blade
{{-- V2/Modules/Institucional/FaleConosco/Views/portal/index.blade.php:57-120 — formulário público --}}
<form action="{{ route('v2.fale-conosco.portal.store') }}" method="POST">
    {{ csrf_field() }}
    <div class="row g-3">
        <div class="col-md-6">
            <label for="name" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
            <input type="text" id="name" name="name" class="form-control" required
                   value="{{ old('name') }}" placeholder="Seu nome completo" maxlength="255">
        </div>
        ...
        <div class="col-md-6">
            <label for="phone" class="form-label fw-semibold">Telefone</label>
            <input type="tel" id="phone" name="phone" class="form-control"
                   value="{{ old('phone') }}" placeholder="(61) 99999-9999"
                   data-mask="phone" maxlength="16" inputmode="tel">
        </div>
        ...
        <textarea id="message" name="message" class="form-control" rows="6" required
                  placeholder="Digite sua mensagem...">{{ old('message') }}</textarea>
        <div class="form-text">Máximo de 10.000 caracteres.</div>
```

Notas: `maxlength` de `message` não é definido no HTML (só a ajuda), e não há
`g-recaptcha`. A máscara de telefone roda em `input` e formata para `(DD) 9999-9999` ou
`(DD) 99999-9999` (`masks.js:39-60`), compatível com a regex do servidor.

```php
// V2/Modules/Institucional/FaleConosco/Controllers/FaleConoscoPortalController.php:137-159 — auto-resposta
$autoReplySubject = DB::table('v2_portal_configs')
    ->where('config_key', 'contato_auto_reply_subject')->value('config_value');
$autoReplyBody = DB::table('v2_portal_configs')
    ->where('config_key', 'contato_auto_reply_body')->value('config_value');

if (!empty($autoReplyBody)) {
    try {
        $this->mailService->send(
            $mensagem->email,
            $autoReplySubject ?: 'Recebemos sua mensagem',
            nl2br(e($autoReplyBody)),
            ['to_name' => $mensagem->name, 'use_template' => false, 'is_html' => true]
        );
    } catch (\Exception $e) {
        \Log::error('[FaleConosco] Falha ao enviar auto-resposta: ' . $e->getMessage());
    }
}
```

Assim como a notificação interna, o `try/catch` não cobre falhas de envio (só falhas pré-`try`, como banco) e o retorno é ignorado; o
texto é escapado antes do `nl2br`.

### 2.5 Algoritmos, consultas e custos

- GET: 1 select em `v2_layout_base_configs` + 1 select `pluck`; sem cache; sem N+1.
- POST: validação → (cURL opcional ao Google, síncrono, sem timeout explícito) → 1 INSERT →
  até 2 selects + até 2 envios SMTP + até 2 inserts de log.
- Sem fila: o tempo de resposta inclui SMTP e, se usado, o reCAPTCHA.
- Sem limite de tentativas: custo por requisição é o do banco + e-mails.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Dependência de tela de outro módulo (SLE)** para o conteúdo da página.
- **Captcha opcional por presença de campo**, com segredo ausente da config.
- **Formulário sem `maxlength` na mensagem** (só ajuda textual).
- **E-mail do SLE como `multiple`** no campo do editor, mas exibido como um único texto.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-FALECONOSCO-PORTAL-01` | Sem `LayoutBaseConfig` `contato`, todos os campos exibidos ficam vazios e os blocos são omitidos (`!empty`). | Página funciona “vazia”, sem erro de variável. | `index.blade.php:135-196` |
| `RN-FALECONOSCO-PORTAL-02` | Chaves do grupo `CONTATO` sobrescrevem os nomes mapeados no merge. | Uma chave `phone` no grupo mudaria o telefone exibido. | `FaleConoscoPortalController.php:61` |
| `RN-FALECONOSCO-PORTAL-03` | `telefone_2` não é exibido. | Segundo telefone só aparece em outros blocos do portal. | `:45-53` |
| `RN-FALECONOSCO-PORTAL-04` | `map_link` vira `src` de `<iframe>` sem validação de domínio. | Um link arbitrário é embutido; risco de conteúdo externo. | `:50`; `index.blade.php:205` |
| `RN-FALECONOSCO-PORTAL-05` | SEO é fixo por setters; a view também passa `pageTitle`/`pageDescription` literais, que perdem para os setters. | Título/descrição não são editáveis. | `:36-38`; `index.blade.php:1-13`; `metadata/seo.blade.php:2-4` |
| `RN-FALECONOSCO-PORTAL-06` | JSON-LD `ContactPage` com URL relativa (`route(..., false)`). | Schema sem host absoluto. | `index.blade.php:5-13` |
| `RN-FALECONOSCO-PORTAL-07` | Sem cache de página/HTTP. | Sempre renderiza o estado atual do SLE. | `PageCacheMiddleware.php:16-23`; `PublicCacheMiddleware.php:31-36` |
| `RN-FALECONOSCO-PORTAL-08` | `robots.txt` bloqueia `/fale-conosco`; não há entrada no sitemap. | Página não é indexada. | `SeoPortalController.php:43-50,325-355` |
| `RN-FALECONOSCO-PORTAL-09` | Formulário usa `old()` em todos os campos. | Erros repopulam, inclusive a mensagem. | `index.blade.php:65-111` |
| `RN-FALECONOSCO-PORTAL-10` | O layout desliga header/footer/áreas laterais e usa `fullWidthMain`. | A página controla o próprio hero/CTA e não tem navegação global. | `index.blade.php:1-22` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Sem `contato` no SLE**: cartão de informações vazio e sem mapa; o form continua.
- **`map_link` inválido**: iframe com URL quebrada, sem fallback.
- **Telefone digitado parcial**: a máscara formata, mas o servidor rejeita se não tiver DDD
  e 8/9 dígitos.
- **Telefone com espaço extra ou sem espaço após o DDD**: rejeitado pela regex.
- **Mensagem com 10.001 caracteres**: erro do servidor (não há `maxlength` no HTML).
- **Campo de captcha enviado por bot**: como o `secret` é `''`, a verificação falha e o bot é
  bloqueado — mas o mesmo acontece com um humano se a view um dia renderizar o widget sem
  configurar o segredo.
- **cURL indisponível/timeout**: `json_decode(false)` → `null` → falha de captcha.
- **SMTP lento**: o POST demora; não há timeout de aplicação.
- **JS desabilitado**: o form funciona, mas sem máscara (e a regex rejeita formatos sem
  máscara).
- **Flash**: aparece no layout global; a página não tem `aria-live` próprio.

### 3.4 Armadilhas de reimplementação

- **Mover os dados de contato para este módulo**: cria segunda fonte e quebra a coerência
  com header/footer.
- **Exigir captcha sempre**: hoje o widget não existe; exigir quebra todos os envios.
- **Assumir que o e-mail do cartão é o destinatário**: destinatário é
  `contato_recipient_email`, independente do exibido.
- **Tratar `map_link` como URL de embed**: o SLE salva um link do Maps (página), não o
  `src` do embed.
- **Cachear a página**: o formulário tem CSRF e o conteúdo pode mudar no SLE; os
  middlewares a excluem de propósito.

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

- **Campo “Link do Google Maps”** no SLE (`contato.blade.php:20-28`) vs uso como `src` de
  iframe (`index.blade.php:205`).
- **`telefone_2`** salvo e nunca exibido aqui.
- **Captcha**: o `BasicPage` semeado tem widget; a view do módulo não. Divergência de
  implementação entre as duas versões da página.
- **Comentário “configurações movidas para o SLE”**: apenas a exibição foi; as chaves de
  comportamento continuam sem tela.
- **SEO fixo**: nenhuma tela edita title/description desta página.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `GET /fale-conosco` | rota | `v2.fale-conosco.portal.index` | — | visitantes, links do site |
| `POST /fale-conosco` | rota | `v2.fale-conosco.portal.store` | CSRF | formulário |
| `FaleConoscoPortalController@index` | ação | `(): View` | SLE instalado para conteúdo | rota |
| `v2-faleconosco::portal.index` | view | `$configs` (array) | — | controller |
| `LayoutBaseConfig` | model externo | `payload` array | SLE instalado | index |

### 4.2 Contrato de UI/JS

- **JS**: apenas `assets/js/masks.js` (defer, do layout) aplicando `data-mask="phone"` e
  `data-mask="cep"` (o CEP aparece no editor do SLE, não nesta página).
- **DOM**: `#name`, `#email`, `#phone`, `#subject`, `#message`; sem IDs de seção.
- **Sem AJAX/eventos customizados**; submit nativo.
- **Sem JS**: máscara não aplicada; validação HTML5 (`required`, `maxlength`) ainda atua.

### 4.3 Ordem e ciclo de vida

1. Manutenção → controller → view; caches pulam a rota.
2. `LayoutBaseConfig` precisa existir como classe/tabela; não há guarda.
3. `SEOService` define metadados antes do `<head>`.
4. O POST depende de `MailService`; a view depende de `$configs` sempre definido.

### 4.4 Receitas de uso

```php
// Reutilizar o mapa de contato em outro contexto (mesma semântica da página)
$payload = \V2\Modules\Support\StructuralLandingEditor\Models\LayoutBaseConfig::where('key','contato')->first()->payload ?? [];
$telefone = $payload['telefone_1'] ?? '';
```

```blade
{{-- Renderizar o formulário apontando para o endpoint correto --}}
<form action="{{ route('v2.fale-conosco.portal.store') }}" method="POST">
    {{ csrf_field() }}
    ...
</form>
```

```http
POST /fale-conosco HTTP/1.1
Content-Type: application/x-www-form-urlencoded

_token=...&name=Ana&email=ana@exemplo.org&phone=%2861%29%2099999-9999&subject=D%C3%BAvida&message=Ol%C3%A1

HTTP/1.1 302 Found
Location: /fale-conosco
```

### 4.5 Anti-uso

- **Exibir `$configs['email']` como destinatário**: o envio usa `contato_recipient_email`.
- **Enviar telefone só com dígitos** (`61999999999`): a regex exige máscara.
- **Usar o `BasicPage` semeado como fallback de conteúdo**: ele contém um form inerte.

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

- 2 e-mails síncronos por envio (quando configurados), cada um com insert de log.
- Sem throttle/quotas: risco de abuso e de fila SMTP.
- `message` até 10.000; sem anexos.
- Sem cache: cada visita consulta SLE + configs.

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Efeito |
|---|---|---|---|---|---|---|
| grupo | — | — | — | `web`; `MaintenanceMiddleware`; `page.cache`; `public.cache` | — | caches excluem a rota |
| endpoint | GET | `/fale-conosco` | `v2.fale-conosco.portal.index` | idem | `@index` | HTML |
| endpoint | POST | `/fale-conosco` | `v2.fale-conosco.portal.store` | idem | `@store` | grava + e-mails |

Detalhes em [`rotas.md`](rotas.md). Sem parâmetros de rota ou query.

## 6. Dados e modelo de domínio

Lê `v2_layout_base_configs` (SLE) e `v2_portal_configs` (grupo `CONTATO`); escreve
`v2_mensagens_contato` e `v2_mail_logs`. Ver [`spec.md`](spec.md) §6.

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant V as Visitante
  participant C as FaleConoscoPortalController
  participant SLE as v2_layout_base_configs
  participant DB as v2_mensagens_contato
  V->>C: GET /fale-conosco
  C->>SLE: payload(key=contato)
  C-->>V: view com $configs
  V->>C: POST {name,email,phone,subject,message}
  C->>C: validate (+captcha se campo)
  C->>DB: INSERT
  C-->>V: 302 /fale-conosco + flash
```

## 8. UI CMS

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

## 9. UI Portal

### 9.1 Estrutura e hierarquia

1. Hero `.breadcrumb-wrapper` com imagem `assets/sucesu/img/hero/breadcrumb.png`,
   breadcrumb `Home › Fale Conosco` e `<h1>Fale Conosco</h1>` (`index.blade.php:31-42`).
2. `<section class="section-padding">` com `<div class="row g-5">`:
   - coluna 7: card do formulário, `<h2 class="h4">Envie sua Mensagem</h2>`;
   - coluna 5: card “Informações de Contato” + card do mapa.
3. Fim da página (sem CTA).

### 9.2 Formulário

| Campo | Tipo | Obrigatório | HTML | Server | Default/repopula |
|---|---|---|---|---|---|
| `name` | text | sim | `required`, `maxlength=255` | `required|string|max:255` | `old('name')` |
| `email` | email | sim | `required`, `maxlength=255` | `required|email|max:255` | `old('email')` |
| `phone` | tel | não | `data-mask="phone"`, `maxlength=16`, `inputmode=tel` | `nullable|string|max:16|regex` | `old('phone')` |
| `subject` | text | sim | `required`, `maxlength=255` | `required|string|max:255` | `old('subject')` |
| `message` | textarea | sim | `required`, `rows=6` (sem `maxlength`) | `required|string|max:10000` | `old('message')` |

Botão: `Enviar Mensagem` com ícone `bi-send`. Sem `novalidate`, sem captcha, sem honeypot.

### 9.3 Cartão de informações

Ordem condicional: Telefone (`phone`), E-mail (`mailto:`), Endereço (+ CEP), Caixa Postal,
Horário; mapa em iframe 16:9 no fim (`index.blade.php:134-215`). Ícones `bi-telephone`,
`bi-envelope`, `bi-geo-alt`, `bi-mailbox`, `bi-clock`, todos com `aria-hidden="true"`.

### 9.4 SEO

| Metadado | Origem |
|---|---|
| title/description/keywords | setters no controller (valores fixos) |
| `pageTitle`/`pageDescription` | literais na view, perdem para os setters |
| canonical | `url()->current()` |
| OG/Twitter | derivados dos mesmos valores; `og:type=website`, `twitter:card=summary_large_image` |
| JSON-LD | `ContactPage` com `name`, `description`, `url` relativa |
| robots | `Disallow: /fale-conosco` |
| sitemap | ausente |

### 9.5 Cache

- `PageCacheMiddleware` exclui `fale-conosco*`; `PublicCacheMiddleware` exclui
  `/fale-conosco` por prefixo. Não há `X-Cache-Status` nem `Cache-Control` público.
- Como o GET exibe dados do SLE e o POST usa CSRF, a exclusão é intencional.

### 9.6 Mídia, responsividade e print

- Hero com `background-image` fixo; não usa `picture`/galeria.
- Layout Bootstrap: `col-lg-7`/`col-lg-5`; empilha abaixo de `lg`.
- Mapa `ratio-16x9`, `loading="lazy"`, `allowfullscreen`, `referrerpolicy`.
- Sem CSS de print no módulo.

### 9.7 Acessibilidade

- Labels associados; obrigatório indicado por `*` visual + `required`.
- Ícones decorativos com `aria-hidden`; iframe com `title`.
- E-mail como link `mailto:`.
- Sem `aria-live` para o flash; sem link “pular para o conteúdo” próprio (vem do layout).
- Contraste do CTA/botão depende do tema.

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

- Pública, sem permissão.
- Três chaves `contato_*` (sem default/tela) em `v2_portal_configs`.
- `config('v2.recaptcha.secret')` **não existe** (default `''`); `services.recaptcha.site_key`
  só é usado no HTML do `BasicPage` semeado, não nesta view.
- Flag do módulo não afeta a rota pública (só o CMS).
- `config('app.name')` no SEO.

## 11. Observabilidade, erros e limites

- Flash de sucesso/erro; erros de validação padrão; `419`; `503`.
- Logs de mail (`[MailService] …`) e de `v2_mail_logs`.
- Nenhum `activity_log` no envio público.
- Limites: 10.000/255/16; sem throttle; sem timeout de cURL/SMTP explícito.
- Dados pessoais: coletados e armazenados sem aviso de privacidade próprio na página.

## 12. Seeds e dados iniciais

`FaleConoscoSeeder` cria um `BasicPage` com slug `fale-conosco` (hero, contatos fixos,
form `action="#"`, reCAPTCHA). A rota do módulo o sombreia; ver [`spec.md`](spec.md) §12.
Nenhum seed de `contato_*`.

## 13. Lacunas

- `LG-FALECONOSCO-PORTAL-01` — Se `map_link` era um embed no passado e mudou de semântica;
  procurar no histórico do SLE. Impacto: mapa quebrado.
- `LG-FALECONOSCO-PORTAL-02` — Se há um widget de reCAPTCHA em outro layout/partial que
  injete `g-recaptcha-response` nesta página; procurar em `V2/layouts/portal` e assets.
  Impacto: spam/bloqueio.
- `LG-FALECONOSCO-PORTAL-03` — Origem do `email` com atributo `multiple` no editor do SLE
  (um único valor é exibido). Impacto: baixo.
- `LG-FALECONOSCO-PORTAL-04` — Se `trackAccess` grava visitas mesmo para esta rota (não está
  na lista de exclusão do SEO). Impacto: métricas.

## 14. Checklist de paridade

- [ ] `PC-FALECONOSCO-PORTAL-01` — Hero/breadcrumb, formulário em card e cartão de
      informações condicional ao payload do SLE.
- [ ] `PC-FALECONOSCO-PORTAL-02` — 5 campos com tipos, obrigatoriedade, `old()` e máscara de
      telefone; mensagem sem `maxlength` no HTML.
- [ ] `PC-FALECONOSCO-PORTAL-03` — Mapa em iframe 16:9 com `title`, `loading=lazy` e
      `referrerpolicy`.
- [ ] `PC-FALECONOSCO-PORTAL-04` — SEO fixo por setters; JSON-LD `ContactPage`; `gtag`
      `page_context=fale-conosco`.
- [ ] `PC-FALECONOSCO-PORTAL-05` — `robots.txt` bloqueia a rota; sitemap não a inclui.
- [ ] `PC-FALECONOSCO-PORTAL-06` — Sem cache de página/HTTP; headers não públicos.
- [ ] `PC-FALECONOSCO-PORTAL-07` — POST válido persiste e redireciona com flash exato.
- [ ] `PC-FALECONOSCO-PORTAL-08` — Captcha só quando o campo existe; falha com a mensagem
      exata e repopulação.
- [ ] `PC-FALECONOSCO-PORTAL-09` — Sem `contato` no SLE: página renderiza sem erros e sem
      blocos vazios.
- [ ] `PC-FALECONOSCO-PORTAL-10` — E-mails condicionados às chaves `contato_*`, com os
      assuntos/corpos definidos em [`spec.md`](spec.md) §2.4.

## 15. Evidências consultadas

- `V2/Modules/Institucional/FaleConosco/Views/portal/index.blade.php:1-221`.
- `V2/Modules/Institucional/FaleConosco/Controllers/FaleConoscoPortalController.php:1-165`.
- `V2/Modules/Support/StructuralLandingEditor/Controllers/LayoutBaseCmsController.php:185-199`;
  `Views/cms/tabs/layoutbase/contato.blade.php:1-101`;
  `Models/LayoutBaseConfig.php:1-40`.
- `V2/Core/Middleware/PageCacheMiddleware.php:16-23`;
  `V2/Core/Middleware/PublicCacheMiddleware.php:31-36,70-76`.
- `V2/Services/SEO/Providers/SEOServiceProvider.php:38-56`;
  `V2/layouts/portal/metadata/seo.blade.php:1-50`;
  `V2/Services/SEO/Controllers/SeoPortalController.php:35-55,325-355`.
- `V2/Services/MailService/MailService.php:39-134`;
  `public/assets/js/masks.js:39-60`.
