Compare commits
29 Commits
fix/man-10
...
feat/whats
| Author | SHA1 | Date | |
|---|---|---|---|
| 400c147a74 | |||
| 08c6d62498 | |||
| 75cce50bd5 | |||
| a0963dd57a | |||
| 77dc592963 | |||
| 623e45cbea | |||
| 455bc4b7d8 | |||
| 3f55180b73 | |||
| ea630328c9 | |||
| 1425d7aaa1 | |||
| 62a19147ad | |||
| 90632ada88 | |||
| ce90cedb18 | |||
| a7d72f3756 | |||
| 0e0724c1cc | |||
| eca261ceba | |||
| ad29ce54d8 | |||
| 0d1ca40c11 | |||
| 484ac77ce7 | |||
| d0e508b237 | |||
| 70cc65227a | |||
| 213d6843e7 | |||
| f8765add85 | |||
| 168a21e459 | |||
| 7902b35393 | |||
| 28e25b4464 | |||
| 8afb925e17 | |||
| 338e3c9adb | |||
| 0ecae4c7d6 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -205,15 +205,12 @@ jobs:
|
|||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install chromium --with-deps
|
- run: npx playwright install chromium --with-deps
|
||||||
- run: php artisan migrate --force
|
- run: php artisan migrate --force
|
||||||
- run: php artisan db:seed --class=VisualContentSeeder --force
|
|
||||||
- run: php artisan storage:link
|
- run: php artisan storage:link
|
||||||
|
|
||||||
- name: Build application image
|
- name: Build application image
|
||||||
run: docker build -t amare-app:ci .
|
run: docker build -t amare-app:ci .
|
||||||
|
|
||||||
- name: Run browser tests against FrankenPHP container
|
- name: Run browser tests against FrankenPHP container
|
||||||
env:
|
|
||||||
APP_FROZEN_NOW: "2026-03-15T12:00:00-03:00"
|
|
||||||
run: |
|
run: |
|
||||||
docker run -d --name amare-web \
|
docker run -d --name amare-web \
|
||||||
-e APP_ENV=testing \
|
-e APP_ENV=testing \
|
||||||
@@ -222,7 +219,6 @@ jobs:
|
|||||||
-e APP_LOCALE=pt_BR \
|
-e APP_LOCALE=pt_BR \
|
||||||
-e APP_FALLBACK_LOCALE=pt_BR \
|
-e APP_FALLBACK_LOCALE=pt_BR \
|
||||||
-e APP_TIMEZONE=America/Sao_Paulo \
|
-e APP_TIMEZONE=America/Sao_Paulo \
|
||||||
-e APP_FROZEN_NOW="${APP_FROZEN_NOW}" \
|
|
||||||
-e DB_CONNECTION=pgsql \
|
-e DB_CONNECTION=pgsql \
|
||||||
-e DB_HOST=host.docker.internal \
|
-e DB_HOST=host.docker.internal \
|
||||||
-e DB_PORT=5432 \
|
-e DB_PORT=5432 \
|
||||||
@@ -253,8 +249,6 @@ jobs:
|
|||||||
mkdir -p artifacts/browser
|
mkdir -p artifacts/browser
|
||||||
docker logs amare-web > artifacts/browser/container.log 2>&1 || true
|
docker logs amare-web > artifacts/browser/container.log 2>&1 || true
|
||||||
cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true
|
cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true
|
||||||
cp -R tests/Browser/Screenshots artifacts/browser/screenshots 2>/dev/null || true
|
|
||||||
cp -R tests/.pest artifacts/browser/pest 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Upload browser failure artifacts
|
- name: Upload browser failure artifacts
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,7 +18,6 @@
|
|||||||
/public/fonts-manifest.dev.json
|
/public/fonts-manifest.dev.json
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/tests/Browser/Screenshots
|
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
@@ -27,3 +26,6 @@ Homestead.json
|
|||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Pest Browser visual diffs (local/CI artifacts)
|
||||||
|
/tests/Browser/Screenshots/
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ Hooks live in `.husky/` and auto-install on any plain `npm install` via the `pre
|
|||||||
|
|
||||||
Follow PSR-4 and Laravel conventions: PascalCase classes, camelCase methods, and snake_case database columns. Use four spaces (two in YAML, except four in Compose files), LF endings, and UTF-8 as defined by `.editorconfig`. Every project-owned PHP file must place `declare(strict_types=1);` immediately after `<?php`. Keep domain code independent of Filament and Livewire. Run `composer pint` to format and `composer phpstan` before review.
|
Follow PSR-4 and Laravel conventions: PascalCase classes, camelCase methods, and snake_case database columns. Use four spaces (two in YAML, except four in Compose files), LF endings, and UTF-8 as defined by `.editorconfig`. Every project-owned PHP file must place `declare(strict_types=1);` immediately after `<?php`. Keep domain code independent of Filament and Livewire. Run `composer pint` to format and `composer phpstan` before review.
|
||||||
|
|
||||||
|
## Design Principles: DRY & YAGNI
|
||||||
|
|
||||||
|
Write for the problem at hand, not an imagined future. **DRY**: extract and reuse a piece of logic as soon as it is genuinely duplicated in more than one place — but not before. **YAGNI**: do not add abstraction, configurability, or layers speculatively; add them only when a concrete requirement demands it. Prefer the simplest thing that solves the current requirement. Duplication that appears once is not yet a reason to abstract — wait for a second real occurrence before generalizing. This repo already encodes YAGNI in `openspec/config.yaml` (no generic repositories / `BaseService`); keep that spirit in new code. Avoid over-engineering and avoid premature extraction.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Name files by behavior, ending in `Test.php`, and add tests in the suite matching the changed layer. Feature tests use `RefreshDatabase`. Add architecture coverage for dependency-boundary changes. No numeric coverage threshold is enforced, but changed behavior must have regression coverage.
|
Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Tests are verification, not a design driver — write them to cover behavior you've already implemented, matching the layer you changed. Name files by behavior, ending in `Test.php`, and add tests in the suite matching the changed layer. Feature tests use `RefreshDatabase`. Add architecture coverage for dependency-boundary changes. No numeric coverage threshold is enforced; add regression tests where a bug was fixed or behavior is non-obvious, without making tests a front-loaded design ceremony.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
|||||||
17
CLAUDE.md
17
CLAUDE.md
@@ -14,6 +14,10 @@ If the import above did not load, read `AGENTS.md` at the repo root now — it i
|
|||||||
- Every project-owned PHP file starts with `declare(strict_types=1);` immediately after `<?php`.
|
- Every project-owned PHP file starts with `declare(strict_types=1);` immediately after `<?php`.
|
||||||
- Browser tests are CI-only (they run against a FrankenPHP container built by `docker build`, not `artisan serve`).
|
- Browser tests are CI-only (they run against a FrankenPHP container built by `docker build`, not `artisan serve`).
|
||||||
|
|
||||||
|
## Design principles: DRY & YAGNI (not TDD)
|
||||||
|
|
||||||
|
Write for the problem at hand, not an imagined future. **DRY**: extract and reuse only once logic is genuinely duplicated in more than one place. **YAGNI**: no speculative abstraction, configurability, or layers — add them only when a concrete requirement demands it. Prefer the simplest thing that solves the current requirement; avoid over-engineering and premature extraction. Tests are verification, not a design driver: write them to cover behavior already implemented, not as a front-loaded TDD ceremony.
|
||||||
|
|
||||||
## Environment note
|
## Environment note
|
||||||
|
|
||||||
PHP and Composer are **not on PATH** in this environment, and `vendor/` and `node_modules/` are absent. Every `composer …` / `php artisan …` command in `AGENTS.md` and `README.md` assumes a PHP 8.4+ runtime with Composer 2 installed. Verify the toolchain before promising a command ran.
|
PHP and Composer are **not on PATH** in this environment, and `vendor/` and `node_modules/` are absent. Every `composer …` / `php artisan …` command in `AGENTS.md` and `README.md` assumes a PHP 8.4+ runtime with Composer 2 installed. Verify the toolchain before promising a command ran.
|
||||||
@@ -53,18 +57,9 @@ Site-wide content is a singleton row reached via `SiteSetting::instance()`. Publ
|
|||||||
|
|
||||||
`App\Application` is **not** covered by that rule. `app/Domain/` currently holds a single placeholder (`DomainModule.php`); business reads live in `app/Application/Queries`. Extend the arch test when you add a boundary.
|
`App\Application` is **not** covered by that rule. `app/Domain/` currently holds a single placeholder (`DomainModule.php`); business reads live in `app/Application/Queries`. Extend the arch test when you add a boundary.
|
||||||
|
|
||||||
## Visual regression — read before touching baselines
|
## Deterministic test support
|
||||||
|
|
||||||
- Baselines are committed `.snap` files under `tests/.pest/snapshots/Browser/VisualRegressionTest/`.
|
`APP_FROZEN_NOW` configures `CarbonImmutable::setTestNow()` through `AppServiceProvider::freezeClockWhenConfigured()` outside production. `VisualContentSeeder` provides deterministic image/content fixtures for tests that explicitly need them. Neither setting is a browser-CI global default.
|
||||||
- `tests/Browser/Screenshots/` is gitignored — it only holds diff output.
|
|
||||||
- Regenerate with `composer visual:update`.
|
|
||||||
- **Baselines are CI-parity artifacts.** CI runs the browser suite against a `docker build`-produced FrankenPHP container (see the `browser` job in `.github/workflows/ci.yml`), so baselines regenerated on macOS against a local server will be rejected by CI. Commit `4578457` exists because of this.
|
|
||||||
|
|
||||||
Determinism relies on three cooperating pieces:
|
|
||||||
|
|
||||||
- `APP_FROZEN_NOW` → `CarbonImmutable::setTestNow()` in `AppServiceProvider::freezeClockWhenConfigured()` (no-op in production).
|
|
||||||
- `Database\Seeders\VisualContentSeeder::FROZEN_NOW` — the value the browser tests and the CI job both pin to.
|
|
||||||
- `Tests\Support\StableScreenshot` — forces Arial, disables transitions/animations, scrolls the page to settle lazy images, and avoids the flaky `networkidle` wait.
|
|
||||||
|
|
||||||
## Other things that bite
|
## Other things that bite
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Estrutura base usa grade central de 12 colunas no desktop e 4 no mobile, com lar
|
|||||||
|
|
||||||
Composições editoriais alternam colunas assimétricas, imagens em diferentes proporções e blocos de leitura curtos. Formulários, listas e navegação mantêm alinhamento estrito. Ritmo estrutural usa múltiplos de `8px`; ajustes tipográficos usam `4px`.
|
Composições editoriais alternam colunas assimétricas, imagens em diferentes proporções e blocos de leitura curtos. Formulários, listas e navegação mantêm alinhamento estrito. Ritmo estrutural usa múltiplos de `8px`; ajustes tipográficos usam `4px`.
|
||||||
|
|
||||||
Desktop aceita spreads, conteúdo deslocado e relações 5/7 ou 4/5. Mobile lineariza ordem sem esconder conteúdo essencial, mantém CTA acessível e troca spreads por sequências verticais. Fotografia pode ocupar viewport amplo, desde que texto e ação permaneçam legíveis.
|
Desktop aceita spreads, conteúdo deslocado e relações 5/7 ou 4/5. Nas aberturas narrativas com fotografia, o spread padrão reserva 5/12 para o texto e 7/12 para a mídia; a coluna de leitura não passa de 470px, e o título mantém quebra normal. Mobile lineariza a ordem para texto seguido da fotografia sem esconder conteúdo essencial e mantém CTA acessível. Fotografia pode ocupar viewport amplo, desde que texto e ação permaneçam legíveis.
|
||||||
|
|
||||||
**The Editorial Rhythm Rule.** Seções alternam imagem, texto, densidade e silêncio; repetição de grades idênticas por toda página é proibida.
|
**The Editorial Rhythm Rule.** Seções alternam imagem, texto, densidade e silêncio; repetição de grades idênticas por toda página é proibida.
|
||||||
|
|
||||||
|
|||||||
28
PRODUCT.md
28
PRODUCT.md
@@ -14,36 +14,42 @@ web
|
|||||||
|
|
||||||
## Product Purpose
|
## Product Purpose
|
||||||
|
|
||||||
A Amare Assessoria usa uma única plataforma para apresentar sua marca, converter visitantes em oportunidades e conduzir a operação dos eventos. O produto deve reduzir planilhas e informações dispersas, preservar o fluxo de trabalho da equipe e deixar claro o que exige atenção.
|
No recorte de lançamento, a Amare Assessoria usa o site institucional e seu CMS para apresentar a marca, publicar conteúdo e converter visitantes em pedidos de proposta por e-mail. O produto futuro continua planejado como uma única plataforma para conduzir leads e a operação dos eventos, mas essa evolução não faz parte do lançamento de 31/08/2026.
|
||||||
|
|
||||||
Sucesso significa:
|
Sucesso no lançamento significa:
|
||||||
|
|
||||||
- visitantes qualificados concluírem o briefing;
|
- visitantes qualificados concluírem o briefing;
|
||||||
|
- a assessora atualizar os conteúdos essenciais sem depender de desenvolvimento;
|
||||||
|
- o site equilibrar eventos sociais e corporativos com conteúdo real e autorizado;
|
||||||
|
- acessibilidade, desempenho, jornadas críticas e deploy serem verificados antes da publicação.
|
||||||
|
|
||||||
|
Nas fases futuras, sucesso também significará:
|
||||||
|
|
||||||
- leads avançarem pelo pipeline sem depender de planilhas externas;
|
- leads avançarem pelo pipeline sem depender de planilhas externas;
|
||||||
- leads conquistados virarem eventos sem redigitação;
|
- leads conquistados virarem eventos sem redigitação;
|
||||||
- pendências reais do dia aparecerem com clareza;
|
- pendências reais do dia aparecerem com clareza;
|
||||||
- tarefas, fornecedores, orçamento, pagamentos manuais e documentos permanecerem organizados por evento;
|
- tarefas, fornecedores, orçamento, pagamentos manuais e documentos permanecerem organizados por evento.
|
||||||
- jornadas críticas, acessibilidade e qualidade visual serem verificadas antes de cada deploy.
|
|
||||||
|
|
||||||
## Positioning
|
## Positioning
|
||||||
|
|
||||||
Site público premium e operação interna formam um fluxo único. A mesma plataforma que transmite profissionalismo e capta a demanda transforma o lead em evento e orienta a equipe pelas pendências e exceções que precisam de ação, sem exigir troca radical de processo.
|
No lançamento, o site público premium e o CMS interno formam a superfície entregue: a mesma aplicação transmite profissionalismo, capta a demanda por e-mail e permite manter o conteúdo essencial. A operação interna completa permanece como direção futura para transformar leads em eventos e orientar a equipe pelas pendências e exceções que precisam de ação, sem exigir troca radical de processo.
|
||||||
|
|
||||||
## Operating Context
|
## Operating Context
|
||||||
|
|
||||||
- Atuação em São Paulo, capital.
|
- Atuação em São Paulo, capital.
|
||||||
- Serviços: casamentos, eventos corporativos e celebrações.
|
- Serviços: casamentos, eventos corporativos e celebrações.
|
||||||
- Operação realizada por assessora principal e equipe pequena.
|
- Operação realizada por assessora principal e equipe pequena.
|
||||||
- Jornada prevista: descoberta no site, briefing, qualificação do lead, pipeline comercial, conversão em evento, checklist, fornecedores, orçamento, pagamentos manuais e documentos.
|
- Jornada de lançamento: descoberta no site, briefing por e-mail e contato comercial fora da plataforma.
|
||||||
|
- Jornada futura prevista: qualificação do lead, pipeline comercial, conversão em evento, checklist, fornecedores, orçamento, pagamentos manuais e documentos.
|
||||||
- Interface pública em português do Brasil; dados financeiros em reais.
|
- Interface pública em português do Brasil; dados financeiros em reais.
|
||||||
- Área interna acessível por administradora e assistentes com papéis simples.
|
- Área interna acessível por administradora e assistentes com papéis simples.
|
||||||
|
|
||||||
## Capabilities and Constraints
|
## Capabilities and Constraints
|
||||||
|
|
||||||
- Produto single-tenant para a operação da Amare; não é SaaS para várias assessorias.
|
- Produto single-tenant para a operação da Amare; não é SaaS para várias assessorias.
|
||||||
- Duas superfícies integradas: site público responsivo com CMS e área interna operacional em Filament.
|
- O lançamento inclui site público responsivo, CMS em Filament, briefing enviado por e-mail sem persistência de Lead, usuários internos simples, SEO básico, acessibilidade automatizada e deploy em contêiner.
|
||||||
- MVP inclui briefing, CRM de leads, conversão em evento, tarefas, fornecedores, orçamento, pagamentos manuais, documentos, dashboard orientado a exceções, notificações, auditoria, SEO básico e deploy automatizado.
|
- CRM de leads, conversão em evento, tarefas, fornecedores, orçamento, pagamentos manuais, documentos, dashboard orientado a exceções, notificações e auditoria permanecem especificados para as Fases 2 a 5, adiadas sem data pela ADR-016.
|
||||||
- Implementação atual cobre fundação, site público e CMS; módulos operacionais posteriores ainda fazem parte do escopo planejado.
|
- O produto futuro mantém duas superfícies integradas: site público responsivo com CMS e área interna operacional em Filament; no lançamento, a área interna cobre apenas CMS, configurações e usuários.
|
||||||
- Não inclui portal ou login para cliente final, aplicativo nativo, pagamentos online, RSVP, gestão de convidados, assinatura eletrônica, integração oficial com WhatsApp, multi-tenancy ou PWA offline avançada.
|
- Não inclui portal ou login para cliente final, aplicativo nativo, pagamentos online, RSVP, gestão de convidados, assinatura eletrônica, integração oficial com WhatsApp, multi-tenancy ou PWA offline avançada.
|
||||||
- Contas internas usam os papéis `admin` e `assistant`; permissões granulares ficam fora do MVP.
|
- Contas internas usam os papéis `admin` e `assistant`; permissões granulares ficam fora do MVP.
|
||||||
- Fatos ainda em aberto: processo real de qualificação comercial, política de retenção LGPD para leads perdidos, canais oficiais, domínio e infraestrutura de produção.
|
- Fatos ainda em aberto: processo real de qualificação comercial, política de retenção LGPD para leads perdidos, canais oficiais, domínio e infraestrutura de produção.
|
||||||
@@ -58,7 +64,7 @@ Site público premium e operação interna formam um fluxo único. A mesma plata
|
|||||||
## Evidence on Hand
|
## Evidence on Hand
|
||||||
|
|
||||||
- `SPEC.md` define escopo, personas, jornadas, métricas, requisitos e decisões do MVP.
|
- `SPEC.md` define escopo, personas, jornadas, métricas, requisitos e decisões do MVP.
|
||||||
- Código atual contém site público, CMS, estrutura de acessibilidade, testes visuais e conteúdo demonstrativo.
|
- Código atual contém site público, CMS, estrutura de acessibilidade, testes browser e conteúdo demonstrativo.
|
||||||
- Textos, fotografias, portfólio, depoimentos, contatos e perfis sociais presentes nos seeders são dados fictícios de desenvolvimento e não constituem prova comercial.
|
- Textos, fotografias, portfólio, depoimentos, contatos e perfis sociais presentes nos seeders são dados fictícios de desenvolvimento e não constituem prova comercial.
|
||||||
- Ainda não há logo, fotografia proprietária, depoimentos autorizados, cases reais, imprensa, credenciais, CNPJ ou texto jurídico definitivo confirmados no repositório. Trabalho futuro não deve inventá-los.
|
- Ainda não há logo, fotografia proprietária, depoimentos autorizados, cases reais, imprensa, credenciais, CNPJ ou texto jurídico definitivo confirmados no repositório. Trabalho futuro não deve inventá-los.
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ Site público premium e operação interna formam um fluxo único. A mesma plata
|
|||||||
2. **Uma informação, um fluxo:** conduzir o dado do briefing ao evento sem redigitação ou planilhas paralelas.
|
2. **Uma informação, um fluxo:** conduzir o dado do briefing ao evento sem redigitação ou planilhas paralelas.
|
||||||
3. **Adoção sem ruptura:** organizar o trabalho existente da equipe sem impor processo desnecessariamente complexo.
|
3. **Adoção sem ruptura:** organizar o trabalho existente da equipe sem impor processo desnecessariamente complexo.
|
||||||
4. **Confiança sustentada por fatos:** usar somente conteúdo, resultados e provas reais e autorizados.
|
4. **Confiança sustentada por fatos:** usar somente conteúdo, resultados e provas reais e autorizados.
|
||||||
5. **Qualidade verificável:** tratar acessibilidade, desempenho, testes visuais e jornadas críticas como critérios de entrega.
|
5. **Qualidade verificável:** tratar acessibilidade, desempenho, motion e jornadas críticas como critérios de entrega.
|
||||||
|
|
||||||
## Accessibility & Inclusion
|
## Accessibility & Inclusion
|
||||||
|
|
||||||
|
|||||||
65
SPEC.md
65
SPEC.md
@@ -23,7 +23,7 @@
|
|||||||
| Área pública | Blade + JS vanilla progressivo (Livewire é dependência do Filament, não usada no site público — ver ADR-015) |
|
| Área pública | Blade + JS vanilla progressivo (Livewire é dependência do Filament, não usada no site público — ver ADR-015) |
|
||||||
| Servidor de aplicação | FrankenPHP em modo regular |
|
| Servidor de aplicação | FrankenPHP em modo regular |
|
||||||
| Banco de dados | PostgreSQL |
|
| Banco de dados | PostgreSQL |
|
||||||
| Testes | Pest, Pest Browser/Playwright e testes visuais |
|
| Testes | Pest e Pest Browser/Playwright |
|
||||||
|
|
||||||
### 0.1 Vocabulário normativo
|
### 0.1 Vocabulário normativo
|
||||||
|
|
||||||
@@ -80,7 +80,6 @@ O agente:
|
|||||||
- NÃO DEVE colocar regras financeiras diretamente em views, Resources, Models observers ou callbacks de formulário.
|
- NÃO DEVE colocar regras financeiras diretamente em views, Resources, Models observers ou callbacks de formulário.
|
||||||
- NÃO DEVE usar `float` para dinheiro.
|
- NÃO DEVE usar `float` para dinheiro.
|
||||||
- NÃO DEVE persistir status derivados que possam ser calculados corretamente a partir dos dados fonte.
|
- NÃO DEVE persistir status derivados que possam ser calculados corretamente a partir dos dados fonte.
|
||||||
- NÃO DEVE atualizar snapshots visuais apenas para fazer o CI passar sem revisar o diff.
|
|
||||||
|
|
||||||
### 1.2 Entrega incremental
|
### 1.2 Entrega incremental
|
||||||
|
|
||||||
@@ -135,7 +134,6 @@ Uma assessora adotará o sistema quando ele:
|
|||||||
| Operação | Tarefas vencidas sem responsável | `0` |
|
| Operação | Tarefas vencidas sem responsável | `0` |
|
||||||
| Financeiro | Itens de orçamento sem valor ou status | Menos de 5% por evento ativo |
|
| Financeiro | Itens de orçamento sem valor ou status | Menos de 5% por evento ativo |
|
||||||
| Qualidade | Jornadas E2E críticas passando | 100% antes de deploy |
|
| Qualidade | Jornadas E2E críticas passando | 100% antes de deploy |
|
||||||
| Visual | Snapshots aprovados | 100% |
|
|
||||||
| Confiabilidade | Erros não tratados | Alerta imediato e tendência decrescente |
|
| Confiabilidade | Erros não tratados | Alerta imediato e tendência decrescente |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -207,7 +205,7 @@ No recorte do lançamento (Fases 0–1):
|
|||||||
- formulário de briefing;
|
- formulário de briefing;
|
||||||
- usuários internos e papéis simples;
|
- usuários internos e papéis simples;
|
||||||
- SEO básico;
|
- SEO básico;
|
||||||
- acessibilidade e testes visuais;
|
- acessibilidade automatizada;
|
||||||
- CI/CD e deploy em contêiner com FrankenPHP.
|
- CI/CD e deploy em contêiner com FrankenPHP.
|
||||||
|
|
||||||
Adiados para depois do lançamento (Fases 2–5, ver ADR-016):
|
Adiados para depois do lançamento (Fases 2–5, ver ADR-016):
|
||||||
@@ -1901,7 +1899,7 @@ A pirâmide deve possuir:
|
|||||||
1. muitos testes unitários rápidos;
|
1. muitos testes unitários rápidos;
|
||||||
2. testes feature/integration suficientes para Laravel, PostgreSQL, Livewire e Filament;
|
2. testes feature/integration suficientes para Laravel, PostgreSQL, Livewire e Filament;
|
||||||
3. poucos testes E2E cobrindo jornadas críticas;
|
3. poucos testes E2E cobrindo jornadas críticas;
|
||||||
4. testes visuais determinísticos nas telas mais importantes.
|
4. testes E2E, de acessibilidade, motion e smoke nas jornadas públicas mais importantes.
|
||||||
|
|
||||||
A distribuição é por intenção, não por percentual rígido.
|
A distribuição é por intenção, não por percentual rígido.
|
||||||
|
|
||||||
@@ -1983,40 +1981,7 @@ Em falha, CI deve publicar:
|
|||||||
- logs do browser;
|
- logs do browser;
|
||||||
- HTML report quando disponível.
|
- HTML report quando disponível.
|
||||||
|
|
||||||
### 13.5 Regressão visual
|
### 13.5 Testes de arquitetura
|
||||||
|
|
||||||
Usar `assertScreenshotMatches()` ou API equivalente do Pest Browser.
|
|
||||||
|
|
||||||
Snapshots obrigatórios:
|
|
||||||
|
|
||||||
| Tela | Desktop | Mobile |
|
|
||||||
|---|---:|---:|
|
|
||||||
| Home | 1440×1000 | 390×844 |
|
|
||||||
| Serviços | 1440×1000 | 390×844 |
|
|
||||||
| Portfólio | 1440×1000 | 390×844 |
|
|
||||||
| Detalhe do portfólio | 1440×1000 | 390×844 |
|
|
||||||
| Briefing vazio | 1280×900 | 390×844 |
|
|
||||||
| Briefing com erros | 1280×900 | 390×844 |
|
|
||||||
| Briefing sucesso | 1280×900 | 390×844 |
|
|
||||||
| Login | 1280×900 | Opcional |
|
|
||||||
| Dashboard | 1440×1000 | Não obrigatório |
|
|
||||||
| Detalhe do evento | 1440×1000 | Não obrigatório |
|
|
||||||
|
|
||||||
Determinismo obrigatório:
|
|
||||||
|
|
||||||
- Chromium e imagem Linux fixos;
|
|
||||||
- viewport fixo;
|
|
||||||
- timezone `America/Sao_Paulo`;
|
|
||||||
- locale `pt-BR`;
|
|
||||||
- fontes instaladas na imagem;
|
|
||||||
- relógio congelado;
|
|
||||||
- seed determinístico;
|
|
||||||
- animações e transições desabilitadas;
|
|
||||||
- dados dinâmicos mascarados quando necessário.
|
|
||||||
|
|
||||||
Atualização de baseline deve usar comando explícito e revisão humana do diff.
|
|
||||||
|
|
||||||
### 13.6 Testes de arquitetura
|
|
||||||
|
|
||||||
Criar regras Pest Architecture:
|
Criar regras Pest Architecture:
|
||||||
|
|
||||||
@@ -2033,14 +1998,14 @@ arch()
|
|||||||
|
|
||||||
Adicionar regras conforme a estrutura real, sem tornar a suíte excessivamente frágil.
|
Adicionar regras conforme a estrutura real, sem tornar a suíte excessivamente frágil.
|
||||||
|
|
||||||
### 13.7 Cobertura
|
### 13.6 Cobertura
|
||||||
|
|
||||||
- meta de 80% para `Domain` e `Application`;
|
- meta de 80% para `Domain` e `Application`;
|
||||||
- não medir views, migrations, código gerado e framework;
|
- não medir views, migrations, código gerado e framework;
|
||||||
- cobertura não substitui critérios de aceite;
|
- cobertura não substitui critérios de aceite;
|
||||||
- queda de cobertura em módulo crítico deve bloquear merge.
|
- queda de cobertura em módulo crítico deve bloquear merge.
|
||||||
|
|
||||||
### 13.8 Acessibilidade automatizada
|
### 13.7 Acessibilidade automatizada
|
||||||
|
|
||||||
Smoke/browser tests devem verificar:
|
Smoke/browser tests devem verificar:
|
||||||
|
|
||||||
@@ -2058,7 +2023,7 @@ Revisão manual mínima antes do lançamento:
|
|||||||
- zoom a 200%;
|
- zoom a 200%;
|
||||||
- leitor de tela nos fluxos de briefing e login.
|
- leitor de tela nos fluxos de briefing e login.
|
||||||
|
|
||||||
### 13.9 Comandos padronizados
|
### 13.8 Comandos padronizados
|
||||||
|
|
||||||
O projeto deve expor scripts equivalentes:
|
O projeto deve expor scripts equivalentes:
|
||||||
|
|
||||||
@@ -2068,7 +2033,6 @@ composer test:feature
|
|||||||
composer test:browser
|
composer test:browser
|
||||||
composer test
|
composer test
|
||||||
composer quality
|
composer quality
|
||||||
composer visual:update
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Composição esperada:
|
Composição esperada:
|
||||||
@@ -2076,7 +2040,7 @@ Composição esperada:
|
|||||||
```text
|
```text
|
||||||
test:unit → Unit + Architecture
|
test:unit → Unit + Architecture
|
||||||
test:feature → Feature + Livewire + Filament
|
test:feature → Feature + Livewire + Filament
|
||||||
test:browser → E2E + visual + acessibilidade + smoke
|
test:browser → E2E + acessibilidade + motion + smoke
|
||||||
quality → Pint check + PHPStan/Larastan + audits + testes
|
quality → Pint check + PHPStan/Larastan + audits + testes
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2091,7 +2055,7 @@ quality → Pint check + PHPStan/Larastan + audits + testes
|
|||||||
| `static` | Pint, PHPStan/Larastan, Composer validate, Composer e npm audit | Sim |
|
| `static` | Pint, PHPStan/Larastan, Composer validate, Composer e npm audit | Sim |
|
||||||
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
||||||
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
||||||
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e visual | Sim |
|
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e motion | Sim |
|
||||||
| `container` | Build da imagem final e healthcheck | Sim |
|
| `container` | Build da imagem final e healthcheck | Sim |
|
||||||
|
|
||||||
### 14.2 Regras do pipeline
|
### 14.2 Regras do pipeline
|
||||||
@@ -2125,7 +2089,6 @@ Uma história só está concluída quando:
|
|||||||
- PHPStan/Larastan está verde;
|
- PHPStan/Larastan está verde;
|
||||||
- unit e feature tests cobrem regras relevantes;
|
- unit e feature tests cobrem regras relevantes;
|
||||||
- E2E foi criado ou atualizado para fluxo crítico;
|
- E2E foi criado ou atualizado para fluxo crítico;
|
||||||
- snapshot foi criado ou revisado para UI coberta;
|
|
||||||
- acessibilidade automatizada não possui issues críticas/sérias;
|
- acessibilidade automatizada não possui issues críticas/sérias;
|
||||||
- migration foi testada;
|
- migration foi testada;
|
||||||
- autorização foi verificada;
|
- autorização foi verificada;
|
||||||
@@ -2383,10 +2346,9 @@ O agente deve implementar na sequência, salvo instrução explícita.
|
|||||||
- [x] privacidade;
|
- [x] privacidade;
|
||||||
- [x] SEO;
|
- [x] SEO;
|
||||||
- [x] mídia otimizada;
|
- [x] mídia otimizada;
|
||||||
- [x] snapshots desktop/mobile;
|
|
||||||
- [x] testes de acessibilidade.
|
- [x] testes de acessibilidade.
|
||||||
|
|
||||||
**Critério de saída:** conteúdo gerenciável no Filament e site público aprovado visualmente (baselines em `tests/.pest/snapshots/`; aprovação humana do diff visual no PR).
|
**Critério de saída:** conteúdo gerenciável no Filament, testes browser funcionais, de acessibilidade e motion aprovados, e revisão humana do site público no PR.
|
||||||
|
|
||||||
### Fase 2 — Leads — ADIADA (ADR-016)
|
### Fase 2 — Leads — ADIADA (ADR-016)
|
||||||
|
|
||||||
@@ -2497,8 +2459,7 @@ Toda operação financeira deve:
|
|||||||
| Risco | Mitigação obrigatória |
|
| Risco | Mitigação obrigatória |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Filament concentrar domínio | Resources finos, Actions e Queries testáveis |
|
| Filament concentrar domínio | Resources finos, Actions e Queries testáveis |
|
||||||
| Site parecer genérico | design tokens, conteúdo real, fotografia e visual tests |
|
| Site parecer genérico | design tokens, conteúdo real, fotografia e revisão humana |
|
||||||
| Snapshots instáveis | contêiner fixo, relógio/dados/fontes determinísticos |
|
|
||||||
| Escopo crescer | lista de não objetivos e mudança somente com hipótese real |
|
| Escopo crescer | lista de não objetivos e mudança somente com hipótese real |
|
||||||
| Financeiro virar contabilidade | limitar a orçamento e pagamentos manuais |
|
| Financeiro virar contabilidade | limitar a orçamento e pagamentos manuais |
|
||||||
| Worker mode vazar estado | manter modo regular até benchmark e ADR |
|
| Worker mode vazar estado | manter modo regular até benchmark e ADR |
|
||||||
@@ -2517,7 +2478,7 @@ Toda operação financeira deve:
|
|||||||
| ADR-002 | Filament para área interna (Livewire é dependência interna do Filament); site público em Blade — ver ADR-015 | Aceita |
|
| ADR-002 | Filament para área interna (Livewire é dependência interna do Filament); site público em Blade — ver ADR-015 | Aceita |
|
||||||
| ADR-003 | PostgreSQL como único banco transacional | Aceita |
|
| ADR-003 | PostgreSQL como único banco transacional | Aceita |
|
||||||
| ADR-004 | Pagamentos somente manuais | Aceita |
|
| ADR-004 | Pagamentos somente manuais | Aceita |
|
||||||
| ADR-005 | Pest unifica unit, feature, browser e visual | Aceita |
|
| ADR-005 | Pest unifica unit, feature e browser | Aceita |
|
||||||
| ADR-006 | FrankenPHP regular mode; worker mode adiado | Aceita |
|
| ADR-006 | FrankenPHP regular mode; worker mode adiado | Aceita |
|
||||||
| ADR-007 | Single-tenant; SaaS e portal do cliente adiados | Aceita |
|
| ADR-007 | Single-tenant; SaaS e portal do cliente adiados | Aceita |
|
||||||
| ADR-008 | Database queue; Redis adiado | Aceita |
|
| ADR-008 | Database queue; Redis adiado | Aceita |
|
||||||
@@ -2563,7 +2524,7 @@ O lançamento está concluído somente quando:
|
|||||||
- assessora edita os conteúdos essenciais sem desenvolvedor;
|
- assessora edita os conteúdos essenciais sem desenvolvedor;
|
||||||
- briefing envia o pedido de proposta de forma segura, com proteção contra abuso e aceite de privacidade registrado;
|
- briefing envia o pedido de proposta de forma segura, com proteção contra abuso e aceite de privacidade registrado;
|
||||||
- usuários e Policies estão corretos;
|
- usuários e Policies estão corretos;
|
||||||
- testes unit, feature, visuais e arquitetura estão verdes;
|
- testes unit, feature, browser funcionais e arquitetura estão verdes;
|
||||||
- CI bloqueia regressões;
|
- CI bloqueia regressões;
|
||||||
- imagem FrankenPHP é reproduzível;
|
- imagem FrankenPHP é reproduzível;
|
||||||
- staging e produção usam a mesma imagem promovida;
|
- staging e produção usam a mesma imagem promovida;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
final readonly class HomeContent
|
final readonly class HomeContent
|
||||||
@@ -16,11 +17,13 @@ final readonly class HomeContent
|
|||||||
* @param Collection<int, Service> $featuredServices
|
* @param Collection<int, Service> $featuredServices
|
||||||
* @param Collection<int, PortfolioCase> $featuredCases
|
* @param Collection<int, PortfolioCase> $featuredCases
|
||||||
* @param Collection<int, Testimonial> $testimonials
|
* @param Collection<int, Testimonial> $testimonials
|
||||||
|
* @param Collection<int, WeddingPackage> $packages
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public SiteSetting $settings,
|
public SiteSetting $settings,
|
||||||
public Collection $featuredServices,
|
public Collection $featuredServices,
|
||||||
public Collection $featuredCases,
|
public Collection $featuredCases,
|
||||||
public Collection $testimonials,
|
public Collection $testimonials,
|
||||||
|
public Collection $packages,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Application\Data;
|
|||||||
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
final readonly class PageMeta
|
final readonly class PageMeta
|
||||||
@@ -84,6 +85,43 @@ final readonly class PageMeta
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed>|null $jsonLd
|
||||||
|
*/
|
||||||
|
public static function forPackage(
|
||||||
|
WeddingPackage $package,
|
||||||
|
string $canonical,
|
||||||
|
SiteSetting $settings,
|
||||||
|
?array $jsonLd = null,
|
||||||
|
): self {
|
||||||
|
$title = filled($package->meta_title)
|
||||||
|
? (string) $package->meta_title
|
||||||
|
: (filled($package->title_line) ? (string) $package->title_line : (string) $package->name);
|
||||||
|
|
||||||
|
$description = filled($package->meta_description)
|
||||||
|
? (string) $package->meta_description
|
||||||
|
: (filled($package->summary) ? (string) $package->summary : self::defaultDescription($settings));
|
||||||
|
|
||||||
|
$ogImageUrl = filled($package->hero_image_path)
|
||||||
|
? url(Storage::disk('public')->url((string) $package->hero_image_path))
|
||||||
|
: self::defaultOgImageUrl($settings);
|
||||||
|
|
||||||
|
$ogImageAlt = filled($package->hero_image_alt)
|
||||||
|
? (string) $package->hero_image_alt
|
||||||
|
: $settings->default_og_image_alt;
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
title: $title,
|
||||||
|
description: $description,
|
||||||
|
canonical: $canonical,
|
||||||
|
ogType: 'article',
|
||||||
|
ogImageUrl: $ogImageUrl,
|
||||||
|
ogImageAlt: $ogImageAlt,
|
||||||
|
jsonLd: $jsonLd,
|
||||||
|
siteName: (string) $settings->brand_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the metadata for branded error pages.
|
* Build the metadata for branded error pages.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Application\Queries\Marketing;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
|
||||||
|
final class FindPublishedWeddingPackageBySlug
|
||||||
|
{
|
||||||
|
public function __invoke(string $slug): ?WeddingPackage
|
||||||
|
{
|
||||||
|
return WeddingPackage::query()
|
||||||
|
->published()
|
||||||
|
->where('slug', $slug)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
|
||||||
final class GetHomeContent
|
final class GetHomeContent
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,10 @@ final class GetHomeContent
|
|||||||
->published()
|
->published()
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->get(),
|
->get(),
|
||||||
|
packages: WeddingPackage::query()
|
||||||
|
->published()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Application\Queries\Marketing;
|
namespace App\Application\Queries\Marketing;
|
||||||
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
final class GetSitemapEntries
|
final class GetSitemapEntries
|
||||||
@@ -38,6 +39,21 @@ final class GetSitemapEntries
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$packages = WeddingPackage::query()
|
||||||
|
->published()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get(['slug', 'updated_at']);
|
||||||
|
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
/** @var Carbon|null $updatedAt */
|
||||||
|
$updatedAt = $package->updated_at;
|
||||||
|
|
||||||
|
$entries[] = [
|
||||||
|
'loc' => route('packages.show', $package->slug),
|
||||||
|
'lastmod' => $updatedAt?->toAtomString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return $entries;
|
return $entries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
|
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (! Storage::disk($disk)->exists($path)) {
|
if (! Storage::disk($disk)->exists($path)) {
|
||||||
|
ResponsiveImage::forgetMetadata($path, $disk);
|
||||||
$this->warn("Missing file: {$path}");
|
$this->warn("Missing file: {$path}");
|
||||||
$skipped++;
|
$skipped++;
|
||||||
|
|
||||||
@@ -56,9 +57,21 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
if ($settings && filled($settings->default_og_image_path)) {
|
if ($settings && filled($settings->default_og_image_path)) {
|
||||||
$paths[] = (string) $settings->default_og_image_path;
|
$paths[] = (string) $settings->default_og_image_path;
|
||||||
}
|
}
|
||||||
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
|
}
|
||||||
if ($settings && filled($settings->about_image_path)) {
|
if ($settings && filled($settings->about_image_path)) {
|
||||||
$paths[] = (string) $settings->about_image_path;
|
$paths[] = (string) $settings->about_image_path;
|
||||||
}
|
}
|
||||||
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
|
}
|
||||||
|
if ($settings && filled($settings->services_hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->services_hero_image_path;
|
||||||
|
}
|
||||||
|
if ($settings && filled($settings->portfolio_hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->portfolio_hero_image_path;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
||||||
$paths[] = (string) $path;
|
$paths[] = (string) $path;
|
||||||
|
|||||||
49
app/Domain/Marketing/PackageIconCatalog.php
Normal file
49
app/Domain/Marketing/PackageIconCatalog.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Marketing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closed catalog of icon keys available for wedding modality detail
|
||||||
|
* sections (benefits strip and included items).
|
||||||
|
*
|
||||||
|
* Keys are stable kebab-case identifiers rendered as inline SVGs by the
|
||||||
|
* public Blade components. Admin selection is constrained to this catalog
|
||||||
|
* so arbitrary SVG markup can never reach the public page.
|
||||||
|
*/
|
||||||
|
final class PackageIconCatalog
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, string> map of icon_key => pt-BR label
|
||||||
|
*/
|
||||||
|
public static function labels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'calendar' => 'Calendário',
|
||||||
|
'checklist' => 'Checklist',
|
||||||
|
'users' => 'Casais',
|
||||||
|
'map' => 'Mapa e fornecedores',
|
||||||
|
'heart' => 'Cuidado',
|
||||||
|
'spark' => 'Detalhes',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function keys(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::labels());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isValid(string $iconKey): bool
|
||||||
|
{
|
||||||
|
return isset(self::labels()[$iconKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function labelFor(string $iconKey): ?string
|
||||||
|
{
|
||||||
|
return self::labels()[$iconKey] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,11 +160,24 @@ class ManageSiteSettings extends Page
|
|||||||
Textarea::make('hero_note')
|
Textarea::make('hero_note')
|
||||||
->label('Nota do hero')
|
->label('Nota do hero')
|
||||||
->rows(2),
|
->rows(2),
|
||||||
|
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem do hero', 'content/home'),
|
||||||
|
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
|
||||||
Textarea::make('about_summary')
|
Textarea::make('about_summary')
|
||||||
->label('Resumo institucional')
|
->label('Resumo institucional')
|
||||||
->rows(3),
|
->rows(3),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
|
Section::make('Imagens das aberturas')
|
||||||
|
->description('Fotos reais e autorizadas para as primeiras dobras. Sem imagem, cada rota mantém a abertura tonal.')
|
||||||
|
->schema([
|
||||||
|
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem da home', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
|
||||||
|
PublicImageUploadRules::fileUpload('services_hero_image_path', 'Imagem de Serviços', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('services_hero_image_alt', 'services_hero_image_path'),
|
||||||
|
PublicImageUploadRules::fileUpload('portfolio_hero_image_path', 'Imagem de Portfólio', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('portfolio_hero_image_alt', 'portfolio_hero_image_path'),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
Section::make('Manifesto editorial')
|
Section::make('Manifesto editorial')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('manifesto_title')
|
TextInput::make('manifesto_title')
|
||||||
@@ -199,6 +212,26 @@ class ManageSiteSettings extends Page
|
|||||||
->reorderable()
|
->reorderable()
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
|
Section::make('Corporate')
|
||||||
|
->description('Etapas apresentadas na frente Amare Corporate da home.')
|
||||||
|
->schema([
|
||||||
|
Repeater::make('corporate_steps')
|
||||||
|
->label('Etapas do serviço corporativo')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('title')
|
||||||
|
->label('Título')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('body')
|
||||||
|
->label('Descrição')
|
||||||
|
->required()
|
||||||
|
->rows(2),
|
||||||
|
])
|
||||||
|
->defaultItems(3)
|
||||||
|
->maxItems(6)
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
Section::make('Princípios')
|
Section::make('Princípios')
|
||||||
->schema([
|
->schema([
|
||||||
TagsInput::make('principles')
|
TagsInput::make('principles')
|
||||||
@@ -218,6 +251,11 @@ class ManageSiteSettings extends Page
|
|||||||
->label('Telefone')
|
->label('Telefone')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('whatsapp_number')
|
||||||
|
->label('WhatsApp oficial para as modalidades')
|
||||||
|
->tel()
|
||||||
|
->maxLength(40)
|
||||||
|
->helperText('Use o número com DDD. Sem um número válido, as CTAs levam ao briefing.'),
|
||||||
TextInput::make('city')
|
TextInput::make('city')
|
||||||
->label('Cidade')
|
->label('Cidade')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateWeddingPackage extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditWeddingPackage extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListWeddingPackages extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Schemas;
|
||||||
|
|
||||||
|
use App\Domain\Marketing\PackageIconCatalog;
|
||||||
|
use App\Support\PublicImageUploadRules;
|
||||||
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class WeddingPackageForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Section::make('Identificação')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('name')
|
||||||
|
->label('Nome da modalidade')
|
||||||
|
->required()
|
||||||
|
->maxLength(255)
|
||||||
|
->live(onBlur: true)
|
||||||
|
->afterStateUpdated(function (?string $state, callable $set, callable $get): void {
|
||||||
|
if (blank($get('slug'))) {
|
||||||
|
$set('slug', str($state)->slug()->toString());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
TextInput::make('slug')
|
||||||
|
->label('Slug')
|
||||||
|
->required()
|
||||||
|
->maxLength(255)
|
||||||
|
->unique(ignoreRecord: true),
|
||||||
|
TextInput::make('level')
|
||||||
|
->label('Número / tipo (ex.: 01 / COMPLETA)')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('tag')
|
||||||
|
->label('Etiqueta curta (ex.: Assessoria completa)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('subtitle')
|
||||||
|
->label('Frase de apoio')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('summary')
|
||||||
|
->label('Resumo')
|
||||||
|
->required()
|
||||||
|
->rows(4),
|
||||||
|
Repeater::make('scope_items')
|
||||||
|
->label('O que está incluído')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('item')
|
||||||
|
->label('Item')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
])
|
||||||
|
->defaultItems(4)
|
||||||
|
->minItems(1)
|
||||||
|
->addActionLabel('Adicionar item')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('cta_label')
|
||||||
|
->label('Texto do botão')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('whatsapp_message')
|
||||||
|
->label('Mensagem do WhatsApp (deixe vazio para usar o padrão)')
|
||||||
|
->helperText('Texto enviado ao cliente ao tocar no botão de contato. O nome da modalidade é citado automaticamente se vazio.')
|
||||||
|
->rows(3),
|
||||||
|
TextInput::make('compare_heading')
|
||||||
|
->label('Comparação — título (leitura rápida)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('compare_summary')
|
||||||
|
->label('Comparação — resumo (leitura rápida)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('sort_order')
|
||||||
|
->label('Ordem')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->required(),
|
||||||
|
DateTimePicker::make('published_at')
|
||||||
|
->label('Publicado em')
|
||||||
|
->seconds(false),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
|
Section::make('Hero da página')
|
||||||
|
->description('Dados de abertura da página da modalidade.')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('eyebrow')
|
||||||
|
->label('Eyebrow (ex.: Assessoria)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('title_line')
|
||||||
|
->label('Título (linha principal)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('title_emphasis')
|
||||||
|
->label('Título (ênfase em itálico)')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('hero_lead')
|
||||||
|
->label('Texto de abertura')
|
||||||
|
->rows(3),
|
||||||
|
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem do hero'),
|
||||||
|
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
|
Section::make('Diferenciais')
|
||||||
|
->description('Faixa escura com os valores da modalidade.')
|
||||||
|
->schema([
|
||||||
|
Repeater::make('benefits')
|
||||||
|
->label('Diferenciais')
|
||||||
|
->schema([
|
||||||
|
Select::make('icon_key')
|
||||||
|
->label('Ícone')
|
||||||
|
->options(PackageIconCatalog::labels())
|
||||||
|
->required(),
|
||||||
|
TextInput::make('label')
|
||||||
|
->label('Rótulo')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
])
|
||||||
|
->defaultItems(4)
|
||||||
|
->minItems(1)
|
||||||
|
->addActionLabel('Adicionar diferencial'),
|
||||||
|
])
|
||||||
|
->columns(1),
|
||||||
|
Section::make('O que está incluso')
|
||||||
|
->schema([
|
||||||
|
Repeater::make('included_items')
|
||||||
|
->label('Itens inclusos')
|
||||||
|
->schema([
|
||||||
|
Select::make('icon_key')
|
||||||
|
->label('Ícone')
|
||||||
|
->options(PackageIconCatalog::labels())
|
||||||
|
->required(),
|
||||||
|
TextInput::make('title')
|
||||||
|
->label('Título')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('description')
|
||||||
|
->label('Descrição')
|
||||||
|
->rows(2)
|
||||||
|
->maxLength(500),
|
||||||
|
])
|
||||||
|
->defaultItems(6)
|
||||||
|
->minItems(1)
|
||||||
|
->addActionLabel('Adicionar item'),
|
||||||
|
])
|
||||||
|
->columns(1),
|
||||||
|
Section::make('Para quem é este pacote')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('audience_heading')
|
||||||
|
->label('Título da seção')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('audience_intro')
|
||||||
|
->label('Texto de introdução')
|
||||||
|
->rows(3),
|
||||||
|
Repeater::make('audience_points')
|
||||||
|
->label('Público')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('point')
|
||||||
|
->label('Ponto')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
])
|
||||||
|
->defaultItems(4)
|
||||||
|
->minItems(1)
|
||||||
|
->addActionLabel('Adicionar ponto'),
|
||||||
|
PublicImageUploadRules::fileUpload('audience_image_path', 'Imagem da seção'),
|
||||||
|
PublicImageUploadRules::altTextField('audience_image_alt', 'audience_image_path'),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
|
Section::make('CTA final')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('final_cta_heading')
|
||||||
|
->label('Título do CTA final')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('final_cta_body')
|
||||||
|
->label('Texto do CTA final')
|
||||||
|
->rows(3),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
|
Section::make('SEO')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('meta_title')
|
||||||
|
->label('Meta title')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('meta_description')
|
||||||
|
->label('Meta description')
|
||||||
|
->rows(3),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Tables;
|
||||||
|
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class WeddingPackagesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Nome')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('level')
|
||||||
|
->label('Número / tipo')
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('slug')
|
||||||
|
->label('Slug')
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('published_at')
|
||||||
|
->label('Publicado em')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('sort_order')
|
||||||
|
->label('Ordem')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->defaultSort('sort_order')
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
EditAction::make(),
|
||||||
|
DeleteAction::make()
|
||||||
|
->requiresConfirmation(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\CreateWeddingPackage;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\EditWeddingPackage;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\ListWeddingPackages;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Schemas\WeddingPackageForm;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Tables\WeddingPackagesTable;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use App\Policies\WeddingPackagePolicy;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class WeddingPackageResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = WeddingPackage::class;
|
||||||
|
|
||||||
|
protected static ?string $policy = WeddingPackagePolicy::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Modalidades';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'modalidade';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = 'modalidades';
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Conteúdo do site';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 3;
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return WeddingPackageForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return WeddingPackagesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListWeddingPackages::route('/'),
|
||||||
|
'create' => CreateWeddingPackage::route('/create'),
|
||||||
|
'edit' => EditWeddingPackage::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,6 @@ final class ContactController extends Controller
|
|||||||
|
|
||||||
private function success(): RedirectResponse
|
private function success(): RedirectResponse
|
||||||
{
|
{
|
||||||
return redirect()->route('contact')->with('status', 'briefing-sent');
|
return redirect()->route('briefing')->with('status', 'briefing-sent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
app/Http/Controllers/PublicSite/PackageController.php
Normal file
46
app/Http/Controllers/PublicSite/PackageController.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
|
use App\Application\Data\PageMeta;
|
||||||
|
use App\Application\Queries\Marketing\FindPublishedWeddingPackageBySlug;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
final class PackageController extends Controller
|
||||||
|
{
|
||||||
|
public function show(string $slug, FindPublishedWeddingPackageBySlug $findPublishedWeddingPackageBySlug): View|Response
|
||||||
|
{
|
||||||
|
$package = $findPublishedWeddingPackageBySlug($slug);
|
||||||
|
|
||||||
|
if ($package === null) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
$canonical = route('packages.show', $package->slug);
|
||||||
|
|
||||||
|
return view('pages.packages.show', [
|
||||||
|
'package' => $package,
|
||||||
|
'siteSettings' => $settings,
|
||||||
|
'pageMeta' => PageMeta::forPackage(
|
||||||
|
package: $package,
|
||||||
|
canonical: $canonical,
|
||||||
|
settings: $settings,
|
||||||
|
jsonLd: [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'Article',
|
||||||
|
'headline' => $package->name,
|
||||||
|
'description' => $package->summary,
|
||||||
|
'url' => $canonical,
|
||||||
|
'datePublished' => $package->published_at?->toAtomString(),
|
||||||
|
'dateModified' => $package->updated_at?->toAtomString(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ final class PageController extends Controller
|
|||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
return view('pages.contact', [
|
return view('pages.partners', [
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('contact'),
|
canonical: route('contact'),
|
||||||
@@ -55,4 +55,14 @@ final class PageController extends Controller
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function briefing(): View
|
||||||
|
{
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
|
return view('pages.contact', [
|
||||||
|
'siteSettings' => $settings,
|
||||||
|
'pageMeta' => PageMeta::forPage(canonical: route('briefing'), settings: $settings, title: PageMeta::withBrandSuffix('Briefing', $settings), description: 'Solicite uma proposta à '.$settings->brand_name.'.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
app/Http/Controllers/PublicSite/PartnerInquiryController.php
Normal file
66
app/Http/Controllers/PublicSite/PartnerInquiryController.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\PublicSite\PartnerInquiryRequest;
|
||||||
|
use App\Mail\PartnerInquiry;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class PartnerInquiryController extends Controller
|
||||||
|
{
|
||||||
|
private const string DUPLICATE_SESSION_KEY = 'partner_inquiry_hash';
|
||||||
|
|
||||||
|
public function store(PartnerInquiryRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (filled($request->input('website'))) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
if (session()->get(self::DUPLICATE_SESSION_KEY) === $this->hash($validated)) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->put(self::DUPLICATE_SESSION_KEY, $this->hash($validated));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
|
if (filled($settings->email)) {
|
||||||
|
Mail::to($settings->email)->send(new PartnerInquiry([
|
||||||
|
'Nome' => $validated['nome'],
|
||||||
|
'Nome profissional ou empresa' => $validated['empresa'] ?? null,
|
||||||
|
'E-mail' => $validated['email'],
|
||||||
|
'Atuação ou serviço' => $validated['atuacao'],
|
||||||
|
'Área de atendimento' => $validated['area_atendimento'],
|
||||||
|
'Mensagem' => $validated['mensagem'],
|
||||||
|
'Portfólio ou redes' => $validated['portfolio_redes'] ?? null,
|
||||||
|
'Telefone' => $validated['telefone'] ?? null,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Log::error('Falha ao enviar consulta de parceiro', ['exception' => $exception::class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $validated */
|
||||||
|
private function hash(array $validated): string
|
||||||
|
{
|
||||||
|
return hash('sha256', serialize($validated));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function success(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('contact')->with('status', 'partner-inquiry-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Application\Data\PageMeta;
|
|||||||
use App\Application\Queries\Marketing\GetPublishedServices;
|
use App\Application\Queries\Marketing\GetPublishedServices;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
|
||||||
final class ServiceController extends Controller
|
final class ServiceController extends Controller
|
||||||
@@ -19,6 +20,7 @@ final class ServiceController extends Controller
|
|||||||
|
|
||||||
return view('pages.services.index', [
|
return view('pages.services.index', [
|
||||||
'services' => $services,
|
'services' => $services,
|
||||||
|
'weddingPackages' => WeddingPackage::query()->published()->orderBy('sort_order')->get(),
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('services.index'),
|
canonical: route('services.index'),
|
||||||
|
|||||||
32
app/Http/Requests/PublicSite/PartnerInquiryRequest.php
Normal file
32
app/Http/Requests/PublicSite/PartnerInquiryRequest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\PublicSite;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class PartnerInquiryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$email = $this->input('email');
|
||||||
|
|
||||||
|
$this->merge(['email' => is_string($email) ? mb_strtolower(trim($email)) : $email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<int, string>> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nome' => ['required', 'string', 'max:120'],
|
||||||
|
'empresa' => ['nullable', 'string', 'max:160'],
|
||||||
|
'email' => ['required', 'email', 'max:254'],
|
||||||
|
'atuacao' => ['required', 'string', 'max:120'],
|
||||||
|
'area_atendimento' => ['required', 'string', 'max:160'],
|
||||||
|
'mensagem' => ['required', 'string', 'max:3000'],
|
||||||
|
'portfolio_redes' => ['nullable', 'string', 'max:500'],
|
||||||
|
'telefone' => ['nullable', 'string', 'max:40'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Mail/PartnerInquiry.php
Normal file
31
app/Mail/PartnerInquiry.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
final class PartnerInquiry extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $fields */
|
||||||
|
public function __construct(public readonly array $fields) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(subject: 'Nova consulta de fornecedor ou parceria — Amare Assessoria');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(html: 'emails.partner-inquiry', text: 'emails.partner-inquiry-text');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,19 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
/**
|
/**
|
||||||
* @property array<string, string|null> $social_links
|
* @property array<string, string|null> $social_links
|
||||||
* @property list<array{title?: string, body?: string}>|null $method_steps
|
* @property list<array{title?: string, body?: string}>|null $method_steps
|
||||||
|
* @property list<array{title?: string, body?: string}>|null $corporate_steps
|
||||||
* @property list<string>|null $principles
|
* @property list<string>|null $principles
|
||||||
* @property bool $analytics_enabled
|
* @property bool $analytics_enabled
|
||||||
* @property string|null $default_og_image_path
|
* @property string|null $default_og_image_path
|
||||||
* @property string|null $default_og_image_alt
|
* @property string|null $default_og_image_alt
|
||||||
* @property string|null $about_image_path
|
* @property string|null $about_image_path
|
||||||
* @property string|null $about_image_alt
|
* @property string|null $about_image_alt
|
||||||
|
* @property string|null $hero_image_path
|
||||||
|
* @property string|null $hero_image_alt
|
||||||
|
* @property string|null $services_hero_image_path
|
||||||
|
* @property string|null $services_hero_image_alt
|
||||||
|
* @property string|null $portfolio_hero_image_path
|
||||||
|
* @property string|null $portfolio_hero_image_alt
|
||||||
* @property string|null $logo_path
|
* @property string|null $logo_path
|
||||||
* @property string|null $logo_alt
|
* @property string|null $logo_alt
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +38,12 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'hero_cta_label',
|
'hero_cta_label',
|
||||||
'hero_secondary_cta_label',
|
'hero_secondary_cta_label',
|
||||||
'hero_note',
|
'hero_note',
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'services_hero_image_path',
|
||||||
|
'services_hero_image_alt',
|
||||||
|
'portfolio_hero_image_path',
|
||||||
|
'portfolio_hero_image_alt',
|
||||||
'about_summary',
|
'about_summary',
|
||||||
'about_image_path',
|
'about_image_path',
|
||||||
'about_image_alt',
|
'about_image_alt',
|
||||||
@@ -39,9 +52,11 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'manifesto_body',
|
'manifesto_body',
|
||||||
'method_intro',
|
'method_intro',
|
||||||
'method_steps',
|
'method_steps',
|
||||||
|
'corporate_steps',
|
||||||
'principles',
|
'principles',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
|
'whatsapp_number',
|
||||||
'city',
|
'city',
|
||||||
'social_links',
|
'social_links',
|
||||||
'default_meta_title',
|
'default_meta_title',
|
||||||
@@ -70,9 +85,11 @@ class SiteSetting extends Model
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => self::defaultMethodSteps(),
|
'method_steps' => self::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => self::defaultCorporateSteps(),
|
||||||
'principles' => self::defaultPrinciples(),
|
'principles' => self::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
|
'whatsapp_number' => null,
|
||||||
'city' => 'São Paulo - SP',
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [],
|
'social_links' => [],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
@@ -106,6 +123,27 @@ class SiteSetting extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{title: string, body: string}>
|
||||||
|
*/
|
||||||
|
public static function defaultCorporateSteps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Planejamento',
|
||||||
|
'body' => 'Estruturação de escopo, cronograma e prioridades.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Produção',
|
||||||
|
'body' => 'Coordenação dos elementos necessários para colocar o evento de pé.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Execução',
|
||||||
|
'body' => 'Condução e acompanhamento do evento conforme o projeto aprovado.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
@@ -127,6 +165,7 @@ class SiteSetting extends Model
|
|||||||
return [
|
return [
|
||||||
'social_links' => 'array',
|
'social_links' => 'array',
|
||||||
'method_steps' => 'array',
|
'method_steps' => 'array',
|
||||||
|
'corporate_steps' => 'array',
|
||||||
'principles' => 'array',
|
'principles' => 'array',
|
||||||
'analytics_enabled' => 'boolean',
|
'analytics_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|||||||
110
app/Models/WeddingPackage.php
Normal file
110
app/Models/WeddingPackage.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\HasPublication;
|
||||||
|
use App\Policies\WeddingPackagePolicy;
|
||||||
|
use Database\Factories\WeddingPackageFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $name
|
||||||
|
* @property string|null $slug
|
||||||
|
* @property string $level
|
||||||
|
* @property string|null $tag
|
||||||
|
* @property string|null $subtitle
|
||||||
|
* @property string $summary
|
||||||
|
* @property list<string> $scope_items
|
||||||
|
* @property string $cta_label
|
||||||
|
* @property string|null $whatsapp_message
|
||||||
|
* @property string|null $compare_heading
|
||||||
|
* @property string|null $compare_summary
|
||||||
|
* @property string|null $eyebrow
|
||||||
|
* @property string|null $title_line
|
||||||
|
* @property string|null $title_emphasis
|
||||||
|
* @property string|null $hero_lead
|
||||||
|
* @property string|null $hero_image_path
|
||||||
|
* @property string|null $hero_image_alt
|
||||||
|
* @property list<array{icon_key: string, label: string}>|null $benefits
|
||||||
|
* @property list<array{icon_key: string, title: string, description: string}>|null $included_items
|
||||||
|
* @property string|null $audience_heading
|
||||||
|
* @property string|null $audience_intro
|
||||||
|
* @property list<string>|null $audience_points
|
||||||
|
* @property string|null $audience_image_path
|
||||||
|
* @property string|null $audience_image_alt
|
||||||
|
* @property string|null $final_cta_heading
|
||||||
|
* @property string|null $final_cta_body
|
||||||
|
* @property string|null $meta_title
|
||||||
|
* @property string|null $meta_description
|
||||||
|
* @property int $sort_order
|
||||||
|
* @property Carbon|null $published_at
|
||||||
|
*/
|
||||||
|
#[Fillable([
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'level',
|
||||||
|
'tag',
|
||||||
|
'subtitle',
|
||||||
|
'summary',
|
||||||
|
'scope_items',
|
||||||
|
'cta_label',
|
||||||
|
'whatsapp_message',
|
||||||
|
'compare_heading',
|
||||||
|
'compare_summary',
|
||||||
|
'sort_order',
|
||||||
|
'published_at',
|
||||||
|
'eyebrow',
|
||||||
|
'title_line',
|
||||||
|
'title_emphasis',
|
||||||
|
'hero_lead',
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'benefits',
|
||||||
|
'included_items',
|
||||||
|
'audience_heading',
|
||||||
|
'audience_intro',
|
||||||
|
'audience_points',
|
||||||
|
'audience_image_path',
|
||||||
|
'audience_image_alt',
|
||||||
|
'final_cta_heading',
|
||||||
|
'final_cta_body',
|
||||||
|
'meta_title',
|
||||||
|
'meta_description',
|
||||||
|
])]
|
||||||
|
#[UsePolicy(WeddingPackagePolicy::class)]
|
||||||
|
class WeddingPackage extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<WeddingPackageFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
use HasPublication;
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::saving(function (WeddingPackage $weddingPackage): void {
|
||||||
|
if (blank($weddingPackage->slug) && filled($weddingPackage->name)) {
|
||||||
|
$weddingPackage->slug = Str::slug($weddingPackage->name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string|class-string> */
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scope_items' => 'array',
|
||||||
|
'benefits' => 'array',
|
||||||
|
'included_items' => 'array',
|
||||||
|
'audience_points' => 'array',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
'published_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Policies/WeddingPackagePolicy.php
Normal file
36
app/Policies/WeddingPackagePolicy.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
|
||||||
|
class WeddingPackagePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
RateLimiter::for('contact-briefing', function (Request $request): Limit {
|
RateLimiter::for('contact-briefing', function (Request $request): Limit {
|
||||||
return Limit::perMinute(5)->by($request->ip().'|contact-briefing');
|
return Limit::perMinute(5)->by($request->ip().'|contact-briefing');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RateLimiter::for('partner-inquiry', function (Request $request): Limit {
|
||||||
|
return Limit::perMinute(5)->by($request->ip().'|partner-inquiry');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function freezeClockWhenConfigured(): void
|
private function freezeClockWhenConfigured(): void
|
||||||
|
|||||||
58
app/Support/PackageContactLink.php
Normal file
58
app/Support/PackageContactLink.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
|
||||||
|
final class PackageContactLink
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Resolve the contextual contact link for a wedding package modality.
|
||||||
|
*
|
||||||
|
* Uses the package-specific WhatsApp message when provided; otherwise
|
||||||
|
* falls back to the default template with the modality name.
|
||||||
|
*
|
||||||
|
* @return array{href: string, isWhatsapp: bool}
|
||||||
|
*/
|
||||||
|
public static function for(SiteSetting $settings, string $packageName, ?string $customMessage = null): array
|
||||||
|
{
|
||||||
|
$message = filled($customMessage)
|
||||||
|
? $customMessage
|
||||||
|
: 'Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.';
|
||||||
|
|
||||||
|
return self::build($settings, $message, $packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a generic contact link (no specific modality), used by the
|
||||||
|
* "Conversar com a Amare" guidance band.
|
||||||
|
*
|
||||||
|
* @return array{href: string, isWhatsapp: bool}
|
||||||
|
*/
|
||||||
|
public static function generic(SiteSetting $settings): array
|
||||||
|
{
|
||||||
|
return self::build(
|
||||||
|
$settings,
|
||||||
|
'Olá, ainda estou decidindo a modalidade ideal para o meu casamento. Podemos conversar?',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{href: string, isWhatsapp: bool}
|
||||||
|
*/
|
||||||
|
private static function build(SiteSetting $settings, string $message, ?string $packageName): array
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
|
||||||
|
$isWhatsapp = strlen($digits) >= 10;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'href' => $isWhatsapp
|
||||||
|
? 'https://wa.me/'.$digits.'?text='.rawurlencode($message)
|
||||||
|
: route('briefing', $packageName !== null ? ['servico_interesse' => $packageName] : []),
|
||||||
|
'isWhatsapp' => $isWhatsapp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\Drivers\Gd\Driver;
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
@@ -12,13 +13,23 @@ use Throwable;
|
|||||||
|
|
||||||
final class ResponsiveImage
|
final class ResponsiveImage
|
||||||
{
|
{
|
||||||
/** @var list<int> */
|
/**
|
||||||
public const WIDTHS = [480, 960, 1440];
|
* 720 sits between the original 480 and 960 because that is where the common
|
||||||
|
* mobile viewport lands: 412 CSS px at a 1.75 device pixel ratio asks for
|
||||||
|
* ~721 px, so a full-width image used to jump straight to the 960 variant and
|
||||||
|
* pay for a third more pixels than it drew. Measured on the hero (MAN-109):
|
||||||
|
* 143 KiB at 960 in jpeg against 75 KiB at 720 in webp.
|
||||||
|
*
|
||||||
|
* @var list<int>
|
||||||
|
*/
|
||||||
|
public const WIDTHS = [480, 720, 960, 1440];
|
||||||
|
|
||||||
public static function generate(string $path, ?string $disk = null): void
|
public static function generate(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
if (! $filesystem->exists($path)) {
|
if (! $filesystem->exists($path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,40 +58,85 @@ final class ResponsiveImage
|
|||||||
};
|
};
|
||||||
|
|
||||||
$filesystem->put($variantPath, (string) $encoded);
|
$filesystem->put($variantPath, (string) $encoded);
|
||||||
|
|
||||||
|
// A webp sibling for every variant. The MAN-109 audit measured the
|
||||||
|
// hero as the LCP element on every mobile page, at 143 KiB for a
|
||||||
|
// 960 px jpeg — webp carries the same picture for roughly a third of
|
||||||
|
// that. `x-media.image` offers these through a <source> so a browser
|
||||||
|
// that cannot decode webp still gets the original format.
|
||||||
|
if ($extension === 'webp') {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$webp = $manager->read($contents);
|
||||||
|
|
||||||
|
if ($webp->width() > $width) {
|
||||||
|
$webp->scale(width: $width);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesystem->put(
|
||||||
|
self::webpVariantPath($path, $width),
|
||||||
|
(string) $webp->toWebp(quality: 80)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::metadata($path, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function deleteVariants(string $path, ?string $disk = null): void
|
public static function deleteVariants(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
foreach (self::WIDTHS as $width) {
|
foreach (self::WIDTHS as $width) {
|
||||||
$variantPath = self::variantPath($path, $width);
|
foreach ([self::variantPath($path, $width), self::webpVariantPath($path, $width)] as $variantPath) {
|
||||||
|
|
||||||
if ($filesystem->exists($variantPath)) {
|
if ($filesystem->exists($variantPath)) {
|
||||||
$filesystem->delete($variantPath);
|
$filesystem->delete($variantPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function delete(string $path, ?string $disk = null): void
|
public static function delete(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
self::deleteVariants($path, $disk);
|
self::deleteVariants($path, $disk);
|
||||||
|
|
||||||
if ($filesystem->exists($path)) {
|
if ($filesystem->exists($path)) {
|
||||||
$filesystem->delete($path);
|
$filesystem->delete($path);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function replace(string $previousPath, string $newPath, ?string $disk = null): void
|
public static function replace(string $previousPath, string $newPath, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($previousPath, $disk);
|
||||||
|
self::forgetMetadata($newPath, $disk);
|
||||||
|
|
||||||
|
try {
|
||||||
if ($previousPath !== '' && $previousPath !== $newPath) {
|
if ($previousPath !== '' && $previousPath !== $newPath) {
|
||||||
self::delete($previousPath, $disk);
|
self::delete($previousPath, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::generate($newPath, $disk);
|
self::generate($newPath, $disk);
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($previousPath, $disk);
|
||||||
|
self::forgetMetadata($newPath, $disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::metadata($newPath, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function variantPath(string $path, int $width): string
|
public static function variantPath(string $path, int $width): string
|
||||||
@@ -93,26 +149,35 @@ final class ResponsiveImage
|
|||||||
return $directory === '' ? $variantName : $directory.'/'.$variantName;
|
return $directory === '' ? $variantName : $directory.'/'.$variantName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The webp sibling of a variant, named by appending rather than replacing the
|
||||||
|
* extension. Uploads are stored under a UUID so a collision is already
|
||||||
|
* unlikely, but `photo-480.jpg.webp` cannot collide with the webp variant of
|
||||||
|
* a `photo.png` the way `photo-480.webp` would.
|
||||||
|
*/
|
||||||
|
public static function webpVariantPath(string $path, int $width): string
|
||||||
|
{
|
||||||
|
return self::variantPath($path, $width).'.webp';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array{path: string, width: int}>
|
* @return list<array{path: string, width: int}>
|
||||||
*/
|
*/
|
||||||
public static function availableVariants(string $path, ?string $disk = null): array
|
public static function availableVariants(string $path, ?string $disk = null): array
|
||||||
{
|
{
|
||||||
$filesystem = self::filesystem($disk);
|
return self::metadata($path, $disk)['variants'] ?? [];
|
||||||
$variants = [];
|
|
||||||
|
|
||||||
foreach (self::WIDTHS as $width) {
|
|
||||||
$variantPath = self::variantPath($path, $width);
|
|
||||||
|
|
||||||
if ($filesystem->exists($variantPath)) {
|
|
||||||
$variants[] = [
|
|
||||||
'path' => $variantPath,
|
|
||||||
'width' => $width,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $variants;
|
/**
|
||||||
|
* The webp variants that exist for a path. Empty when the media predates
|
||||||
|
* `media:generate-variants` running with webp support, which is why
|
||||||
|
* `x-media.image` treats the <source> as optional rather than assuming it.
|
||||||
|
*
|
||||||
|
* @return list<array{path: string, width: int}>
|
||||||
|
*/
|
||||||
|
public static function availableWebpVariants(string $path, ?string $disk = null): array
|
||||||
|
{
|
||||||
|
return self::metadata($path, $disk)['webp_variants'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,13 +185,38 @@ final class ResponsiveImage
|
|||||||
*/
|
*/
|
||||||
public static function dimensions(string $path, ?string $disk = null): ?array
|
public static function dimensions(string $path, ?string $disk = null): ?array
|
||||||
{
|
{
|
||||||
|
$metadata = self::metadata($path, $disk);
|
||||||
|
|
||||||
|
return $metadata === null ? null : [
|
||||||
|
'width' => $metadata['width'],
|
||||||
|
'height' => $metadata['height'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* width: int,
|
||||||
|
* height: int,
|
||||||
|
* variants: list<array{path: string, width: int}>,
|
||||||
|
* webp_variants: list<array{path: string, width: int}>
|
||||||
|
* }|null
|
||||||
|
*/
|
||||||
|
public static function metadata(string $path, ?string $disk = null): ?array
|
||||||
|
{
|
||||||
|
$key = self::metadataCacheKey($path, $disk);
|
||||||
|
$cached = self::cachedMetadata($key);
|
||||||
|
|
||||||
|
if (is_array($cached)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
if (! $filesystem->exists($path)) {
|
if (! $filesystem->exists($path)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$contents = $filesystem->get($path);
|
$contents = $filesystem->get($path);
|
||||||
|
|
||||||
if ($contents === null) {
|
if ($contents === null) {
|
||||||
@@ -134,18 +224,88 @@ final class ResponsiveImage
|
|||||||
}
|
}
|
||||||
|
|
||||||
$image = (new ImageManager(new Driver))->read($contents);
|
$image = (new ImageManager(new Driver))->read($contents);
|
||||||
|
$metadata = [
|
||||||
return [
|
|
||||||
'width' => $image->width(),
|
'width' => $image->width(),
|
||||||
'height' => $image->height(),
|
'height' => $image->height(),
|
||||||
|
'variants' => self::existingVariants($filesystem, $path, false),
|
||||||
|
'webp_variants' => self::existingVariants($filesystem, $path, true),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
self::storeMetadata($key, $metadata);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function forgetMetadata(string $path, ?string $disk = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Cache::forget(self::metadataCacheKey($path, $disk));
|
||||||
|
} catch (Throwable) {
|
||||||
|
// Cache availability must not block public media rendering or cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static function filesystem(?string $disk): Filesystem
|
private static function filesystem(?string $disk): Filesystem
|
||||||
{
|
{
|
||||||
return Storage::disk($disk ?? PublicImageUploadRules::disk());
|
return Storage::disk(self::diskName($disk));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{path: string, width: int}>
|
||||||
|
*/
|
||||||
|
private static function existingVariants(Filesystem $filesystem, string $path, bool $webp): array
|
||||||
|
{
|
||||||
|
$variants = [];
|
||||||
|
|
||||||
|
foreach (self::WIDTHS as $width) {
|
||||||
|
$variantPath = $webp
|
||||||
|
? self::webpVariantPath($path, $width)
|
||||||
|
: self::variantPath($path, $width);
|
||||||
|
|
||||||
|
if ($filesystem->exists($variantPath)) {
|
||||||
|
$variants[] = ['path' => $variantPath, 'width' => $width];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $variants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function metadataCacheKey(string $path, ?string $disk): string
|
||||||
|
{
|
||||||
|
return 'responsive-image:metadata:'.sha1(self::diskName($disk).'|'.$path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private static function cachedMetadata(string $key): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$cached = Cache::get($key);
|
||||||
|
|
||||||
|
return is_array($cached) ? $cached : null;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $metadata
|
||||||
|
*/
|
||||||
|
private static function storeMetadata(string $key, array $metadata): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Cache::forever($key, $metadata);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// The uncached result remains safe to use for this request.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function diskName(?string $disk): string
|
||||||
|
{
|
||||||
|
return $disk ?? PublicImageUploadRules::disk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,9 +87,6 @@
|
|||||||
"npm audit --omit=dev --audit-level=high",
|
"npm audit --omit=dev --audit-level=high",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
"visual:update": [
|
|
||||||
"@php artisan test --testsuite=Browser --update-snapshots"
|
|
||||||
],
|
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover --ansi",
|
"@php artisan package:discover --ansi",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When set outside production, the application clock is frozen for
|
| When set outside production, the application clock is frozen for
|
||||||
| deterministic rendering (visual regression / seeded content).
|
| deterministic time-dependent tests or seeded content.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
56
database/factories/WeddingPackageFactory.php
Normal file
56
database/factories/WeddingPackageFactory.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/** @extends Factory<WeddingPackage> */
|
||||||
|
class WeddingPackageFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = WeddingPackage::class;
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$name = fake()->unique()->words(2, true);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => Str::slug($name),
|
||||||
|
'level' => 'Assessoria',
|
||||||
|
'tag' => fake()->words(2, true),
|
||||||
|
'subtitle' => fake()->sentence(),
|
||||||
|
'summary' => fake()->sentence(),
|
||||||
|
'scope_items' => [fake()->sentence()],
|
||||||
|
'cta_label' => 'Conversar sobre esta modalidade',
|
||||||
|
'compare_heading' => fake()->words(3, true),
|
||||||
|
'compare_summary' => fake()->sentence(),
|
||||||
|
'sort_order' => 0,
|
||||||
|
'published_at' => null,
|
||||||
|
'eyebrow' => 'Assessoria',
|
||||||
|
'title_line' => fake()->words(2, true),
|
||||||
|
'title_emphasis' => fake()->word(),
|
||||||
|
'hero_lead' => fake()->paragraph(),
|
||||||
|
'benefits' => [
|
||||||
|
['icon_key' => 'checklist', 'label' => fake()->word()],
|
||||||
|
],
|
||||||
|
'included_items' => [
|
||||||
|
['icon_key' => 'checklist', 'title' => fake()->word(), 'description' => fake()->sentence()],
|
||||||
|
],
|
||||||
|
'audience_heading' => 'Para quem é este pacote',
|
||||||
|
'audience_intro' => fake()->paragraph(),
|
||||||
|
'audience_points' => [fake()->sentence()],
|
||||||
|
'final_cta_heading' => 'Vamos conversar?',
|
||||||
|
'final_cta_body' => fake()->paragraph(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function published(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (): array => ['published_at' => now()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->string('hero_image_path')->nullable()->after('hero_note');
|
||||||
|
$table->string('hero_image_alt')->nullable()->after('hero_image_path');
|
||||||
|
$table->string('services_hero_image_path')->nullable()->after('hero_image_alt');
|
||||||
|
$table->string('services_hero_image_alt')->nullable()->after('services_hero_image_path');
|
||||||
|
$table->string('portfolio_hero_image_path')->nullable()->after('services_hero_image_alt');
|
||||||
|
$table->string('portfolio_hero_image_alt')->nullable()->after('portfolio_hero_image_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn([
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'services_hero_image_path',
|
||||||
|
'services_hero_image_alt',
|
||||||
|
'portfolio_hero_image_path',
|
||||||
|
'portfolio_hero_image_alt',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->jsonb('corporate_steps')->nullable()->after('method_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('corporate_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->string('whatsapp_number')->nullable()->after('phone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('whatsapp_number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('level');
|
||||||
|
$table->string('summary');
|
||||||
|
$table->jsonb('scope_items');
|
||||||
|
$table->string('cta_label');
|
||||||
|
$table->integer('sort_order')->default(0)->index();
|
||||||
|
$table->timestamp('published_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('wedding_packages');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->string('tag')->nullable();
|
||||||
|
$table->string('subtitle')->nullable();
|
||||||
|
$table->string('compare_heading')->nullable();
|
||||||
|
$table->string('compare_summary')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['tag', 'subtitle', 'compare_heading', 'compare_summary']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->string('slug')->nullable()->unique()->after('name');
|
||||||
|
$table->string('eyebrow')->nullable()->after('level');
|
||||||
|
$table->string('title_line')->nullable()->after('eyebrow');
|
||||||
|
$table->string('title_emphasis')->nullable()->after('title_line');
|
||||||
|
$table->text('hero_lead')->nullable()->after('title_emphasis');
|
||||||
|
$table->string('hero_image_path')->nullable()->after('hero_lead');
|
||||||
|
$table->string('hero_image_alt')->nullable()->after('hero_image_path');
|
||||||
|
$table->jsonb('benefits')->nullable()->after('hero_image_alt');
|
||||||
|
$table->jsonb('included_items')->nullable()->after('benefits');
|
||||||
|
$table->string('audience_heading')->nullable()->after('included_items');
|
||||||
|
$table->text('audience_intro')->nullable()->after('audience_heading');
|
||||||
|
$table->jsonb('audience_points')->nullable()->after('audience_intro');
|
||||||
|
$table->string('audience_image_path')->nullable()->after('audience_points');
|
||||||
|
$table->string('audience_image_alt')->nullable()->after('audience_image_path');
|
||||||
|
$table->string('final_cta_heading')->nullable()->after('audience_image_alt');
|
||||||
|
$table->text('final_cta_body')->nullable()->after('final_cta_heading');
|
||||||
|
$table->string('meta_title')->nullable()->after('final_cta_body');
|
||||||
|
$table->text('meta_description')->nullable()->after('meta_title');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->backfillSlugs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->dropUnique(['slug']);
|
||||||
|
$table->dropColumn([
|
||||||
|
'slug',
|
||||||
|
'eyebrow',
|
||||||
|
'title_line',
|
||||||
|
'title_emphasis',
|
||||||
|
'hero_lead',
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'benefits',
|
||||||
|
'included_items',
|
||||||
|
'audience_heading',
|
||||||
|
'audience_intro',
|
||||||
|
'audience_points',
|
||||||
|
'audience_image_path',
|
||||||
|
'audience_image_alt',
|
||||||
|
'final_cta_heading',
|
||||||
|
'final_cta_body',
|
||||||
|
'meta_title',
|
||||||
|
'meta_description',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backfillSlugs(): void
|
||||||
|
{
|
||||||
|
$rows = DB::table('wedding_packages')->whereNull('slug')->get(['id', 'name']);
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$slug = Str::slug($row->name);
|
||||||
|
$candidate = $slug;
|
||||||
|
$suffix = 2;
|
||||||
|
|
||||||
|
while (DB::table('wedding_packages')->where('slug', $candidate)->exists()) {
|
||||||
|
$candidate = "{$slug}-{$suffix}";
|
||||||
|
$suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('wedding_packages')->where('id', $row->id)->update(['slug' => $candidate]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->text('whatsapp_message')->nullable()->after('cta_label');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('whatsapp_message');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -49,6 +49,7 @@ class ContentSeeder extends Seeder
|
|||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->call(TestimonialsSeeder::class);
|
$this->call(TestimonialsSeeder::class);
|
||||||
|
$this->call(WeddingPackagesSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -61,6 +62,12 @@ class ContentSeeder extends Seeder
|
|||||||
'hero_cta_label' => 'Solicitar proposta',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
|
'hero_image_path' => $this->copyFixture('case-casamento-ana-lucas.jpg', 'content/heroes/home.jpg'),
|
||||||
|
'hero_image_alt' => 'Celebração ao ar livre em São Paulo',
|
||||||
|
'services_hero_image_path' => $this->copyFixture('service-eventos-corporativos.jpg', 'content/heroes/services.jpg'),
|
||||||
|
'services_hero_image_alt' => 'Mesa preparada para um evento corporativo',
|
||||||
|
'portfolio_hero_image_path' => $this->copyFixture('case-lancamento-verano.jpg', 'content/heroes/portfolio.jpg'),
|
||||||
|
'portfolio_hero_image_alt' => 'Ambientação de um evento de lançamento',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
||||||
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
||||||
@@ -69,6 +76,7 @@ class ContentSeeder extends Seeder
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => SiteSetting::defaultMethodSteps(),
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => SiteSetting::defaultCorporateSteps(),
|
||||||
'principles' => SiteSetting::defaultPrinciples(),
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use Illuminate\Support\Carbon;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministic content for visual regression. Keep separate from ContentSeeder demo data.
|
* Deterministic content and image fixtures. Keep separate from ContentSeeder demo data.
|
||||||
*/
|
*/
|
||||||
class VisualContentSeeder extends Seeder
|
class VisualContentSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->seedTestimonials();
|
$this->seedTestimonials();
|
||||||
|
$this->call(WeddingPackagesSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -42,6 +43,12 @@ class VisualContentSeeder extends Seeder
|
|||||||
'hero_cta_label' => 'Solicitar proposta',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
|
'hero_image_path' => $this->writeSolidJpeg('visual/heroes/home.jpg', 1600, 1100, [218, 224, 205]),
|
||||||
|
'hero_image_alt' => 'Imagem editorial da home',
|
||||||
|
'services_hero_image_path' => $this->writeSolidJpeg('visual/heroes/services.jpg', 1600, 1100, [196, 200, 184]),
|
||||||
|
'services_hero_image_alt' => 'Imagem editorial de Serviços',
|
||||||
|
'portfolio_hero_image_path' => $this->writeSolidJpeg('visual/heroes/portfolio.jpg', 1600, 1100, [228, 226, 221]),
|
||||||
|
'portfolio_hero_image_alt' => 'Imagem editorial de Portfólio',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
||||||
'about_image_alt' => 'Imagem editorial da página Sobre',
|
'about_image_alt' => 'Imagem editorial da página Sobre',
|
||||||
@@ -50,6 +57,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => SiteSetting::defaultMethodSteps(),
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => SiteSetting::defaultCorporateSteps(),
|
||||||
'principles' => SiteSetting::defaultPrinciples(),
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
@@ -192,16 +200,22 @@ class VisualContentSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
||||||
{
|
{
|
||||||
|
$disk = Storage::disk('public');
|
||||||
|
|
||||||
|
if ($disk->exists($destination)) {
|
||||||
|
return $destination;
|
||||||
|
}
|
||||||
|
|
||||||
$image = imagecreatetruecolor($width, $height);
|
$image = imagecreatetruecolor($width, $height);
|
||||||
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
||||||
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
imagejpeg($image, null, 90);
|
imagejpeg($image, null, 80);
|
||||||
$binary = (string) ob_get_clean();
|
$binary = (string) ob_get_clean();
|
||||||
imagedestroy($image);
|
imagedestroy($image);
|
||||||
|
|
||||||
Storage::disk('public')->put($destination, $binary);
|
$disk->put($destination, $binary);
|
||||||
|
|
||||||
return $destination;
|
return $destination;
|
||||||
}
|
}
|
||||||
|
|||||||
175
database/seeders/WeddingPackagesSeeder.php
Normal file
175
database/seeders/WeddingPackagesSeeder.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wedding package modalities shown on the home page (03 — Amare Casamentos)
|
||||||
|
* and the services page. Copy is approved from the structural model preview
|
||||||
|
* (preview(2).html) and remains editable via the Filament resource. Detail
|
||||||
|
* copy follows the complete-assessoria preview
|
||||||
|
* (amare-assessoria-completa.html) adapted to each modality.
|
||||||
|
*/
|
||||||
|
class WeddingPackagesSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const PUBLISHED_AT = '2026-08-11 00:00:00';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$packages = [
|
||||||
|
[
|
||||||
|
'name' => 'Essenza',
|
||||||
|
'slug' => 'essenza',
|
||||||
|
'level' => '01 / COMPLETA',
|
||||||
|
'tag' => 'Assessoria completa',
|
||||||
|
'subtitle' => 'Do planejamento ao grande dia.',
|
||||||
|
'summary' => 'Para casais que desejam contar com a Amare desde o início ou ainda estão estruturando etapas importantes.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Estruturação do planejamento',
|
||||||
|
'Cronogramas, prazos e prioridades',
|
||||||
|
'Orientação e gestão de fornecedores',
|
||||||
|
'Alinhamento entre os envolvidos',
|
||||||
|
'Coordenação da execução',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Essenza',
|
||||||
|
'compare_heading' => 'Começar com a Amare',
|
||||||
|
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'eyebrow' => 'Assessoria',
|
||||||
|
'title_line' => 'Assessoria',
|
||||||
|
'title_emphasis' => 'Completa',
|
||||||
|
'hero_lead' => 'Do planejamento ao grande dia, com calma, precisão e cuidado.',
|
||||||
|
'benefits' => [
|
||||||
|
['icon_key' => 'calendar', 'label' => 'Organização integral'],
|
||||||
|
['icon_key' => 'checklist', 'label' => 'Planejamento personalizado'],
|
||||||
|
['icon_key' => 'users', 'label' => 'Fornecedores selecionados'],
|
||||||
|
['icon_key' => 'heart', 'label' => 'Experiência leve e inesquecível'],
|
||||||
|
],
|
||||||
|
'included_items' => [
|
||||||
|
['icon_key' => 'heart', 'title' => 'Escuta e briefing', 'description' => 'Cada casal é único; começamos por ouvir.'],
|
||||||
|
['icon_key' => 'checklist', 'title' => 'Planejamento completo', 'description' => 'Cada etapa mapeada com clareza.'],
|
||||||
|
['icon_key' => 'map', 'title' => 'Gestão de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
|
||||||
|
['icon_key' => 'calendar', 'title' => 'Acompanhamento contínuo', 'description' => 'Perto do casal em cada decisão.'],
|
||||||
|
['icon_key' => 'spark', 'title' => 'Produção e logística', 'description' => 'Dia a dia do evento organizado nos detalhes.'],
|
||||||
|
['icon_key' => 'users', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
|
||||||
|
],
|
||||||
|
'audience_heading' => 'Para quem é este pacote',
|
||||||
|
'audience_intro' => 'Para quem quer uma assessoria completa, do início ao fim.',
|
||||||
|
'audience_points' => [
|
||||||
|
'Casamentos',
|
||||||
|
'Aniversários',
|
||||||
|
'Eventos sociais e celebrações',
|
||||||
|
'Eventos corporativos',
|
||||||
|
],
|
||||||
|
'final_cta_heading' => 'Pronta para começar?',
|
||||||
|
'final_cta_body' => 'Conte seu evento para a Amare e receba uma proposta.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Conduzione',
|
||||||
|
'slug' => 'conduzione',
|
||||||
|
'level' => '02 / PARCIAL',
|
||||||
|
'tag' => 'Assessoria parcial',
|
||||||
|
'subtitle' => 'Para quem já começou e quer seguir acompanhado.',
|
||||||
|
'summary' => 'Para casais que já tomaram decisões e contrataram parte dos fornecedores, mas precisam estruturar o que falta.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Diagnóstico do planejamento existente',
|
||||||
|
'Organização das pendências',
|
||||||
|
'Gestão dos fornecedores contratados',
|
||||||
|
'Orientação para próximas decisões',
|
||||||
|
'Coordenação da execução',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Conduzione',
|
||||||
|
'compare_heading' => 'Trazer a Amare para o caminho',
|
||||||
|
'compare_summary' => 'O planejamento existe, mas ainda há decisões e gestão pela frente.',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'eyebrow' => 'Assessoria',
|
||||||
|
'title_line' => 'Assessoria',
|
||||||
|
'title_emphasis' => 'Parcial',
|
||||||
|
'hero_lead' => 'Para retomar o planejamento, organizar o que falta e seguir com acompanhamento profissional.',
|
||||||
|
'benefits' => [
|
||||||
|
['icon_key' => 'checklist', 'label' => 'Diagnóstico completo'],
|
||||||
|
['icon_key' => 'calendar', 'label' => 'Organização integral'],
|
||||||
|
['icon_key' => 'users', 'label' => 'Fornecedores selecionados'],
|
||||||
|
['icon_key' => 'heart', 'label' => 'Experiência leve e inesquecível'],
|
||||||
|
],
|
||||||
|
'included_items' => [
|
||||||
|
['icon_key' => 'checklist', 'title' => 'Diagnóstico do planejamento', 'description' => 'Um olhar profissional sobre o que já existe.'],
|
||||||
|
['icon_key' => 'calendar', 'title' => 'Organização das pendências', 'description' => 'Prioridades claras, etapa a etapa.'],
|
||||||
|
['icon_key' => 'map', 'title' => 'Gestão de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
|
||||||
|
['icon_key' => 'spark', 'title' => 'Gestão dos próximos passos', 'description' => 'Um plano para chegar tranquilo ao grande dia.'],
|
||||||
|
['icon_key' => 'users', 'title' => 'Acompanhamento contínuo', 'description' => 'Perto do casal em cada decisão.'],
|
||||||
|
['icon_key' => 'heart', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
|
||||||
|
],
|
||||||
|
'audience_heading' => 'Para quem é este pacote',
|
||||||
|
'audience_intro' => 'Para quem já começou a planejar e quer retomar as rédeas com acompanhamento profissional.',
|
||||||
|
'audience_points' => [
|
||||||
|
'Casamentos em andamento',
|
||||||
|
'Casais com pouco tempo disponível',
|
||||||
|
'Quem precisa priorizar pendências',
|
||||||
|
'Quem busca fornecedores confiáveis',
|
||||||
|
],
|
||||||
|
'final_cta_heading' => 'Pronta para retomar?',
|
||||||
|
'final_cta_body' => 'Conte onde seu planejamento está e receba uma proposta.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Grand Jour',
|
||||||
|
'slug' => 'grand-jour',
|
||||||
|
'level' => '03 / FINAL',
|
||||||
|
'tag' => 'Assessoria final',
|
||||||
|
'subtitle' => 'O planejamento está pronto. Agora é hora de viver.',
|
||||||
|
'summary' => 'Para casais na reta final que precisam de uma equipe para assumir alinhamentos, cronograma e operação.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Imersão no planejamento existente',
|
||||||
|
'Conferência das informações',
|
||||||
|
'Alinhamento dos fornecedores',
|
||||||
|
'Organização do cronograma final',
|
||||||
|
'Gestão da operação do evento',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Grand Jour',
|
||||||
|
'compare_heading' => 'Entregar a operação para a Amare',
|
||||||
|
'compare_summary' => 'O projeto está pronto e o foco passa a ser alinhamento e execução.',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'eyebrow' => 'Assessoria',
|
||||||
|
'title_line' => 'Assessoria',
|
||||||
|
'title_emphasis' => 'Final',
|
||||||
|
'hero_lead' => 'Para assumir os alinhamentos e a operação quando o grande dia está próximo.',
|
||||||
|
'benefits' => [
|
||||||
|
['icon_key' => 'checklist', 'label' => 'Imersão no que já existe'],
|
||||||
|
['icon_key' => 'users', 'label' => 'Alinhamento de fornecedores'],
|
||||||
|
['icon_key' => 'calendar', 'label' => 'Cronograma final'],
|
||||||
|
['icon_key' => 'heart', 'label' => 'Operação com leveza'],
|
||||||
|
],
|
||||||
|
'included_items' => [
|
||||||
|
['icon_key' => 'checklist', 'title' => 'Imersão no planejamento', 'description' => 'Entendemos cada decisão já tomada.'],
|
||||||
|
['icon_key' => 'map', 'title' => 'Alinhamento de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
|
||||||
|
['icon_key' => 'calendar', 'title' => 'Cronograma final', 'description' => 'Cada etapa posicionada na reta final.'],
|
||||||
|
['icon_key' => 'spark', 'title' => 'Ensaios e detalhes', 'description' => 'Produção e logística nos detalhes.'],
|
||||||
|
['icon_key' => 'users', 'title' => 'Operação do evento', 'description' => 'Gestão da equipe e do dia a dia.'],
|
||||||
|
['icon_key' => 'heart', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
|
||||||
|
],
|
||||||
|
'audience_heading' => 'Para quem é este pacote',
|
||||||
|
'audience_intro' => 'Para casais já planejados que querem chegar ao grande dia com equipe e operação no lugar.',
|
||||||
|
'audience_points' => [
|
||||||
|
'Casamentos já planejados',
|
||||||
|
'Casais na reta final',
|
||||||
|
'Quem precisa de operação no dia',
|
||||||
|
'Quem quer viver o momento com leveza',
|
||||||
|
],
|
||||||
|
'final_cta_heading' => 'Pronta para o grande dia?',
|
||||||
|
'final_cta_body' => 'Conte a data do seu evento e receba uma proposta.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
WeddingPackage::query()->updateOrCreate(
|
||||||
|
['name' => $package['name']],
|
||||||
|
[...$package, 'published_at' => Carbon::parse(self::PUBLISHED_AT)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
409
docs/evidence/lighthouse/2026-08-10-local-antes.md
Normal file
409
docs/evidence/lighthouse/2026-08-10-local-antes.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Lighthouse — local
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T15:55:03.101Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `2e43fde`
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.53 s | 0.37 s | 0.000 | 0 ms | 55 ms | 0.53 s – 0.53 s |
|
||||||
|
| contato | mobile | 97 | 100 | 100 | 100 | 2.55 s | 1.50 s | 0.000 | 0 ms | 97 ms | 2.55 s – 2.56 s |
|
||||||
|
| home | desktop | 99 | 100 | 100 | 100 | 0.87 s | 0.37 s | 0.000 | 0 ms | 75 ms | 0.87 s – 0.87 s |
|
||||||
|
| home | mobile | 83 | 100 | 100 | 100 | 4.58 s | 1.51 s | 0.000 | 0 ms | 217 ms | 4.36 s – 4.58 s |
|
||||||
|
| portfolio-detalhe | desktop | 99 | 100 | 100 | 100 | 0.97 s | 0.37 s | 0.000 | 0 ms | 89 ms | 0.97 s – 0.97 s |
|
||||||
|
| portfolio-detalhe | mobile | 87 | 100 | 100 | 100 | 3.98 s | 1.51 s | 0.000 | 0 ms | 126 ms | 3.98 s – 4.05 s |
|
||||||
|
| portfolio | desktop | 99 | 100 | 100 | 100 | 0.83 s | 0.37 s | 0.000 | 0 ms | 187 ms | 0.63 s – 0.83 s |
|
||||||
|
| portfolio | mobile | 87 | 100 | 100 | 100 | 3.98 s | 1.51 s | 0.000 | 0 ms | 108 ms | 3.91 s – 4.20 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.77 s | 0.37 s | 0.000 | 0 ms | 124 ms | 0.77 s – 0.77 s |
|
||||||
|
| servicos | mobile | 89 | 100 | 100 | 100 | 3.68 s | 1.51 s | 0.000 | 0 ms | 78 ms | 3.61 s – 3.68 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.61 s | 0.37 s | 0.000 | 0 ms | 48 ms | 0.57 s – 0.61 s |
|
||||||
|
| sobre | mobile | 94 | 100 | 100 | 100 | 3.01 s | 1.51 s | 0.000 | 0 ms | 133 ms | 2.87 s – 3.01 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.41 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 2.10 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.11 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.11 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Render blocking requests | 0.60 s | — |
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.45 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.27 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.08 s |
|
||||||
|
| `/brand/mark-on-light.webp` | Image | 42 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.25 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 3.30 s |
|
||||||
|
| Load Time | 0.11 s |
|
||||||
|
| Render Delay | 0.72 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.25 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.25 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.25 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.23 s |
|
||||||
|
| `/brand/mark-on-light.webp` | Image | 42 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.24 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.24 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.35 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.30 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.54 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.26 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg` | Image | 287 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.11 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.62 s |
|
||||||
|
| Load Time | 0.06 s |
|
||||||
|
| Render Delay | 0.85 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.16 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.16 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.14 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.19 s |
|
||||||
|
| Load Delay | 0.51 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.23 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.20 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.22 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.21 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.25 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.68 s |
|
||||||
|
| Load Time | 0.09 s |
|
||||||
|
| Render Delay | 0.76 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.13 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.35 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.13 s |
|
||||||
|
| Load Delay | 0.49 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.15 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.15 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.15 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.22 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.94 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.12 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.12 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.12 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.25 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.22 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.06 s |
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.05 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.05 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.94 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.54 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.16 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.16 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.30 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
407
docs/evidence/lighthouse/2026-08-10-local-depois.md
Normal file
407
docs/evidence/lighthouse/2026-08-10-local-depois.md
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# Lighthouse — local-sizes
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T17:38:33.741Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `9ab2beb` (o script gravou `65919d4`, o HEAD no momento da coleta; a árvore medida é a que virou `9ab2beb`)
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 91 ms | 0.36 s – 0.37 s |
|
||||||
|
| contato | mobile | 100 | 100 | 100 | 100 | 1.50 s | 0.92 s | 0.000 | 0 ms | 66 ms | 1.50 s – 1.51 s |
|
||||||
|
| home | desktop | 100 | 100 | 100 | 100 | 0.53 s | 0.25 s | 0.000 | 0 ms | 110 ms | 0.53 s – 0.54 s |
|
||||||
|
| home | mobile | 98 | 100 | 100 | 100 | 2.49 s | 0.92 s | 0.000 | 0 ms | 77 ms | 2.48 s – 2.57 s |
|
||||||
|
| portfolio-detalhe | desktop | 100 | 100 | 100 | 100 | 0.65 s | 0.25 s | 0.000 | 0 ms | 81 ms | 0.65 s – 0.66 s |
|
||||||
|
| portfolio-detalhe | mobile | 99 | 100 | 100 | 100 | 2.18 s | 0.90 s | 0.000 | 0 ms | 77 ms | 2.18 s – 2.18 s |
|
||||||
|
| portfolio | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 72 ms | 0.36 s – 0.49 s |
|
||||||
|
| portfolio | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 71 ms | 1.58 s – 1.58 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.24 s | 0.000 | 0 ms | 59 ms | 0.36 s – 0.38 s |
|
||||||
|
| servicos | mobile | 99 | 100 | 100 | 100 | 2.03 s | 0.90 s | 0.000 | 0 ms | 62 ms | 1.58 s – 2.03 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 51 ms | 0.36 s – 0.37 s |
|
||||||
|
| sobre | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 93 ms | 1.58 s – 1.58 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.24 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 1.05 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-720.jpg.webp" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…"`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.25 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-720.jpg.webp` | Image | 74 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.14 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.14 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.12 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.12 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-720.jpg.webp" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…"`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.24 s |
|
||||||
|
| Load Time | 0.10 s |
|
||||||
|
| Render Delay | 0.69 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-720.jpg.webp` | Image | 74 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.09 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.28 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.22 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg.webp` | Image | 210 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-720.jpg.webp` | Image | 105 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-720.jpg.webp` | Image | 88 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.09 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.11 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.57 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-720.jpg.webp` | Image | 105 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-720.jpg.webp` | Image | 88 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.14 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.09 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.66 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.42 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-720.jpg.webp" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/se`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.14 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.10 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-720.jpg.webp` | Image | 49 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-720.jpg.webp` | Image | 39 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-720.jpg.webp` | Image | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.07 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-720.jpg.webp" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/se`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.08 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.43 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-720.jpg.webp` | Image | 49 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-720.jpg.webp` | Image | 39 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-720.jpg.webp` | Image | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.07 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-720.jpg.webp" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.11 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/storage/content/about/about-image-720.jpg.webp` | Image | 22 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/sobre` | Document | 5 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-720.jpg.webp" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.77 s |
|
||||||
|
| Load Time | 0.02 s |
|
||||||
|
| Render Delay | 0.33 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/storage/content/about/about-image-720.jpg.webp` | Image | 22 KiB | 0.11 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
| `/sobre` | Document | 5 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
# Lighthouse — local-pos
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T16:20:48.372Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `65919d4` (o script gravou `2e43fde`, o HEAD no momento da coleta; a árvore medida é a que virou `65919d4`)
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 20 ms | 0.36 s – 0.37 s |
|
||||||
|
| contato | mobile | 100 | 100 | 100 | 100 | 1.51 s | 0.92 s | 0.000 | 0 ms | 46 ms | 1.50 s – 1.51 s |
|
||||||
|
| home | desktop | 100 | 100 | 100 | 100 | 0.67 s | 0.25 s | 0.000 | 0 ms | 87 ms | 0.67 s – 0.67 s |
|
||||||
|
| home | mobile | 92 | 100 | 100 | 100 | 3.39 s | 0.92 s | 0.000 | 0 ms | 88 ms | 3.38 s – 3.46 s |
|
||||||
|
| portfolio-detalhe | desktop | 100 | 100 | 100 | 100 | 0.79 s | 0.25 s | 0.000 | 0 ms | 74 ms | 0.79 s – 0.79 s |
|
||||||
|
| portfolio-detalhe | mobile | 95 | 100 | 100 | 100 | 3.01 s | 0.91 s | 0.000 | 0 ms | 89 ms | 2.93 s – 3.01 s |
|
||||||
|
| portfolio | desktop | 100 | 100 | 100 | 100 | 0.65 s | 0.25 s | 0.000 | 0 ms | 73 ms | 0.37 s – 0.65 s |
|
||||||
|
| portfolio | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 69 ms | 1.58 s – 3.24 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 121 ms | 0.36 s – 0.59 s |
|
||||||
|
| servicos | mobile | 97 | 100 | 100 | 100 | 2.63 s | 0.90 s | 0.000 | 0 ms | 73 ms | 1.58 s – 2.63 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.37 s | 0.25 s | 0.000 | 0 ms | 97 ms | 0.36 s – 0.45 s |
|
||||||
|
| sobre | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 48 ms | 1.58 s – 1.96 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.24 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.03 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.03 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.03 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.03 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.03 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.02 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.03 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 1.05 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.05 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.05 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.33 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.19 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.15 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.88 s |
|
||||||
|
| Load Time | 0.17 s |
|
||||||
|
| Render Delay | 0.88 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.12 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.12 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.43 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.21 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg` | Image | 287 KiB | 0.08 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.63 s |
|
||||||
|
| Load Time | 0.06 s |
|
||||||
|
| Render Delay | 0.87 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.28 s |
|
||||||
|
| Load Time | 0.05 s |
|
||||||
|
| Render Delay | 0.19 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.64 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.46 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.18 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.06 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.14 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.14 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.13 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.13 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.13 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.50 s |
|
||||||
|
| Load Time | 0.11 s |
|
||||||
|
| Render Delay | 0.56 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.09 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.09 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.60 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.13 s |
|
||||||
|
| Load Delay | 0.15 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.09 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.11 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.11 s |
|
||||||
|
| `/sobre` | Document | 4 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.11 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.57 s |
|
||||||
|
| Load Time | 0.10 s |
|
||||||
|
| Render Delay | 0.46 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/sobre` | Document | 4 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
205
docs/evidence/lighthouse/README.md
Normal file
205
docs/evidence/lighthouse/README.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Lighthouse — MAN-109
|
||||||
|
|
||||||
|
Medições versionadas porque a rodada anterior (PR #34) sobreviveu apenas como
|
||||||
|
uma tabela digitada à mão num comentário do Linear: `storage/app/lighthouse` é
|
||||||
|
gitignored, então não havia contra o quê comparar. Os relatórios brutos pesam
|
||||||
|
~48 MB por passada e continuam fora do repositório; o que fica versionado são os
|
||||||
|
resumos, que já carregam a decomposição do LCP e a lista de requests até o LCP.
|
||||||
|
|
||||||
|
## Como reproduzir
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sobe a imagem de produção. O seed roda no host, não no container:
|
||||||
|
# ContentSeeder é no-op fora de local/staging/testing (database/seeders/ContentSeeder.php:44),
|
||||||
|
# então sob APP_ENV=production ele não semeia nada e o Lighthouse mede páginas vazias.
|
||||||
|
docker build -t amare-app:man109 .
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan db:seed --class=ContentSeeder --force
|
||||||
|
php artisan media:generate-variants # sem isso o LCP da home infla ~2,5 s
|
||||||
|
docker run -d --name amare-web -p 8000:8000 -e APP_ENV=production ... amare-app:man109
|
||||||
|
|
||||||
|
TARGET=local bash scripts/perf/lighthouse.sh storage/app/lighthouse
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada página é auditada 3 vezes por preset e o relatório de LCP mediano é o
|
||||||
|
reportado — nunca a média. O LCP se move alguns décimos entre execuções na mesma
|
||||||
|
build, e uma única execução não sustenta comparação.
|
||||||
|
|
||||||
|
## Resultado
|
||||||
|
|
||||||
|
Preset mobile padrão do Lighthouse 12.8.2: throttling simulado de 150 ms de RTT,
|
||||||
|
~1,6 Mbps, CPU 4× mais lenta. Chrome estável do sistema (não o Chromium do
|
||||||
|
Playwright), imagem de produção local, seeder `ContentSeeder`.
|
||||||
|
|
||||||
|
Antes: commit `2e43fde`. As colunas intermediárias mostram cada correção
|
||||||
|
isoladamente, medida com uma passada completa antes da seguinte entrar.
|
||||||
|
|
||||||
|
| página | preset | antes | + fontes e marca | + webp e 720w | + sizes correto | perf antes → depois | bytes antes → depois |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 0.53 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 262 → 106 KiB |
|
||||||
|
| contato | mobile | 2.55 s | 1.51 s | 1.51 s | **1.50 s** | 97 → 100 | 262 → 106 KiB |
|
||||||
|
| home | desktop | 0.87 s | 0.67 s | 0.53 s | **0.53 s** | 99 → 100 | 798 → 347 KiB |
|
||||||
|
| home | mobile | 4.58 s | 3.39 s | 2.87 s | **2.49 s** | 83 → 98 | 798 → 347 KiB |
|
||||||
|
| portfolio | desktop | 0.83 s | 0.65 s | 0.37 s | **0.36 s** | 99 → 100 | 609 → 259 KiB |
|
||||||
|
| portfolio | mobile | 3.98 s | 1.58 s | 2.64 s | **1.58 s** | 87 → 100 | 609 → 259 KiB |
|
||||||
|
| portfolio-detalhe | desktop | 0.97 s | 0.79 s | 0.66 s | **0.65 s** | 99 → 100 | 930 → 508 KiB |
|
||||||
|
| portfolio-detalhe | mobile | 3.98 s | 3.01 s | 2.71 s | **2.18 s** | 87 → 99 | 785 → 365 KiB |
|
||||||
|
| servicos | desktop | 0.77 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 552 → 222 KiB |
|
||||||
|
| servicos | mobile | 3.68 s | 2.63 s | 1.58 s | **2.03 s** | 89 → 99 | 552 → 222 KiB |
|
||||||
|
| sobre | desktop | 0.61 s | 0.37 s | 0.37 s | **0.36 s** | 100 → 100 | 322 → 127 KiB |
|
||||||
|
| sobre | mobile | 3.01 s | 1.58 s | 1.60 s | **1.58 s** | 94 → 100 | 322 → 127 KiB |
|
||||||
|
|
||||||
|
FCP cai de 1,51 s para 0,91 s em todas as páginas no mobile. Acessibilidade,
|
||||||
|
boas práticas e SEO marcam 100 em todas as páginas nos dois presets, antes e
|
||||||
|
depois. CLS é 0,000 e TBT é 0 ms em todas — as metas de §6.6 para essas três
|
||||||
|
métricas já passavam e continuam passando.
|
||||||
|
|
||||||
|
**Contra a meta de LCP ≤ 2,5 s da §6.6: todas as páginas passam nos dois
|
||||||
|
presets.** Desktop com folga (máximo 0,65 s). No mobile o pior caso é a home a
|
||||||
|
2,49 s, ou seja **em cima da linha** — a pior das três execuções dela deu 2,57 s.
|
||||||
|
Tratar a home como aprovada por margem, não com folga.
|
||||||
|
|
||||||
|
Duas colunas intermediárias merecem leitura cuidadosa em vez de conclusão:
|
||||||
|
|
||||||
|
- `portfolio` mobile aparece pior na coluna do webp (2,64 s) do que na anterior
|
||||||
|
(1,58 s). É variância, não regressão: as três execuções daquela passada foram
|
||||||
|
1,59 / 2,64 / 2,78 s. A página é a mais instável do conjunto e a mediana pulou
|
||||||
|
de ponta. Na passada final as três deram 1,58 s.
|
||||||
|
- `servicos` mobile sobe de 1,58 s para 2,03 s da terceira para a quarta coluna,
|
||||||
|
pelo mesmo motivo (1,58 / 1,58 / 2,03).
|
||||||
|
|
||||||
|
É exatamente por isso que o script roda três vezes e reporta a mediana; ainda
|
||||||
|
assim, diferenças abaixo de meio segundo entre passadas não devem ser lidas como
|
||||||
|
efeito de uma correção.
|
||||||
|
|
||||||
|
Ressalva: a medição é local, então latência de origem e TLS não entram, e o
|
||||||
|
throttling de rede é simulado. Trate o LCP como piso, não como valor de campo.
|
||||||
|
|
||||||
|
## O que cada correção comprou
|
||||||
|
|
||||||
|
**Fontes servidas em dobro — 88 KiB fora do caminho crítico.** Bunny entrega
|
||||||
|
cada peso de EB Garamond em woff2 e woff, e o plugin de fontes emitia uma regra
|
||||||
|
`@font-face` para cada, woff2 primeiro e woff depois. Duas regras com a mesma
|
||||||
|
família, peso, estilo e unicode-range fazem a **última** vencer: o navegador
|
||||||
|
renderizava a partir dos woff e descartava os woff2 pré-carregados.
|
||||||
|
|
||||||
|
A prova está no log de rede da home antes da correção: 3 woff em prioridade
|
||||||
|
`VeryHigh` (88 KiB) — a prioridade mais alta da página, à frente do elemento de
|
||||||
|
LCP — somados a 3 woff2 em `High` (74 KiB) que só foram baixados porque estavam
|
||||||
|
em `<link rel="preload">`. 162 KiB de tráfego de fonte para 74 KiB de fonte útil.
|
||||||
|
|
||||||
|
woff2 é suportado por todo navegador que este site atende desde 2016, então as
|
||||||
|
regras woff não eram fallback e sim peso morto. O plugin `amare:fonts-woff2-only`
|
||||||
|
em `vite.config.js` remove as regras do CSS e do manifest e tira os arquivos do
|
||||||
|
bundle. É o que derruba o FCP de 1,51 s para 0,91 s em todas as páginas.
|
||||||
|
|
||||||
|
**Ativos de marca reencodados — 102 KiB fora do caminho crítico.** O logotipo
|
||||||
|
era servido a 512 px de largura para renderizar em 48 px (lockup, cabeçalho e
|
||||||
|
rodapé) e 32 px (mark, home): 84 KiB + 42 KiB com `loading="eager"` em todas as
|
||||||
|
páginas. Reencodados a 3× do maior render — lockup 149×144 (14 KiB) e mark
|
||||||
|
191×96 (10 KiB) —, mantendo os mesmos nomes de arquivo para não invalidar cache.
|
||||||
|
As variantes `on-dark` foram reencodadas junto por consistência; nenhuma view as
|
||||||
|
usa hoje.
|
||||||
|
|
||||||
|
Isto absorve MAN-122: os ativos de marca foram reencodados sem alterar a forma
|
||||||
|
ou a cor percebida, reduzindo apenas o peso transferido.
|
||||||
|
|
||||||
|
Os arquivos versionados aqui são: `2026-08-10-local-antes.md` (commit `2e43fde`),
|
||||||
|
`2026-08-10-local-etapa-fontes-e-marca.md` (passada intermediária) e
|
||||||
|
`2026-08-10-local-depois.md` (estado final).
|
||||||
|
|
||||||
|
**Variantes WebP nas imagens de conteúdo.** Com fontes e marca resolvidas, o
|
||||||
|
elemento de LCP de toda página no mobile era a imagem do hero, e o Load Delay de
|
||||||
|
1,88 s era contenção de banda pura: 143 KiB de JPEG q82 a 960 px, com as três
|
||||||
|
capas do portfólio somando outros 348 KiB. `ResponsiveImage::generate()` passa a
|
||||||
|
escrever uma variante `.webp` ao lado de cada variante no formato original, e
|
||||||
|
`x-media.image` a oferece num `<source type="image/webp">`. O `<img>` continua
|
||||||
|
apontando para o formato original, então nada quebra em quem não decodifica webp,
|
||||||
|
e mídia antiga sem irmãos webp renderiza `<img>` puro como antes.
|
||||||
|
|
||||||
|
**`sizes` que descreve a realidade.** Nenhuma imagem do site ocupa a viewport
|
||||||
|
inteira: todas ficam dentro de `container-amare`, que reserva 1,5rem de padding
|
||||||
|
de cada lado. Declarar `100vw` fazia uma viewport de 412 px em DPR 1,75 pedir
|
||||||
|
721 px e pular para a variante de 960 para desenhar uma caixa de 637 px — errar
|
||||||
|
por um pixel custava um terço a mais de bytes. Com `calc(100vw - 3rem)` a home
|
||||||
|
passa a usar a variante de 720 (**74 KiB**, contra 143 KiB no início).
|
||||||
|
|
||||||
|
Foi também por isso que 720 entrou em `ResponsiveImage::WIDTHS`: sem ela o salto
|
||||||
|
de 480 para 960 é grande demais para a viewport mobile mais comum.
|
||||||
|
|
||||||
|
Decomposição final do LCP da home no mobile:
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0,45 s |
|
||||||
|
| Load Delay | 1,24 s |
|
||||||
|
| Load Time | 0,10 s |
|
||||||
|
| Render Delay | 0,69 s |
|
||||||
|
|
||||||
|
## O ganho de imagem só aparece depois do deploy regenerar as variantes
|
||||||
|
|
||||||
|
Toda a medição acima usou `FILESYSTEM_DISK=local`. Em staging e produção o disco
|
||||||
|
é `r2`, e lá as variantes `.webp` e a largura 720 **ainda não existem** para a
|
||||||
|
mídia já publicada. Até `media:generate-variants` rodar, `availableWebpVariants()`
|
||||||
|
volta vazio, o `<source>` é omitido e o `srcset` do formato original perde a
|
||||||
|
entrada de 720w — ou seja, nenhum dos dois ganhos de imagem aparece.
|
||||||
|
|
||||||
|
O serviço `migrate` do `docker-compose.deploy.yml` roda
|
||||||
|
`php artisan media:generate-variants` sem condição a cada deploy, depois de
|
||||||
|
`migrate` e do seed, e o comando só pula um caminho quando o arquivo original não
|
||||||
|
existe (`MediaGenerateVariantsCommand`) — regenera mesmo quando já há variantes.
|
||||||
|
Então o primeiro deploy desta branch produz as variantes novas por conta própria.
|
||||||
|
|
||||||
|
Enquanto isso não acontece, há um custo sem contrapartida: `x-media.image` faz
|
||||||
|
agora **8 chamadas `exists()` por imagem** (4 larguras × 2 formatos) contra o
|
||||||
|
object store, no lugar de 3. Na home são ~32 round trips remotos por request em
|
||||||
|
vez de ~12. Isso agrava a hipótese não validada abaixo em vez de melhorá-la, e é
|
||||||
|
mais um motivo para cachear esses metadados.
|
||||||
|
|
||||||
|
## Se a home precisar de mais folga
|
||||||
|
|
||||||
|
O Load Delay de 1,24 s ainda é contenção: as três capas do portfólio somam
|
||||||
|
155 KiB e, embora sejam `loading="lazy"` e prioridade `Low`, o navegador as busca
|
||||||
|
porque entram no limiar de lazy loading da viewport emulada. Os levers restantes,
|
||||||
|
em ordem de custo:
|
||||||
|
|
||||||
|
1. Baixar a qualidade webp de 80 para 75 (medido: 109 → 92 KiB na imagem do
|
||||||
|
hero a 960 px). Barato, mas mexe na qualidade de imagem de uma marca cujo
|
||||||
|
posicionamento é acabamento editorial — decisão de produto, não de engenharia.
|
||||||
|
2. Reduzir o Render Delay de 0,69 s, que agora é a segunda maior fatia e é
|
||||||
|
trabalho de main thread, não de rede.
|
||||||
|
3. FrankenPHP worker mode para o TTFB de 0,45 s. Tem gatilho objetivo em
|
||||||
|
SPEC §22 e não deve ser puxado antes dele.
|
||||||
|
|
||||||
|
## Cobertura que este trabalho não tem
|
||||||
|
|
||||||
|
Os testes browser não exercitam `srcset` nem `<picture>` com variantes geradas:
|
||||||
|
nem `ContentSeeder` nem `VisualContentSeeder` geram variantes. Nesses cenários,
|
||||||
|
`availableVariants()` volta vazio e o componente renderiza `<img>` puro. A
|
||||||
|
cobertura do caminho com variantes fica nos testes de feature
|
||||||
|
(`MediaImageComponentTest`).
|
||||||
|
|
||||||
|
## Staging
|
||||||
|
|
||||||
|
Não medido. A origem de staging responde `303` para
|
||||||
|
`blocked.teams.cloudflare.com` a partir da rede corporativa da Creditas
|
||||||
|
("O conteúdo deste site viola a Política de Segurança da Informação"), inclusive
|
||||||
|
em `/up`. O preflight de HTTP 200 do script barra a execução antes de gastar
|
||||||
|
minutos auditando páginas de bloqueio.
|
||||||
|
|
||||||
|
Duas hipóteses seguem **não validadas** porque só existem com
|
||||||
|
`FILESYSTEM_DISK=r2`, que é configuração de staging e produção:
|
||||||
|
|
||||||
|
- `x-media.image` chama `ResponsiveImage::availableVariants()`,
|
||||||
|
`availableWebpVariants()` (8 `exists()` somados) e `ResponsiveImage::dimensions()`
|
||||||
|
(que baixa o arquivo inteiro) a cada render, sem cache. No disco `local` são
|
||||||
|
leituras de filesystem; no `r2` são ~9 round trips remotos por imagem no lado
|
||||||
|
servidor, ~36 na home. Teste discriminante: comparar o TTFB de `/contato`
|
||||||
|
(zero imagens) com o de `/portfolio` (N imagens). Se o TTFB escalar com a
|
||||||
|
contagem de imagens, está confirmado. **Este é o item mais urgente da lista**,
|
||||||
|
porque as variantes webp multiplicaram o número de chamadas.
|
||||||
|
- A mídia vem de `R2_URL`, uma origem cross-origin, e não há `preconnect` no
|
||||||
|
`<head>` — DNS e TLS entram antes do LCP.
|
||||||
|
|
||||||
|
Para medir: rodar `TARGET=staging BASE_URL=<origem> bash scripts/perf/lighthouse.sh`
|
||||||
|
de uma rede sem o filtro corporativo.
|
||||||
81
docs/superpowers/plans/2026-08-11-full-bleed-home-hero.md
Normal file
81
docs/superpowers/plans/2026-08-11-full-bleed-home-hero.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Hero full-bleed da home Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Fazer a fotografia configurada do hero da home preencher toda a coluna direita abaixo do header, preservando a leitura editorial e o fallback sem foto.
|
||||||
|
|
||||||
|
**Architecture:** `x-home.hero` continuará a renderizar as duas variantes. A variante com imagem usará uma grade full-width com a coluna textual ancorada à régua global e a mídia como segundo trilho sem container; a variante tonal permanece contida. Os testes de feature e browser documentam os contratos de markup, tamanho e responsividade.
|
||||||
|
|
||||||
|
**Tech Stack:** Laravel Blade, Tailwind CSS 4 utilities, Pest feature/browser tests, Vite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: Layout e contratos do hero
|
||||||
|
|
||||||
|
### Task 1: Cobrir o spread full-bleed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/Feature/PublicSite/ImmersivePhotoHeroTest.php`
|
||||||
|
- Modify: `tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Escrever as expectativas de feature para a variante com foto**
|
||||||
|
|
||||||
|
Exigir `data-split-hero`, `data-hero-content`, `data-motion="page-open"`, `data-reveal-group`, `aria-labelledby="hero-heading"`, `loading="eager"`, `fetchpriority="high"` e `sizes="(max-width: 767px) 100vw, 55vw"`. Com `hero_image_path` nulo, exigir `data-tonal-hero`, os mesmos atributos de motion/semântica, ausência de `data-split-hero` e ausência de `<img` dentro de `[data-tonal-hero]`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Estender a verificação browser de geometria**
|
||||||
|
|
||||||
|
No viewport 1440×1000, obter os retângulos de `.site-header`, `[data-chapter="hero"]`, `[data-hero-content]` e `[data-split-hero]`; esperar, com tolerância de 1px, que `hero.top === header.bottom`, `media.top === hero.top`, `media.bottom === hero.bottom`, `media.right === innerWidth` e `hero.height === innerHeight - header.height`. No viewport 390×844, comparar `data-split-hero` com `[data-hero-content]`; esperar que a mídia inicie depois do conteúdo, que `width / height` fique entre 0,79 e 0,81, e que não exista overflow horizontal. Antes das duas visitas de fallback, zerar `hero_image_path`; numa visita com reduced motion, esperar conteúdo visível. Em outra com `reducedMotion: no-preference` e `IntersectionObserver` desabilitado antes da navegação, esperar ausência de `data-motion` em `document.documentElement`, cada `[data-motion="page-open"]` com `.is-active`, cada `[data-reveal]` com `.is-revealed` e `aria-labelledby="hero-heading"`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Executar os testes focados e confirmar a falha inicial**
|
||||||
|
|
||||||
|
Run: `php artisan test tests/Feature/PublicSite/ImmersivePhotoHeroTest.php && php artisan test tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
Expected: falha nas novas expectativas de geometria/markup até a alteração do componente.
|
||||||
|
|
||||||
|
### Task 2: Implementar a grade full-bleed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/components/home/hero.blade.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Separar a variante com imagem do container compartilhado**
|
||||||
|
|
||||||
|
Substituir `min-h-[100dvh]` do `<section>` por `md:h-[calc(100dvh-5rem)]` quando `hero_image_path` estiver preenchido e renderizar um wrapper `grid min-h-[calc(100dvh-5rem)] md:h-full md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]`. A coluna de texto deve manter `data-reveal-group`, receber `data-hero-content`, ter `padding-left: max(var(--amare-container-padding), calc((100vw - var(--amare-container-max)) / 2 + var(--amare-container-padding)))`, padding direito fluido e centralização vertical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Tornar a mídia contínua**
|
||||||
|
|
||||||
|
Aplicar ao bloco `data-split-hero` `overflow-hidden`, `bg-amare-bg-deep`, `md:min-h-full`; abaixo de `md`, aplicar `aspect-[4/5]`. Manter `data-motion-beat="media"`, `data-reveal-media`, `object-cover`, path/alt CMS e mudar `sizes` para `(max-width: 767px) 100vw, 55vw`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Preservar o fallback**
|
||||||
|
|
||||||
|
Manter o wrapper tonal existente para hero sem imagem, com `data-tonal-hero`, `data-motion="page-open"` e `data-reveal-group`; não renderizar o bloco de mídia nessa variante.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Executar os testes focados e confirmar a passagem**
|
||||||
|
|
||||||
|
Run: `php artisan test tests/Feature/PublicSite/ImmersivePhotoHeroTest.php && php artisan test tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Verificar e registrar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/components/home/hero.blade.php`
|
||||||
|
- Modify: `tests/Feature/PublicSite/ImmersivePhotoHeroTest.php`
|
||||||
|
- Modify: `tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Formatar e executar as verificações proporcionais**
|
||||||
|
|
||||||
|
Run: `composer pint && composer phpstan && composer test:feature && composer test:browser && npm run build`
|
||||||
|
|
||||||
|
Expected: todos os comandos passam.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Inspecionar o diff e registrar**
|
||||||
|
|
||||||
|
Run: `git diff --check && git status --short`
|
||||||
|
|
||||||
|
Expected: apenas os três arquivos de implementação/testes e a documentação deste plano/especificação aparecem como escopo.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
Run: `git add resources/views/components/home/hero.blade.php tests/Feature/PublicSite/ImmersivePhotoHeroTest.php tests/Browser/HomeEditorialCadenceTest.php docs/superpowers && git commit -m "feat(home): tornar hero fotográfico full-bleed"`
|
||||||
|
|
||||||
|
Expected: commit focado, sem artefatos de build ou screenshots.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Hero da home com fotografia full-bleed
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Dar à fotografia do hero da home a mesma presença contínua da referência aprovada: no desktop, ela preenche integralmente a coluna direita abaixo do header; o conteúdo editorial permanece concentrado na coluna esquerda.
|
||||||
|
|
||||||
|
## Escopo
|
||||||
|
|
||||||
|
- Alterar somente o componente `resources/views/components/home/hero.blade.php` e seus testes diretos.
|
||||||
|
- Quando `hero_image_path` existir, usar uma grade externa `0.9fr / 1.1fr` a partir de `md` (768px), com altura exata `calc(100dvh - 5rem)`: o espaço restante depois do header desktop. A mídia deve chegar ao topo e à base da seção, sem padding vertical ou lateral de container.
|
||||||
|
- Manter o texto, CTAs, imagem CMS, `alt`, carregamento eager, `fetchpriority`, responsividade de imagem e atributos de motion existentes.
|
||||||
|
- Abaixo de `md`, manter a ordem texto seguido de imagem, sem overflow horizontal, e fixar a mídia em `aspect-ratio: 4 / 5` com `object-cover`.
|
||||||
|
- Quando não houver imagem configurada, manter a abertura tonal, tipográfica e contida já existente, sem área vazia para mídia.
|
||||||
|
|
||||||
|
## Fora de escopo
|
||||||
|
|
||||||
|
- Não alterar conteúdo do CMS, imagens, Open Graph, outras rotas públicas, header, animações, tokens globais ou dependências.
|
||||||
|
- Não criar ou publicar mídia nova.
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
O componente continuará sendo a única unidade de layout do hero. A variante com foto terá um wrapper full-width em desktop. A coluna textual terá `data-hero-content`, margem esquerda `max(var(--amare-container-padding), calc((100vw - var(--amare-container-max)) / 2 + var(--amare-container-padding)))`, a mesma régua esquerda de `container-amare`, e padding direito fluido para não comprimir a leitura. O bloco de mídia ocupará os 55% da grade, receberá `data-split-hero` e `data-reveal-media`, e deixará de ficar limitado pelo container. O atributo `sizes` será `(max-width: 767px) 100vw, 55vw`. A variante sem foto permanece no wrapper atual.
|
||||||
|
|
||||||
|
## Critérios de aceitação
|
||||||
|
|
||||||
|
- Em desktop, `data-split-hero` inicia sob o header e mede `calc(100dvh - 5rem)`, sem margens externas de container.
|
||||||
|
- A foto usa `object-cover`, `loading="eager"`, `fetchpriority="high"`, path/alt próprios do hero e `sizes="(max-width: 767px) 100vw, 55vw"`.
|
||||||
|
- Em mobile, o conteúdo essencial continua acessível antes da foto, sem overflow nem texto recortado.
|
||||||
|
- O fallback sem foto continua com `data-tonal-hero`, sem `data-split-hero` e sem mídia renderizada.
|
||||||
|
- `data-motion="page-open"`, `data-reveal-group`, beats existentes, motion reduzido, CTAs e semântica atual continuam funcionais.
|
||||||
|
- Testes verificam os contratos full-bleed da variante com foto, a ausência de mídia no fallback, e a continuidade de motion/semântica.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Design — Home editorial conforme preview
|
||||||
|
|
||||||
|
Fonte de verdade visual: modelo estrutural validado (`preview(1).html`) + tokens `resources/css/tokens.css` (inalterados; assertados por `HeritageEditorialTokensTest`). Paleta do preview já mapeada para os utilitários `amare-*`.
|
||||||
|
|
||||||
|
## Estrutura da home (8 capítulos)
|
||||||
|
|
||||||
|
| Capítulo | Id/âncora | Componente | Conteúdo |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hero | — | `x-home.hero` | Eyebrow fixo, h1 fixo, lede fixo, CTAs "Conhecer a Amare" (`#sobre`) + "Enviar briefing" (`#contato`), arte `hero_image_path` ou placeholder rotulado "Fotografia hero" |
|
||||||
|
| 01 — A Amare | `#sobre` | `x-home.manifesto` | Editorial-split 0.8fr/1.2fr; statement fixo + placeholder institucional |
|
||||||
|
| 02 — Duas vertentes | `#vertentes` | `x-home.vertentes` | 2 cards (Casamentos/Corporate) com overlay gradiente 43%, CTAs para âncoras |
|
||||||
|
| 03 — Amare Casamentos | `#casamentos` | `x-home.packages` | Grid 3 colunas com bordas (Eyebrow/título/descrição/items/CTA primário → `contact`), bloco de orientação accent-deep + nota de nomenclatura |
|
||||||
|
| 04 — Amare Corporate | `#corporate` | `x-home.corporate` | Etapas `corporate_steps` (número 44px, bordas) + placeholder "Portfólio Corporate / Conteúdo em construção" |
|
||||||
|
| 05 — Portfólio | `#portfolio` | `x-home.portfolio` | Grid 1.2fr/0.8fr/0.8fr, 1 foto alta (row-span-2, 634px) + 4; cases reais ou labels "FOTO 0X" |
|
||||||
|
| 06 — Depoimentos | `#depoimentos` | `x-home.testimonials` | 3 cards brancos com borda; sem carrossel; placeholders até completar 3 |
|
||||||
|
| 07 — Briefing | `#contato` | `x-home.briefing` | Formulário completo reusando `contact.store` (underline inputs, labels uppercase 12px, honeypot `#empresa`, checkbox LGPD) |
|
||||||
|
|
||||||
|
## Padrões visuais
|
||||||
|
|
||||||
|
- Seções: `home-chapter border-b border-amare-border bg-amare-bg`, padding `py-16 md:py-24`, `container-amare`, `data-reveal-group` + blocos `data-reveal data-reveal-from="up"`.
|
||||||
|
- Eyebrow: `text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep`.
|
||||||
|
- h2 `clamp(2.375rem,5vw,4rem)`, h3 `clamp(1.5625rem,2.7vw,2.125rem)`, lede `clamp(1.1875rem,2vw,1.5rem)` max-w 760px, `leading-[1.55]`.
|
||||||
|
- Botão primário: `min-h-[48px] bg-amare-accent text-amare-accent-text uppercase text-xs font-bold tracking-[0.09em]`, hover accent-deep; contorno: `border-amare-accent text-amare-accent-deep`; em bloco escuro: `border-amare-accent-text text-amare-accent-text`.
|
||||||
|
- Header: sticky 74px, `bg-amare-bg/95 backdrop-blur-sm`, nav 13px uppercase `tracking-[0.08em]`, botão "Conte seu evento" → `contact`.
|
||||||
|
- Footer: `py-14 md:py-[58px]`, marca + "Assessoria & produção de eventos • São Paulo", social links do settings ou placeholder "Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)".
|
||||||
|
|
||||||
|
## Acessibilidade
|
||||||
|
|
||||||
|
- Âncoras `aria-labelledby` por seção; foco/ordem tab preservados; skip-link intacto.
|
||||||
|
- `prefers-reduced-motion`: blocos globais em tokens.css (0.01ms) — nenhum motion extra adicionado.
|
||||||
|
- Erros de validação do briefing com `role="alert"` e `aria-describedby`; honeypot oculto.
|
||||||
|
- Contrastes: accent `#556B2F` sobre bg `#FBF9F4` ≈ 4.6:1; accent-text branco sobre accent ≈ 6.9:1; muted `#5D6155` sobre bg ≈ 5.4:1 (dentro do AA).
|
||||||
|
|
||||||
|
## Fora de escopo visual
|
||||||
|
|
||||||
|
Copy final institucional (placeholders), acervo fotográfico autorizado, canal de fornecedores (pós-MVP), folios ornamentais, carrossel, parallax.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Home editorial conforme preview estrutural
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
A home atual não reflete a direção editorial aprovada para o site (Dossiê Editorial do Evento). O cliente validou um modelo estrutural (`preview(1).html`) que demonstra hierarquia, jornada e diferenciação de fluxos: hero editorial, editorial-split institucional, duas vertentes (Casamentos/Corporate), modalidades de casamento com orientação humana, frente corporativa honesta sobre portfólio em construção, portfólio curado, depoimentos sem carrossel e briefing comercial único. A home também é a porta de conversão primária (briefing), então a apresentação deve construir confiança antes de pedir conversão.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Home em 8 seções** (componentes `x-home.*`): hero assimétrico (1.1fr/0.9fr, arte rotulada até acervo autorizado), `01 — A Amare` (editorial-split), `02 — Duas vertentes` (2 cards), `03 — Amare Casamentos` (grid de modalidades com bordas + bloco de orientação em accent-deep), `04 — Amare Corporate` (etapas + placeholder "Conteúdo em construção"), `05 — Portfólio` (1 foto alta + 4), `06 — Depoimentos` (3 cards, sem carrossel), `07 — Briefing comercial` (formulário completo reaproveitando `contact.store`).
|
||||||
|
- **Novo modelo `WeddingPackage`** (Modalidades): migration, model com publication, factory, policy, seeder com copy exata do preview (Essenza/Conduzione/Grand Jour) e recurso Filament "Modalidades" com repeater de itens.
|
||||||
|
- **`SiteSetting.corporate_steps`**: JSON de 3 etapas (Planejamento/Produção/Execução), editável via repeater no `ManageSiteSettings`, semeadas nos dois seeders.
|
||||||
|
- **Header/footer globais** restilizados conforme preview (barra 74px, nav uppercase 13px, botão "Conte seu evento", footer editorial), preservando menu mobile, skip-link, logo e links legais.
|
||||||
|
- Motion mantém convenções existentes (`data-motion="page-open"`, `data-motion-beat`, `data-reveal`), respeitando `prefers-reduced-motion` global.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Copy final institucional onde o preview declara placeholder (ex.: "Aqui entra a apresentação institucional da marca e da Michele…") — permanece placeholder.
|
||||||
|
- Fotografias autorizadas da Amare (acervo ainda em organização) — imagens permanecem rotuladas.
|
||||||
|
- Canal/forma de fornecedores e parcerias (pós-MVP, caixa tracejada) — não misturar ao briefing comercial.
|
||||||
|
- Carrossel de depoimentos, parallax, scroll-jacking ou folios ornamentais.
|
||||||
|
- Alteração de tokens do design system (assertados por `HeritageEditorialTokensTest`).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `wedding-packages`: cadastro de modalidades de casamento (Essenza/Conduzione/Grand Jour) com itens, ordenação e publicação via Filament; exibidas na home na seção `03 — Amare Casamentos`.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: home recomposta em 8 capítulos com âncoras (`#sobre`, `#vertentes`, `#casamentos`, `#corporate`, `#portfolio`, `#depoimentos`, `#contato`); header/footer globais restilizados; briefing da home reutiliza o fluxo `contact.store` existente.
|
||||||
|
- `content-media`: heróis continuam usando `hero_image_path` com fallback tipográfico rotulado; portfólio usa `featuredCases` reais ou labels provisórios.
|
||||||
|
- `design-tokens`: nenhum valor alterado; novos componentes usam os tokens existentes via utilitários `amare-*`.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Wedding Packages
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Cadastro editorial das modalidades de acompanhamento de casamento oferecidas pela Amare (Essenza, Conduzione, Grand Jour). Fonte única de verdade para a seção `03 — Amare Casamentos` da home.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
- FR-1: Um pacote contém `eyebrow` (ex.: "01 / COMPLETA"), `title` (ex.: "Essenza"), `description`, lista ordenada de `items` (o que está incluído) e `sort_order`.
|
||||||
|
- FR-2: Publicação via `published_at` (modelo usa `HasPublication`); pacotes rascunho nunca aparecem na home.
|
||||||
|
- FR-3: A home renderiza pacotes publicados ordenados por `sort_order`, cada um com CTA primário "Quero conhecer a {title}" apontando para o briefing (`route('contact')`).
|
||||||
|
- FR-4: O recurso Filament "Modalidades" (grupo "Conteúdo do site") permite criar/editar/excluir pacotes e ajustar itens via repeater; acesso restrito a admins (`WeddingPackagePolicy`).
|
||||||
|
|
||||||
|
## Non-Requirements
|
||||||
|
|
||||||
|
- Nenhum vínculo com checkout/pagamento nesta capacidade.
|
||||||
|
- Nomenclaturas exibidas são provisórias até confirmação com a cliente (nota visível na home).
|
||||||
|
|
||||||
|
## Data
|
||||||
|
|
||||||
|
- Tabela `wedding_packages` (migration `2026_08_11_000000`): `eyebrow string`, `title string`, `description text`, `items json`, `sort_order int`, `published_at timestamp nullable`, timestamps.
|
||||||
|
- Seed padrão (ambos os seeders): Essenza `01 / COMPLETA`, Conduzione `02 / PARCIAL`, Grand Jour `03 / FINAL`, com copy exata do preview estrutural.
|
||||||
18
openspec/changes/2026-08-11-home-editorial-redesign/tasks.md
Normal file
18
openspec/changes/2026-08-11-home-editorial-redesign/tasks.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Tasks — Home editorial conforme preview
|
||||||
|
|
||||||
|
## Concluído
|
||||||
|
|
||||||
|
- [x] **T0** Worktree `.worktrees/home-redesign` (branch `feat/home-redesign`) a partir de `main`.
|
||||||
|
- [x] **T1** `WeddingPackage` (model + migration `2026_08_11_000000` + factory + policy + seeder + recurso Filament "Modalidades"): 3 modalidades semeadas com copy exata do preview; seeders `ContentSeeder`/`VisualContentSeeder` passam a chamar `WeddingPackagesSeeder`.
|
||||||
|
- [x] **T2** `SiteSetting.corporate_steps` (migration `2026_08_11_010000`, `defaultCorporateSteps()`, casts/fillable/docblock, repeater em `ManageSiteSettings`, seeders atualizados).
|
||||||
|
- [x] **T3** Header/footer globais (`layouts/public.blade.php`) restilizados conforme preview; menu mobile, skip-link, logo e links legais preservados.
|
||||||
|
- [x] **T4** Home recomposta em 8 seções (`x-home.*`: hero, manifesto/sobre, vertentes, packages, corporate, portfolio, testimonials, briefing); `HomeContent` ganha `packages`; `GetHomeContent` busca pacotes publicados por `sort_order`.
|
||||||
|
- [x] **T5** Motion via convenções existentes (`data-reveal`, `data-motion-beat`, `data-motion="page-open"`); `prefers-reduced-motion` global respeitado; sem carrossel/parallax.
|
||||||
|
- [x] **T6** Testes: `HomePageContentTest` reescrito (8 seções, packages, corporate steps, briefing form, placeholders), `HomePageTest`, `GetHomeContentTest` (packages), `ImmersivePhotoHeroTest`/`MotionMarkupTest` atualizados para o novo contrato, `HomeEditorialCadenceTest` reescrito para o layout assimétrico. **178 feature + 42 unit verdes**.
|
||||||
|
- [x] **T7** `composer pint` + `phpstan` limpos; 2 commits convencionais; PR #53 aberto.
|
||||||
|
|
||||||
|
## Pendente
|
||||||
|
|
||||||
|
- [ ] **T8** Proposta openspec (este change) revisada e arquivada após merge.
|
||||||
|
- [ ] Merge do PR #53 após CI verde; limpeza do worktree (`git worktree remove`).
|
||||||
|
- [ ] Follow-up pós-merge: confirmar nomenclaturas das modalidades com a cliente (nota na home: "Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.").
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
A suíte browser contém uma camada exclusiva de comparação de pixels: um teste com baselines versionados, um trait para estabilizar capturas, exportação de diffs e configuração de CI para seeds e artefatos determinísticos. O restante da cobertura browser — smoke, acessibilidade, motion e fixtures de conteúdo — não depende dessa camada.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Remover integralmente a comparação de pixels, os baselines e a infraestrutura exclusiva de suporte.
|
||||||
|
- Manter o job browser em FrankenPHP e suas verificações funcionais, de acessibilidade e motion.
|
||||||
|
- Atualizar as fontes normativas e as specs ativas para que não exijam snapshots visuais.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Não substituir a comparação de pixels por outro serviço ou ferramenta de regressão visual.
|
||||||
|
- Não remover `VisualContentSeeder`, fixtures JPEG, `APP_FROZEN_NOW` que tenham usos fora das capturas, screenshots documentais ou testes browser não relacionados.
|
||||||
|
- Não alterar APIs públicas, rotas, conteúdo nem comportamentos de interface.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **Eliminar a capability em vez de reduzir a matriz.** Todos os requisitos de `visual-regression` serão removidos, pois o produto não terá mais comparação de pixels. Alternativa rejeitada: conservar apenas algumas telas, pois ainda manteria baselines e o custo operacional indesejado.
|
||||||
|
2. **Preservar diagnósticos gerais de browser.** O CI manterá screenshots, logs e relatórios úteis a falhas funcionais; serão removidos apenas seed global, relógio congelado e artefatos específicos de baseline/diff. Alternativa rejeitada: retirar todos os diagnósticos, pois reduziria a capacidade de investigar falhas não visuais.
|
||||||
|
3. **Separar determinismo reutilizável da captura visual.** `VisualContentSeeder` e `APP_FROZEN_NOW` permanecem quando usados por outros testes ou pela aplicação; somente as referências exclusivas a screenshot são excluídas. Alternativa rejeitada: remover os nomes por associação, pois quebraria fixtures e cenários existentes.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Uma mudança puramente visual deixa de bloquear CI] → acessibilidade, smoke, motion e revisão humana continuam no fluxo de qualidade.
|
||||||
|
- [Remover preparação comum do job browser afeta testes restantes] → inspecionar usos de seed/relógio e executar a suíte browser contra FrankenPHP após a alteração.
|
||||||
|
- [Uma spec ativa ainda reintroduz snapshots] → remover as menções no delta de `enhance-public-motion` antes da validação OpenSpec.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Publicar a remoção junto com a atualização da documentação e do CI; não há migração de dados. O rollback é um revert do commit, restaurando os arquivos versionados e a configuração anterior.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
As comparações pixel-a-pixel e seus baselines adicionam manutenção e infraestrutura exclusiva sem serem um gate de qualidade desejado. A qualidade do site continuará protegida por jornadas browser, acessibilidade, motion, smoke e screenshots documentais, sem dependência de snapshots versionados.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Remove a capability `visual-regression` e todos os requisitos de baselines, snapshots e diffs visuais.
|
||||||
|
- Atualiza os quality gates para manter a cobertura browser funcional, de acessibilidade e motion sem comparações de pixels.
|
||||||
|
- Remove a infraestrutura de testes, CI e documentação usada exclusivamente para regressão visual.
|
||||||
|
- **BREAKING** para contribuidores: o comando `composer visual:update` deixa de existir.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
Nenhuma.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `visual-regression`: Remover os requisitos da capability descontinuada.
|
||||||
|
- `quality-gates`: Remover a exigência de executar e publicar diagnósticos de snapshots visuais.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta os testes browser e seus helpers exclusivos, baselines rastreados, configuração de Composer e CI, `.gitignore`, `SPEC.md` e a documentação ativa de motion. Não altera rotas, APIs, dados de conteúdo, fixtures reutilizadas, nem a cobertura funcional, de acessibilidade ou motion.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Browser tests run against FrankenPHP-served application
|
||||||
|
|
||||||
|
The system SHALL execute browser tests using Pest Browser/Playwright against an application served by FrankenPHP in CI. The `browser` job MUST cover the E2E journeys available in the current phase, the automated accessibility checks, motion behavior and smoke checks for public routes (SPEC §13.4, §13.7 and §14.1), without pixel comparison.
|
||||||
|
|
||||||
|
#### Scenario: Browser job validates served application
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** tests execute against the built application artifact or equivalent production-like image
|
||||||
|
|
||||||
|
#### Scenario: Browser job covers functional, accessibility and motion assertions
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** it MUST execute the functional browser, accessibility, motion and smoke suites
|
||||||
|
- **AND** a failing functional assertion or a critical/serious accessibility issue MUST block merge
|
||||||
|
|
||||||
|
#### Scenario: Browser failures publish diagnostics
|
||||||
|
|
||||||
|
- **WHEN** a browser test fails in CI
|
||||||
|
- **THEN** the job MUST publish screenshots, application logs and browser logs as diagnostics
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
**Reason**: O projeto não manterá mais comparação de pixels nem baselines versionados para telas públicas.
|
||||||
|
**Migration**: Remover as asserções de screenshot e os arquivos baseline; manter testes browser funcionais, de acessibilidade, motion e smoke.
|
||||||
|
|
||||||
|
### Requirement: Visual runs are deterministic
|
||||||
|
**Reason**: Não haverá execução de captura para comparação visual.
|
||||||
|
**Migration**: Preservar seeds e relógio congelado apenas onde forem necessários por outros testes.
|
||||||
|
|
||||||
|
### Requirement: Baseline updates are explicit and reviewed
|
||||||
|
**Reason**: O comando e o fluxo de atualização de baselines foram descontinuados junto com a capability.
|
||||||
|
**Migration**: Remover `composer visual:update` e qualquer configuração de CI dedicada a regenerar, transportar ou publicar baselines e diffs.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## 1. Contratos normativos
|
||||||
|
|
||||||
|
- [x] 1.1 Remover os requisitos e critérios de regressão visual de `SPEC.md`, ADR-005 e do delta ativo `enhance-public-motion`.
|
||||||
|
- [x] 1.2 Sincronizar os deltas da mudança para remover a capability `visual-regression` e atualizar `quality-gates`.
|
||||||
|
|
||||||
|
## 2. Infraestrutura de regressão visual
|
||||||
|
|
||||||
|
- [x] 2.1 Remover o teste browser, os 16 baselines, o trait de screenshot, o exportador de diffs e seu teste unitário.
|
||||||
|
- [x] 2.2 Simplificar o `TestCase` e remover o script Composer e a regra de ignore exclusivos de baselines.
|
||||||
|
|
||||||
|
## 3. Browser CI e documentação operacional
|
||||||
|
|
||||||
|
- [x] 3.1 Remover seed global, relógio congelado e cópias de snapshots/diffs exclusivos de baseline do job browser, preservando diagnósticos funcionais.
|
||||||
|
- [x] 3.2 Atualizar a documentação operacional que referencia o fluxo descontinuado sem afetar fixtures ou congelamento usados fora dele.
|
||||||
|
|
||||||
|
## 4. Verificação e arquivamento
|
||||||
|
|
||||||
|
- [x] 4.1 Confirmar ausência dos símbolos e comandos removidos no código ativo e executar os gates de qualidade aplicáveis.
|
||||||
|
- [x] 4.2 Validar OpenSpec estritamente, marcar as tarefas e arquivar a mudança sincronizada.
|
||||||
@@ -10,7 +10,7 @@ O site público usa Blade/Tailwind e já contém um runtime pequeno em `resource
|
|||||||
- Manter texto legível durante toda entrada, animando somente `transform` e recorte de mídia.
|
- Manter texto legível durante toda entrada, animando somente `transform` e recorte de mídia.
|
||||||
- Garantir enhancement progressivo: estado final imediato sem JavaScript, observer ou com movimento reduzido.
|
- Garantir enhancement progressivo: estado final imediato sem JavaScript, observer ou com movimento reduzido.
|
||||||
- Preservar interação, foco, validação e ausência de overflow em desktop/mobile.
|
- Preservar interação, foco, validação e ausência de overflow em desktop/mobile.
|
||||||
- Cobrir marcação, comportamento real, acessibilidade, console e capturas determinísticas.
|
- Cobrir marcação, comportamento real, acessibilidade e console.
|
||||||
|
|
||||||
**Non-Goals:**
|
**Non-Goals:**
|
||||||
|
|
||||||
@@ -32,15 +32,14 @@ O site público usa Blade/Tailwind e já contém um runtime pequeno em `resource
|
|||||||
|
|
||||||
6. **Progresso com coalescência por frame.** Eventos de scroll apenas agendam uma atualização por `requestAnimationFrame`; o cálculo existente é preservado. Alternativa rejeitada: calcular em todo evento, que multiplica leituras/escritas durante scroll.
|
6. **Progresso com coalescência por frame.** Eventos de scroll apenas agendam uma atualização por `requestAnimationFrame`; o cálculo existente é preservado. Alternativa rejeitada: calcular em todo evento, que multiplica leituras/escritas durante scroll.
|
||||||
|
|
||||||
7. **Testes em camadas.** Feature tests comprovam contratos Blade/CSS/JS e renderização de erros; browser tests comprovam entrada, stagger, direções, reduced motion, fallback, interação, overflow e console. Axe/snapshots passam a incluir sobre, contato, privacidade e 404; demais erros ficam em renderização feature.
|
7. **Testes em camadas.** Feature tests comprovam contratos Blade/CSS/JS e renderização de erros; browser tests comprovam entrada, stagger, direções, reduced motion, fallback, interação, overflow e console. Axe e console incluem sobre, contato, privacidade e 404; demais erros ficam em renderização feature.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- [Conteúdo pisca entre estado final e início do enhancement] → inicializar no primeiro módulo Vite, limitar transformações a distâncias pequenas e nunca ocultar texto.
|
- [Conteúdo pisca entre estado final e início do enhancement] → inicializar no primeiro módulo Vite, limitar transformações a distâncias pequenas e nunca ocultar texto.
|
||||||
- [Transforms laterais causam overflow horizontal] → limitar distância por breakpoint, manter recorte no contêiner público e testar `scrollWidth` em ambos viewports.
|
- [Transforms laterais causam overflow horizontal] → limitar distância por breakpoint, manter recorte no contêiner público e testar `scrollWidth` em ambos viewports.
|
||||||
- [Snapshots ficam instáveis] → continuar capturando com `reducedMotion: reduce` e transições desabilitadas.
|
|
||||||
- [Muitos observers/estilos inline] → usar um único observer, custom property de índice limitada e `unobserve` imediato.
|
- [Muitos observers/estilos inline] → usar um único observer, custom property de índice limitada e `unobserve` imediato.
|
||||||
- [Páginas de erro não carregam o runtime em todos os contextos] → o estado final é o baseline seguro; feature tests cobrem os cinco templates.
|
- [Páginas de erro não carregam o runtime em todos os contextos] → o estado final é seguro; feature tests cobrem os cinco templates.
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ O site público já possui uma abertura focal na home, mas o restante da experi
|
|||||||
- Alternar depoimentos entre esquerda e direita, filtrando citações vazias antes de renderizar a sequência.
|
- Alternar depoimentos entre esquerda e direita, filtrando citações vazias antes de renderizar a sequência.
|
||||||
- Adicionar feedback curto e não bloqueante a links, CTAs, navegação e controles de formulário, preservando foco e mensagens de validação.
|
- Adicionar feedback curto e não bloqueante a links, CTAs, navegação e controles de formulário, preservando foco e mensagens de validação.
|
||||||
- Garantir estado final imediato sem JavaScript, sem `IntersectionObserver` e com `prefers-reduced-motion: reduce`.
|
- Garantir estado final imediato sem JavaScript, sem `IntersectionObserver` e com `prefers-reduced-motion: reduce`.
|
||||||
- Limitar o progresso/índice da home a uma atualização por frame e ampliar testes de marcação, browser, acessibilidade, console e regressão visual.
|
- Limitar o progresso/índice da home a uma atualização por frame e ampliar testes de marcação, browser, acessibilidade e console.
|
||||||
- Não objetivos: não criar transições entre rotas, alterar conteúdo ou layout estrutural, adicionar biblioteca de animação, modificar CMS/API/banco/Filament, nem introduzir itens fora do MVP listados em SPEC.md §4.2.
|
- Não objetivos: não criar transições entre rotas, alterar conteúdo ou layout estrutural, adicionar biblioteca de animação, modificar CMS/API/banco/Filament, nem introduzir itens fora do MVP listados em SPEC.md §4.2.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
@@ -23,7 +23,6 @@ O site público já possui uma abertura focal na home, mas o restante da experi
|
|||||||
- `design-tokens`: Especificar tokens compartilhados de duração, stagger, distância e easing para motion editorial.
|
- `design-tokens`: Especificar tokens compartilhados de duração, stagger, distância e easing para motion editorial.
|
||||||
- `public-site-pages`: Cobrir todas as páginas públicas visuais e templates de erro com o contrato compartilhado de abertura e reveal.
|
- `public-site-pages`: Cobrir todas as páginas públicas visuais e templates de erro com o contrato compartilhado de abertura e reveal.
|
||||||
- `testimonials`: Filtrar citações vazias e alternar explicitamente a direção de entrada dos depoimentos renderizados.
|
- `testimonials`: Filtrar citações vazias e alternar explicitamente a direção de entrada dos depoimentos renderizados.
|
||||||
- `visual-regression`: Ampliar a matriz visual para sobre, contato, privacidade e 404, mantendo capturas determinísticas sem animação.
|
|
||||||
- `web-accessibility`: Ampliar axe/console e validar conteúdo imediatamente utilizável com movimento reduzido ou enhancement indisponível.
|
- `web-accessibility`: Ampliar axe/console e validar conteúdo imediatamente utilizável com movimento reduzido ou enhancement indisponível.
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
- [x] 3.1 Add browser tests for real opening/reveal state, 90 ms capped stagger, one-shot observation and interaction during motion
|
- [x] 3.1 Add browser tests for real opening/reveal state, 90 ms capped stagger, one-shot observation and interaction during motion
|
||||||
- [x] 3.2 Add desktop/mobile tests for directional distance, reduced motion final state, no-JS/observer fallback, horizontal overflow and clean console
|
- [x] 3.2 Add desktop/mobile tests for directional distance, reduced motion final state, no-JS/observer fallback, horizontal overflow and clean console
|
||||||
- [x] 3.3 Extend axe and console route matrices to about, contact, privacy and branded 404
|
- [x] 3.3 Extend axe and console route matrices to about, contact, privacy and branded 404
|
||||||
- [x] 3.4 Extend deterministic desktop/mobile visual snapshots to about, contact, privacy and branded 404 and review the generated diffs
|
- [x] 3.4 Extend axe and console route matrices to about, contact, privacy and branded 404
|
||||||
|
|
||||||
## 4. Verification and delivery
|
## 4. Verification and delivery
|
||||||
|
|
||||||
- [x] 4.1 Run focused feature/browser tests, build, Pint, isolated-cache PHPStan and strict OpenSpec validation
|
- [x] 4.1 Run focused feature/browser tests, build, Pint, isolated-cache PHPStan and strict OpenSpec validation
|
||||||
- [x] 4.2 Run `composer quality` and visually review desktop/mobile with normal and reduced motion
|
- [x] 4.2 Run `composer quality` and visually review desktop/mobile with normal and reduced motion
|
||||||
- [ ] 4.3 Stage only scoped files, commit conventionally, open a PR with screenshots and verification evidence, watch CI to green, merge and remove the worktree
|
- [x] 4.3 Stage only scoped files, commit conventionally, open a PR with screenshots and verification evidence, watch CI to green, merge and remove the worktree — delivered in PR #22 with the requested screenshots and local verification; the PR merged as `0aee15e`, the worktree/remote branch no longer exists, and the current implementation is covered by all five green CI jobs on `main` (run 31501097667). Pixel snapshots were later removed by PR #44 and are not part of the current gate.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
O commit de base ja separa as imagens hero de Open Graph, oferece variantes responsivas, fallbacks tonais e preserva o formulario de briefing. As views publicas ainda usam algumas grades simetricas, especialmente nas colecoes de portfolio. A referencia aprovada define composicao e materialidade, nunca novo acervo ou copy.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Aplicar uma cadencia de campos tonais, regras e proporcoes alternadas nas paginas narrativas.
|
||||||
|
- Manter uma ordem DOM linear e completa em telas pequenas.
|
||||||
|
- Reforcar os contratos existentes de acessibilidade, motion progressivo, performance de imagem e SEO.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Alterar dados CMS, contratos de rota, controller, metadata, uploads, variantes ou o formulario.
|
||||||
|
- Usar imagens externas, criar conteudo comercial, adicionar dependencias, atualizar snapshots ou reintroduzir regressao visual.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Usar variacoes de `col-span` e `aspect-ratio` nas galerias existentes
|
||||||
|
|
||||||
|
As imagens continuam no mesmo componente responsivo, mas a primeira imagem e os itens pares recebem encaixes editoriais no desktop. A alternativa seria uma galeria JavaScript ou masonry; foi descartada por prejudicar a ordem de leitura e adicionar comportamento sem necessidade.
|
||||||
|
|
||||||
|
### Aplicar assimetria pela composicao, nao por posicionamento absoluto
|
||||||
|
|
||||||
|
Grid responsivo, margens e campos tonais criam o deslocamento sem tirar conteudo do fluxo. Isso preserva foco, leitura mobile e fallback sem JavaScript. Posicionamento absoluto foi descartado por aumentar risco de sobreposicao e overflow.
|
||||||
|
|
||||||
|
### Reutilizar motion de transformacao e clip ja existente
|
||||||
|
|
||||||
|
As novas regioes conservam `data-reveal` e grupos existentes. Nenhum texto passa a depender de opacidade; `prefers-reduced-motion` continua exibindo o estado final imediatamente.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Ritmo visual pode parecer irregular com poucos cases] -> regras e metadados mantem uma leitura coerente mesmo com um unico item.
|
||||||
|
- [Classes responsivas podem afetar o fluxo em mobile] -> DOM linear e classes de uma coluna continuam sendo a base abaixo de `md`.
|
||||||
|
- [Mudar CSS pode gerar diferencas de ambiente] -> validar markup, acessibilidade funcional e build; snapshots permanecem fora do escopo.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Nao ha migracao de dados. A mudanca e retrocompativel: sem imagens, os fallbacks tonais existentes continuam ativos; com imagens, o mesmo `srcset` responsivo e usado. Reverter o commit restaura somente apresentacao.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma. Fotografia autorizada de producao permanece uma dependencia de conteudo futura.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
O site público já possui os conteúdos, fluxos e a base Heritage Editorial, mas algumas páginas ainda repetem grades regulares que enfraquecem a cadência da referência aprovada. A recomposição consolida uma leitura editorial contínua sem alterar dados CMS, SEO ou a jornada de briefing.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Recompõe as superfícies públicas narrativas com campos tonais, regras finas, assimetria desktop e sequência vertical legível em mobile.
|
||||||
|
- Organiza a home nos capítulos editoriais existentes, com portfólio em campo oliva e recortes de imagens em proporções alternadas.
|
||||||
|
- Reestiliza listagens e detalhe de portfólio para que imagens e blocos de leitura não dependam de uma grade de cartões repetida.
|
||||||
|
- Mantém contato, privacidade e erros em tratamento sóbrio; preserva menu móvel, foco, formulário, SEO, motion progressivo e conteúdo do CMS.
|
||||||
|
- Documenta que heros fotográficos e sua administração já estão presentes na base atual, sem acoplar mídia editorial à imagem Open Graph.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- Nenhuma.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: composição e cadência visual das rotas públicas passam a exigir ritmos editoriais assimétricos.
|
||||||
|
- `content-media`: a apresentação de mídia de portfólio passa a suportar proporções editoriais alternadas sem mudar armazenamento ou variantes.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta Blade e estilos do site público, testes de estrutura/renderização e a documentação OpenSpec. Não altera rotas, controladores, dados de conteúdo, dependências ou contratos de SEO.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
Não introduz itens excluídos por SPEC.md §4.2, novos canais, prova comercial, conteúdo de produção, imagens externas, dependências de animação, snapshots visuais ou mudanças no Filament.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Editorial layouts MUST use CSS-only tonal treatment and alternating aspect ratios for portfolio media while retaining document order and the image component's `srcset`, `sizes`, dimensions and loading behavior. The system MUST NOT introduce external image CDN dependencies that compromise browser-test reliability.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
|
|
||||||
|
#### Scenario: Editorial portfolio media retains responsive delivery
|
||||||
|
|
||||||
|
- **WHEN** a portfolio listing or gallery applies an editorial image proportion
|
||||||
|
- **THEN** the rendered image MUST still expose the responsive media component markup
|
||||||
|
- **AND** image order and lazy-loading behavior MUST remain intact
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition: asymmetric spreads and tonal fields on desktop, with portfolio images in alternating editorial proportions, and a linear complete sequence on mobile rather than rounded card grids.
|
||||||
|
|
||||||
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
|
- **GIVEN** published services, cases and testimonials exist
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
- **AND** the portfolio section MUST use an olive tonal field with editorial image proportions
|
||||||
|
|
||||||
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
|
- **GIVEN** no published services, cases or testimonials
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the response MUST be 200
|
||||||
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
|
- **WHEN** the home is loaded in a real browser at desktop and mobile viewports
|
||||||
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
|
|
||||||
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
|
||||||
|
|
||||||
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
|
- **GIVEN** a published case with multiple gallery images
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the images MUST be rendered ordered by `sort_order`
|
||||||
|
- **AND** desktop presentation MUST alternate editorial image proportions without changing that order
|
||||||
|
|
||||||
|
#### Scenario: Listings paginate open-ended growth
|
||||||
|
|
||||||
|
- **WHEN** the number of published cases exceeds the page size
|
||||||
|
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
||||||
|
|
||||||
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. Contact, privacy and error surfaces MUST remain sober tonal layouts without decorative photography. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
|
- **WHEN** a visitor requests a non-existent public URL
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
- **AND** the page MUST use the public layout and offer navigation back to the home
|
||||||
|
|
||||||
|
#### Scenario: Server error hides internals in production
|
||||||
|
|
||||||
|
- **GIVEN** `APP_DEBUG` is false
|
||||||
|
- **WHEN** an unhandled exception occurs on a public route
|
||||||
|
- **THEN** the response MUST be a generic branded error page
|
||||||
|
- **AND** MUST NOT contain a stack trace, file path, or environment variable
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## 1. Editorial composition
|
||||||
|
|
||||||
|
- [x] 1.1 Add regression coverage for alternating public portfolio composition and sober functional surfaces.
|
||||||
|
- [x] 1.2 Recompose home portfolio, portfolio listing and case gallery with responsive editorial proportions while preserving order and responsive media delivery.
|
||||||
|
- [x] 1.3 Refine public content-page rhythm with tonal fields and desktop offsets without changing routes, CMS content or briefing behavior.
|
||||||
|
|
||||||
|
## 2. Verification
|
||||||
|
|
||||||
|
- [x] 2.1 Run focused feature and browser checks for public routes, motion, accessibility and navigation.
|
||||||
|
- [x] 2.2 Run formatting, static analysis, frontend build and strict OpenSpec validation without updating snapshots.
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
|
|
||||||
## 6. Phase 0 exit evidence
|
## 6. Phase 0 exit evidence
|
||||||
|
|
||||||
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
- [x] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output) — run 31395107465 deployed `7e68c0e` successfully; smoke recorded `/up`, `/` and `/admin/login` returning HTTP 200.
|
||||||
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
||||||
- [x] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any — flipped L2338 (FrankenPHP/Compose) and the auth/npm-audit/coverage bullets to `[x]`; left the staging hello-world bullet and the phase exit-criterion line unchecked (Dokploy deploy still failing, out of scope here)
|
- [x] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any — local runtime/auth/audit/coverage evidence was recorded by PR #39, and the staging exit criterion was subsequently marked complete from successful run 31395107465.
|
||||||
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
- [x] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green — local quality evidence is recorded in task 2.4; PR #39 passed `static`, `unit`, `feature`, `browser` and `container`, then run 31395107465 deployed that exact merge SHA and passed all three smoke endpoints.
|
||||||
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks (1–3) also complete
|
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks are complete — blocked by tasks 3.4 and 6.2, tracked separately in MAN-125.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: Public screens have desktop and mobile visual baselines
|
|
||||||
|
|
||||||
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio, Detalhe do portfólio, Sobre, Contato, Privacidade and branded 404, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
|
||||||
|
|
||||||
#### Scenario: Unintended visual change fails the suite
|
|
||||||
|
|
||||||
- **GIVEN** approved baselines exist
|
|
||||||
- **WHEN** a code change alters the rendering of a covered screen
|
|
||||||
- **THEN** the visual assertion MUST fail and report the diff
|
|
||||||
|
|
||||||
#### Scenario: Both viewports are covered
|
|
||||||
|
|
||||||
- **WHEN** the visual suite runs
|
|
||||||
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
|
||||||
|
|
||||||
#### Scenario: Heritage Editorial identity is captured
|
|
||||||
|
|
||||||
- **WHEN** approved baselines for the home are reviewed after this change
|
|
||||||
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
|
||||||
|
|
||||||
#### Scenario: Motion does not destabilize new baselines
|
|
||||||
|
|
||||||
- **WHEN** Sobre, Contato, Privacidade, or 404 is captured
|
|
||||||
- **THEN** the browser MUST use the final reduced-motion state before asserting the screenshot
|
|
||||||
2
openspec/changes/hero-editorial-spread/.openspec.yaml
Normal file
2
openspec/changes/hero-editorial-spread/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
48
openspec/changes/hero-editorial-spread/design.md
Normal file
48
openspec/changes/hero-editorial-spread/design.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
As imagens de abertura por rota, `ResponsiveImage`, texto alternativo obrigatório e fallback tonal já existem. O componente público atual, porém, põe o texto sobre a fotografia e a home mantém duas colunas iguais. A referência aprovada pede uma abertura editorial legível, reutilizável e linear sem JavaScript.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Reunir as cinco aberturas narrativas em um único componente Blade de spread 5/7.
|
||||||
|
- Limitar a coluna de leitura a 470 px e manter títulos com quebra normal, sem clipping ou overflow.
|
||||||
|
- Preservar a ordem DOM texto → mídia em mobile, mídia responsiva, foco, motion progressivo e fallback tonal.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Alterar campos CMS, uploads, dados, rotas, cópia, SEO/Open Graph, contato, privacidade, erros ou dependências.
|
||||||
|
- Criar ou atualizar snapshots, introduzir imagens externas ou transformar a rota de protótipo em funcionalidade de produção.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Um componente compartilhado recebe todo o conteúdo de abertura
|
||||||
|
|
||||||
|
`x-public.photo-hero` passa a renderizar texto, slot opcional e mídia tanto para as páginas institucionais quanto para a home. A home delega o seu conteúdo ao componente para impedir que os contratos de proporção, fallback e carregamento se separem. Duplicar uma segunda implementação da home foi descartado por já ter produzido divergência 50/50.
|
||||||
|
|
||||||
|
### Grade CSS 5/7 acima de `lg`; fluxo normal abaixo dela
|
||||||
|
|
||||||
|
No desktop, uma grade de doze colunas entrega 5/12 ao campo de texto e 7/12 à mídia. No mobile e tablet, a ordem do DOM produz texto seguido pela imagem de proporção 4/5. Posicionamento absoluto e painel sobre a foto foram descartados porque reduzem a largura de leitura previsível e dificultam a contenção do texto.
|
||||||
|
|
||||||
|
### Mídia de hero continua prioritária e dimensionada pela área real
|
||||||
|
|
||||||
|
A única imagem de abertura mantém `loading=eager`, `fetchpriority=high`, `object-cover` e o componente responsivo existente. Seu `sizes` informa `100vw` abaixo de `lg` e aproximadamente `58vw` acima dele, em vez dos antigos `50vw`/`100vw` indiscriminados. O fallback não emite imagem quando o campo está vazio.
|
||||||
|
|
||||||
|
### Motion preserva o conteúdo legível desde o HTML
|
||||||
|
|
||||||
|
Os atributos existentes de `page-open` e beats de mídia/título/CTA permanecem. Nenhum texto fica escondido por opacidade; movimento reduzido e ausência de JavaScript exibem o estado final.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Uma imagem de origem estreita pode recortar conteúdo importante em 7/12] → `object-cover` preserva o contrato atual; curadoria/crop da mídia continua sendo responsabilidade editorial.
|
||||||
|
- [Conteúdo de título excepcionalmente longo pode pressionar a coluna] → limite de leitura, quebra normal e testes de ausência de overflow evitam palavras cortadas.
|
||||||
|
- [Breakpoint pode atualizar depois de resize em browser test] → os testes aguardam dois frames de animação antes de medir a mídia.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Não há migração de dados. O deploy troca apenas markup e classes; o rollback do commit restaura a abertura anterior. Rotas sem imagem continuam no fallback tonal já previsto.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma. Fotografias autorizadas de produção continuam uma dependência de conteúdo, não desta mudança.
|
||||||
25
openspec/changes/hero-editorial-spread/proposal.md
Normal file
25
openspec/changes/hero-editorial-spread/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
As aberturas fotográficas já contam com mídia própria, mas a composição atual alterna uma imagem de fundo ou uma divisão 50/50 que achata a leitura editorial. Um spread consistente de texto 5/12 e imagem 7/12 reforça a hierarquia de WEB-01, WEB-02, WEB-03 e WEB-07 sem inventar conteúdo nem ampliar o CMS.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Define um componente único de abertura fotográfica em spread: conteúdo à esquerda em 5/12, fotografia à direita em 7/12, coluna de leitura limitada e imagem com recorte editorial.
|
||||||
|
- Aplica o mesmo contrato às aberturas narrativas da home, serviços, portfólio, detalhe de case e Sobre; telas menores linearizam texto antes da mídia.
|
||||||
|
- Mantém os campos de mídia existentes, carregamento prioritário apenas do hero, alternativa tonal sem imagem e a separação entre imagens editoriais e Open Graph.
|
||||||
|
- Mantém contato, privacidade e páginas de erro como superfícies tonais sóbrias.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- Nenhuma.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: as cinco aberturas narrativas passam a exigir o spread editorial 5/7 em desktop e uma sequência texto-para-mídia em telas menores.
|
||||||
|
- `content-media`: imagens de abertura passam a declarar tamanhos compatíveis com 100% da largura em mobile e 7/12 do viewport em desktop.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta os componentes Blade de hero público e da home, a documentação de design, os testes de renderização e browser das rotas públicas e as delta specs. Não altera rotas de produção, controladores, CMS, uploads, SEO/OG, dependências ou os itens fora do MVP em SPEC.md §4.2.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Editorial hero media declares its responsive spread width
|
||||||
|
Configured narrative hero media SHALL load eagerly with high priority and declare responsive sizes matching the rendered spread: full viewport width below the desktop breakpoint and seven twelfths of the viewport at desktop. Each configured hero image MUST retain meaningful alternative text; a missing image MUST use the tonal fallback without emitting media markup (SPEC §6.4, §6.6).
|
||||||
|
|
||||||
|
#### Scenario: A configured hero reports its responsive width
|
||||||
|
- **WHEN** a narrative route renders configured hero media
|
||||||
|
- **THEN** the image MUST declare `100vw` for smaller viewports and approximately `58vw` for the desktop spread
|
||||||
|
- **AND** it MUST use eager loading and high fetch priority
|
||||||
|
|
||||||
|
#### Scenario: An unconfigured hero emits no media
|
||||||
|
- **WHEN** a narrative route has no hero image path
|
||||||
|
- **THEN** the opening MUST not render an image element
|
||||||
|
- **AND** it MUST retain its readable tonal content
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Narrative openings use a readable editorial spread
|
||||||
|
The home, services, portfolio listing, portfolio detail, and About routes SHALL render configured hero media as a Heritage Editorial spread: a 5/12 text field and 7/12 media field on desktop, with a text-first linear sequence on smaller viewports. The text field MUST limit reading width to 470 px, preserve normal word wrapping, and avoid horizontal overflow or clipped text. Contact, privacy, and error surfaces MUST remain sober tonal layouts (WEB-01, WEB-02, WEB-03, WEB-07).
|
||||||
|
|
||||||
|
#### Scenario: A configured narrative opening renders the desktop spread
|
||||||
|
- **WHEN** a visitor loads a narrative route with its configured hero image at a desktop viewport
|
||||||
|
- **THEN** its text field MUST occupy five of twelve grid columns and its media field seven of twelve grid columns
|
||||||
|
- **AND** the media MUST use an editorial `object-cover` crop
|
||||||
|
|
||||||
|
#### Scenario: A configured narrative opening stacks without losing reading content
|
||||||
|
- **WHEN** a visitor loads a narrative route with its configured hero image below the desktop breakpoint
|
||||||
|
- **THEN** the text content MUST precede the media in DOM and visual order
|
||||||
|
- **AND** the route MUST not horizontally overflow or clip text
|
||||||
|
|
||||||
|
#### Scenario: A narrative opening has no configured image
|
||||||
|
- **WHEN** a visitor loads a narrative route without configured hero media
|
||||||
|
- **THEN** the route MUST render its intentional tonal fallback without an empty image request
|
||||||
|
|
||||||
|
#### Scenario: Functional routes remain sober
|
||||||
|
- **WHEN** a visitor loads contact or privacy
|
||||||
|
- **THEN** the route MUST use its sober tonal opening
|
||||||
|
- **AND** it MUST not render the narrative photo spread
|
||||||
14
openspec/changes/hero-editorial-spread/tasks.md
Normal file
14
openspec/changes/hero-editorial-spread/tasks.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
## 1. Editorial contract
|
||||||
|
|
||||||
|
- [x] 1.1 Record the approved 5/7 spread rule in the Heritage Editorial design guidance.
|
||||||
|
- [x] 1.2 Add failing feature and browser coverage for the 5/7 desktop grid, 470 px reading column, responsive image sizes, text-first mobile order, and contained text.
|
||||||
|
|
||||||
|
## 2. Shared opening implementation
|
||||||
|
|
||||||
|
- [x] 2.1 Rebuild the public photo hero as the single responsive spread component, retaining fallback, accessible media, motion attributes, optional content, and no-image behavior.
|
||||||
|
- [x] 2.2 Delegate the home opening to the shared component while retaining its CTAs, note, chapter marker, and motion hooks.
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 Run focused feature and browser/accessibility/motion checks without creating or updating screenshots.
|
||||||
|
- [x] 3.2 Run Pint, PHPStan, frontend build, and strict OpenSpec validation.
|
||||||
2
openspec/changes/package-detail-pages/.openspec.yaml
Normal file
2
openspec/changes/package-detail-pages/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-12
|
||||||
96
openspec/changes/package-detail-pages/design.md
Normal file
96
openspec/changes/package-detail-pages/design.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
`WeddingPackage` já cobre cards ordenados + CTA WhatsApp/briefing (change `restructure-home-weddings-corporate`, capability ainda só no delta completo). Campos atuais: `name`, `level`, `summary`, `scope_items` (json), `cta_label`, `sort_order`, `published_at`. Sem slug, sem corpo de detalhe, sem imagens. Portfólio (`portfolio.show`, `FindPublishedPortfolioCaseBySlug`, PageMeta, sitemap) é o padrão de detalhe a espelhar. Mock visual: `/home/manoelfreitas/Downloads/amare-assessoria-completa.html`. Tokens: `DESIGN.md` / `resources/css/tokens.css` (Heritage Editorial) — não Cormorant/Inter do HTML estático.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Detalhe CMS-driven `/pacotes/{slug}` para as 3 modalidades, publish-gated.
|
||||||
|
- Composição mock → tokens do projeto (olive accent, EB Garamond via stack existente, bg cream).
|
||||||
|
- CTA final = mesmo canal contextual dos cards (`wa.me` ou `/briefing?servico_interesse=`).
|
||||||
|
- SEO mínimo: meta por registro, canonical, OG image (hero ou default), entrada no sitemap.
|
||||||
|
- Admin Filament editável sem deploy de copy.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Rota índice `/pacotes`; substituir cards da home; WhatsApp Business API; preços; multi-idioma; Livewire público; gallery multi-imagem além de hero + audience.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Estender `WeddingPackage`, não criar modelo novo
|
||||||
|
|
||||||
|
Uma entidade comercial já existe e é o que home/serviços já consomem. Detalhe é projeção do mesmo agregado. Modelo paralelo geraria sync de publicação/slug e viola YAGNI.
|
||||||
|
|
||||||
|
### Slug único + publicação por `published_at`
|
||||||
|
|
||||||
|
Igual portfólio: URL estável, scope `published()`, query Application `FindPublishedWeddingPackageBySlug`. Draft/inexistente → 404 HTTP, sem vazar campos internos.
|
||||||
|
|
||||||
|
### Campos de detalhe (additive)
|
||||||
|
|
||||||
|
| Campo | Uso |
|
||||||
|
|-------|-----|
|
||||||
|
| `slug` | string unique |
|
||||||
|
| `eyebrow` | ex. "Assessoria" |
|
||||||
|
| `title_line` | parte romana do H1 |
|
||||||
|
| `title_emphasis` | parte itálica do H1 |
|
||||||
|
| `hero_lead` | parágrafo do hero |
|
||||||
|
| `hero_image_path` / `hero_image_alt` | foto hero |
|
||||||
|
| `benefits` | jsonb `[{icon_key,label}]` — faixa escura |
|
||||||
|
| `included_items` | jsonb `[{icon_key,title,description}]` — grid 3 col |
|
||||||
|
| `audience_heading` | default editorial se vazio ok |
|
||||||
|
| `audience_intro` | texto |
|
||||||
|
| `audience_points` | jsonb `string[]` checklist |
|
||||||
|
| `audience_image_path` / `audience_image_alt` | foto 50/50 |
|
||||||
|
| `final_cta_heading` / `final_cta_body` | bloco final |
|
||||||
|
| `meta_title` / `meta_description` | SEO opcional |
|
||||||
|
|
||||||
|
Manter `summary` + `scope_items` para cards. Não migrar cards para `included_items` neste ciclo.
|
||||||
|
|
||||||
|
### Catálogo fechado de ícones (`PackageIconCatalog`)
|
||||||
|
|
||||||
|
Select Filament → `icon_key` string. Map PHP/Blade para SVGs inline (ou partials). Sem upload de SVG arbitrário (XSS/ops). Chaves mínimas cobrem mock (ex.: calendar, checklist, users, map, heart, spark — nomes estáveis kebab).
|
||||||
|
|
||||||
|
### Hero de pacote vs `photo-hero` genérico
|
||||||
|
|
||||||
|
Mock exige H1 bipartido (linha + ênfase itálica), divisor vertical, CTA outline no hero e foto. Preferir componente dedicado `x-public.package-hero` (ou estender `photo-hero` só se API ficar genérica sem branching feio). Seções: `package-benefits`, `package-included`, `package-audience`, reutilizar `final-cta` se encaixar.
|
||||||
|
|
||||||
|
### CTA WhatsApp
|
||||||
|
|
||||||
|
Reutilizar VO/helper já usado nos cards (digits de `SiteSetting.whatsapp_number`, mensagem com nome da modalidade). Mesma regra de fallback briefing. Não duplicar lógica de normalização.
|
||||||
|
|
||||||
|
### Links a partir dos cards
|
||||||
|
|
||||||
|
Card: título/área → `route('packages.show', $package)` quando slug presente; botão CTA continua WhatsApp/briefing. Sem quebrar testes de CTA existentes.
|
||||||
|
|
||||||
|
### Imagens
|
||||||
|
|
||||||
|
`PublicImageUploadRules` + disk público existente + `x-media.image`. Paths nullable; se hero ausente, layout degrada (omitir slot de imagem).
|
||||||
|
|
||||||
|
### Camada Application
|
||||||
|
|
||||||
|
- `FindPublishedWeddingPackageBySlug`
|
||||||
|
- Estender `GetSitemapEntries` com slugs de pacotes publicados
|
||||||
|
- PageMeta no controller (espelhar PortfolioCaseController)
|
||||||
|
|
||||||
|
Sem repository genérico / BaseAction.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Capability `wedding-packages` ainda não está em `openspec/specs/`] → delta ADDED nesta change; ao arquivar, consolidar com requirements de cards do change completo ou arquivar ambos em ordem.
|
||||||
|
- [Copy das 3 modalidades incompleta] → seeder: Essenza = mock; Conduzione/Grand Jour estrutura paralela adaptada; admin pode editar.
|
||||||
|
- [Ícones insuficientes no catálogo] → adicionar chave no map + opção Filament; sem free-text SVG.
|
||||||
|
- [Cards sem slug durante migrate] → migration backfill slug a partir do name; seeder garante os 3 oficiais.
|
||||||
|
- [Duplicação visual mock vs tokens] → testes de tokens existentes + asserts de classes/roles; não copiar hex do HTML.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Deploy migration additive + código + assets Blade juntos.
|
||||||
|
2. Rodar seeder/update das 3 modalidades (slug + detalhe).
|
||||||
|
3. Verificar `/pacotes/essenza` (etc.), 404 draft, sitemap, CTA WA.
|
||||||
|
4. Rollback: remover rota/views; colunas additive podem permanecer; unpublish pacotes se necessário.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Nenhuma bloqueante (decisões de produto já confirmadas: detalhe-only, extend model, 3 modalidades, WA CTA).
|
||||||
|
- WEB-ID dedicado a package detail: não existe no SPEC; rastrear via WEB-02 + padrão WEB-03 até SPEC ganhar ID explícito.
|
||||||
32
openspec/changes/package-detail-pages/proposal.md
Normal file
32
openspec/changes/package-detail-pages/proposal.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Modalidades de casamento (Essenza, Conduzione, Grand Jour) existem no CMS e na home/serviços só como cards com CTA WhatsApp. Não há página de detalhe: o visitante não consegue ler o que está incluso, para quem é o pacote ou prova visual editorial antes de converter. O mock aprovado (`amare-assessoria-completa.html`) define a jornada; falta rota CMS-driven `/pacotes/{slug}` alinhada a WEB-02/WEB-03 e ao padrão de detalhe do portfólio.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Estender `WeddingPackage` com slug estável, campos editoriais de detalhe (hero, benefícios, itens inclusos, audiência, imagens, SEO opcional) sem remover `summary`/`scope_items` dos cards.
|
||||||
|
- Expor rota pública `packages.show` em `/pacotes/{slug}`: só publicados; rascunho → 404 (mesmo contrato de `portfolio.show`).
|
||||||
|
- Renderizar página de detalhe Heritage Editorial espelhando o mock (hero split, faixa de benefícios, grid “O que está incluso”, bloco audiência, CTA final WhatsApp contextual com fallback briefing).
|
||||||
|
- Incluir slugs publicados no sitemap; meta title/description/canonical/OG por pacote.
|
||||||
|
- Filament: seções de detalhe + catálogo fechado de ícones SVG; seeder preenche as 3 modalidades.
|
||||||
|
- Cards na home e em `/servicos` passam a linkar para o detalhe (navegação secundária); CTA primário permanece WhatsApp/briefing.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `wedding-packages`: modalidades publicáveis com detalhe por slug (baseline ainda não arquivada em `openspec/specs/`; esta change define o contrato de detalhe e reafirma cards/CTA já entregues).
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: incluir rota `packages.show` (`/pacotes/{slug}`) e regra de 404 para modalidade não publicada; cards de modalidade podem apontar ao detalhe.
|
||||||
|
- `public-seo`: sitemap e metadados de página para modalidades publicadas.
|
||||||
|
- `service-catalog`: listagem de serviços/casamentos referencia o detalhe da modalidade quando existir slug publicado.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Migration/model/factory/seeder `WeddingPackage`; query `FindPublishedWeddingPackageBySlug`; controller + rota + view Blade; componentes de seção; `GetSitemapEntries` / PageMeta; Filament `WeddingPackageResource`; links em partials de packages (home/serviços); testes feature (200/404, sitemap, CTA, conteúdo). Atende extensão de WEB-01/WEB-02 e o padrão de detalhe de WEB-03. Sem page builder, sem índice `/pacotes`, sem integração oficial WhatsApp, sem CRM (SPEC.md §4.2).
|
||||||
|
|
||||||
|
## Não objetivos
|
||||||
|
|
||||||
|
- Índice de pacotes, substituir `/servicos`, builder genérico, preços dinâmicos, checkout, multi-idioma, Livewire no site público, inventar prova ou cases Corporate (SPEC.md §4.2).
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Every public page emits title, description and canonical
|
||||||
|
|
||||||
|
The system SHALL render a unique `<title>`, a `<meta name="description">` and a `<link rel="canonical">` on every public route (SPEC §6.6, §19). Portfolio cases and wedding modalities MUST use `meta_title`/`meta_description` when filled and fall back to title/name and summary/hero lead otherwise. Pages without page-level metadata MUST fall back to `default_meta_title` and `default_meta_description` from `site_settings`.
|
||||||
|
|
||||||
|
#### Scenario: Page-level metadata overrides defaults
|
||||||
|
|
||||||
|
- **GIVEN** a published case with `meta_title` and `meta_description` filled
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the rendered title and description MUST use the case values
|
||||||
|
|
||||||
|
#### Scenario: Wedding modality metadata overrides defaults
|
||||||
|
|
||||||
|
- **GIVEN** a published wedding modality with `meta_title` and `meta_description` filled
|
||||||
|
- **WHEN** a visitor loads `/pacotes/{slug}`
|
||||||
|
- **THEN** the rendered title and description MUST use the modality values
|
||||||
|
|
||||||
|
#### Scenario: Missing metadata falls back to site defaults
|
||||||
|
|
||||||
|
- **GIVEN** a published case without `meta_title`
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the rendered title MUST be derived from the case title
|
||||||
|
- **AND** the description MUST fall back to the case summary or the site default
|
||||||
|
|
||||||
|
#### Scenario: Canonical points to the absolute route URL
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** the canonical URL MUST be the absolute URL of that route without query parameters
|
||||||
|
|
||||||
|
### Requirement: Open Graph metadata is emitted for sharing
|
||||||
|
|
||||||
|
The system SHALL emit Open Graph tags (`og:title`, `og:description`, `og:type`, `og:url`, `og:image`) on public pages. The image MUST use the page cover/hero image when available and `default_og_image_path` from `site_settings` otherwise.
|
||||||
|
|
||||||
|
#### Scenario: Case detail uses its cover as OG image
|
||||||
|
|
||||||
|
- **GIVEN** a published case with a cover image
|
||||||
|
- **WHEN** the case detail is rendered
|
||||||
|
- **THEN** `og:image` MUST reference the case cover image URL
|
||||||
|
|
||||||
|
#### Scenario: Wedding modality detail uses hero as OG image
|
||||||
|
|
||||||
|
- **GIVEN** a published wedding modality with a hero image
|
||||||
|
- **WHEN** `/pacotes/{slug}` is rendered
|
||||||
|
- **THEN** `og:image` MUST reference the modality hero image URL
|
||||||
|
|
||||||
|
#### Scenario: Pages without cover use the default OG image
|
||||||
|
|
||||||
|
- **WHEN** a page without its own image is rendered
|
||||||
|
- **THEN** `og:image` MUST reference `default_og_image_path`
|
||||||
|
|
||||||
|
### Requirement: Sitemap and robots are served by the application
|
||||||
|
|
||||||
|
The system SHALL serve `/sitemap.xml` listing the home, institutional routes, the services listing, the portfolio listing, every published case slug, and every published wedding modality slug with its last modification date. `/robots.txt` MUST be served by an application route referencing the sitemap URL.
|
||||||
|
|
||||||
|
#### Scenario: Sitemap contains only published slugs
|
||||||
|
|
||||||
|
- **GIVEN** one published case and one draft case
|
||||||
|
- **WHEN** `/sitemap.xml` is requested
|
||||||
|
- **THEN** the response MUST include the published slug
|
||||||
|
- **AND** MUST NOT include the draft slug
|
||||||
|
|
||||||
|
#### Scenario: Sitemap contains published wedding modality slugs
|
||||||
|
|
||||||
|
- **GIVEN** one published wedding modality and one draft wedding modality
|
||||||
|
- **WHEN** `/sitemap.xml` is requested
|
||||||
|
- **THEN** the response MUST include `/pacotes/{published-slug}`
|
||||||
|
- **AND** MUST NOT include the draft modality slug
|
||||||
|
|
||||||
|
#### Scenario: Newly published case enters the sitemap
|
||||||
|
|
||||||
|
- **WHEN** an admin publishes a case
|
||||||
|
- **THEN** the case slug MUST appear in `/sitemap.xml` on the next request
|
||||||
|
|
||||||
|
#### Scenario: Newly published wedding modality enters the sitemap
|
||||||
|
|
||||||
|
- **WHEN** an admin publishes a wedding modality
|
||||||
|
- **THEN** the modality package URL MUST appear in `/sitemap.xml` on the next request
|
||||||
|
|
||||||
|
#### Scenario: Robots references the sitemap
|
||||||
|
|
||||||
|
- **WHEN** `/robots.txt` is requested
|
||||||
|
- **THEN** the response MUST be `text/plain`
|
||||||
|
- **AND** MUST contain the absolute `/sitemap.xml` URL
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public routes serve published content without authentication
|
||||||
|
|
||||||
|
The system SHALL expose the public routes of SPEC §5.1 plus the additive package detail route: `home` (`/`), `services.index` (`/servicos`), `packages.show` (`/pacotes/{slug}`), `portfolio.index` (`/portfolio`), `portfolio.show` (`/portfolio/{slug}`), `about` (`/sobre`), `contact` (`/contato`) and `privacy` (`/privacidade`). Every public route MUST respond without authentication and MUST NOT expose unpublished content, internal fields, or internal notes (SPEC §19).
|
||||||
|
|
||||||
|
#### Scenario: Guest reaches every public route
|
||||||
|
|
||||||
|
- **WHEN** an unauthenticated visitor requests any public route listed above that has published content where required
|
||||||
|
- **THEN** the response status MUST be 200
|
||||||
|
- **AND** no redirect to `/admin/login` MUST occur
|
||||||
|
|
||||||
|
#### Scenario: Unpublished content is invisible
|
||||||
|
|
||||||
|
- **GIVEN** a service, portfolio case, wedding modality, or testimonial with `published_at` null
|
||||||
|
- **WHEN** a visitor loads the corresponding public listing or home section
|
||||||
|
- **THEN** the record MUST NOT appear in the rendered output
|
||||||
|
|
||||||
|
#### Scenario: Unpublished case detail returns 404
|
||||||
|
|
||||||
|
- **GIVEN** a portfolio case saved as draft
|
||||||
|
- **WHEN** a visitor requests `/portfolio/{slug}` for that case
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
|
||||||
|
#### Scenario: Unpublished wedding modality detail returns 404
|
||||||
|
|
||||||
|
- **GIVEN** a wedding modality saved as draft
|
||||||
|
- **WHEN** a visitor requests `/pacotes/{slug}` for that modality
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
|
||||||
|
#### Scenario: Published case detail becomes reachable
|
||||||
|
|
||||||
|
- **GIVEN** a portfolio case saved as draft
|
||||||
|
- **WHEN** an admin fills the required fields and publishes the case
|
||||||
|
- **THEN** `/portfolio/{slug}` MUST respond 200
|
||||||
|
- **AND** the case MUST appear in the `/portfolio` listing
|
||||||
|
|
||||||
|
#### Scenario: Published wedding modality detail becomes reachable
|
||||||
|
|
||||||
|
- **GIVEN** a wedding modality saved as draft with required detail fields
|
||||||
|
- **WHEN** an admin publishes the modality
|
||||||
|
- **THEN** `/pacotes/{slug}` MUST respond 200
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Wedding modality detail uses Heritage Editorial public layout
|
||||||
|
The system SHALL render `/pacotes/{slug}` with the public layout (header/footer shared), Heritage Editorial tokens, and section order: package hero (eyebrow, bipartite title, lead, outline secondary actions as designed, hero media), dark benefit strip, included-items grid, audience split (media + checklist), final conversion CTA. The page MUST be server-rendered Blade without a public Livewire dependency. Missing optional images MUST NOT break the page (omit or degrade the media slot).
|
||||||
|
|
||||||
|
#### Scenario: Detail page section order
|
||||||
|
- **GIVEN** a published modality with complete detail content
|
||||||
|
- **WHEN** a visitor loads `/pacotes/{slug}`
|
||||||
|
- **THEN** the document MUST include the hero, benefits, included items, audience and final CTA regions in that order
|
||||||
|
- **AND** MUST use the public layout chrome
|
||||||
|
|
||||||
|
#### Scenario: Detail page has no console errors
|
||||||
|
- **WHEN** the modality detail is loaded in a real browser at desktop and mobile viewports
|
||||||
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Services wedding section links to modality detail
|
||||||
|
When the services page renders published wedding modalities, each modality card SHALL expose a link to `/pacotes/{slug}` for published records that have a slug, while keeping the existing WhatsApp/briefing conversion CTA.
|
||||||
|
|
||||||
|
#### Scenario: Services card navigates to package detail
|
||||||
|
- **GIVEN** at least one published wedding modality with a slug
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** the wedding modalities region MUST include a link to that modality's `/pacotes/{slug}` URL
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Wedding modalities are managed as publishable ordered content
|
||||||
|
The system SHALL let an admin manage `WeddingPackage` records with name, unique slug, level, summary, ordered scope items, CTA label, sort order and `published_at`. Only published records SHALL appear in public wedding card sections, in ascending `sort_order`.
|
||||||
|
|
||||||
|
#### Scenario: Published modalities follow CMS order
|
||||||
|
- **GIVEN** published and draft wedding modalities with different sort orders
|
||||||
|
- **WHEN** a visitor loads the home or services page
|
||||||
|
- **THEN** only published modalities appear in ascending `sort_order`
|
||||||
|
|
||||||
|
#### Scenario: Official initial modalities are available
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** Essenza, Conduzione and Grand Jour are created as the official initial modalities with stable unique slugs
|
||||||
|
|
||||||
|
### Requirement: Wedding modality CTA has a contextual channel and conversion fallback
|
||||||
|
The system SHALL build a WhatsApp deeplink containing the selected modality when `whatsapp_number` is valid. When it is missing or invalid, the CTA SHALL point to `/briefing` with the modality prefilled as `servico_interesse`. The same rule MUST apply on modality cards and on the modality detail page final CTA.
|
||||||
|
|
||||||
|
#### Scenario: Valid WhatsApp setting creates contextual link
|
||||||
|
- **GIVEN** a valid official WhatsApp number
|
||||||
|
- **WHEN** a visitor activates a wedding modality CTA on a card or detail page
|
||||||
|
- **THEN** the link targets `wa.me` with a URL-encoded message naming that modality
|
||||||
|
|
||||||
|
#### Scenario: Missing WhatsApp setting preserves briefing conversion
|
||||||
|
- **GIVEN** no valid official WhatsApp number
|
||||||
|
- **WHEN** a visitor activates a wedding modality CTA on a card or detail page
|
||||||
|
- **THEN** the visitor reaches `/briefing` with `servico_interesse` prefilled
|
||||||
|
|
||||||
|
### Requirement: Wedding modality detail content is CMS-managed
|
||||||
|
The system SHALL store per-modality detail fields: eyebrow, bipartite title (`title_line`, `title_emphasis`), hero lead, optional hero image with alt, ordered benefits (`icon_key`, label), ordered included items (`icon_key`, title, description), audience heading/intro/points and optional audience image with alt, final CTA heading/body, and optional `meta_title` / `meta_description`. Icon keys MUST come from a closed catalog enforced in admin validation. `summary` and `scope_items` MUST remain available for card surfaces.
|
||||||
|
|
||||||
|
#### Scenario: Admin can edit detail sections
|
||||||
|
- **WHEN** an admin opens a WeddingPackage in Filament
|
||||||
|
- **THEN** the form MUST expose detail sections for hero, benefits, included items, audience, final CTA and SEO
|
||||||
|
- **AND** icon fields MUST offer only catalog keys
|
||||||
|
|
||||||
|
#### Scenario: Seeder fills detail for official modalities
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** each official modality MUST have slug and non-empty hero lead plus at least one benefit and one included item suitable for public render
|
||||||
|
|
||||||
|
### Requirement: Published modality detail page is publicly reachable by slug
|
||||||
|
The system SHALL expose `packages.show` at `/pacotes/{slug}` without authentication. Only modalities with non-null `published_at` in the past or present MUST resolve. Draft or unknown slugs MUST return HTTP 404 without exposing internal fields.
|
||||||
|
|
||||||
|
#### Scenario: Published modality detail returns 200
|
||||||
|
- **GIVEN** a published wedding modality with slug `essenza`
|
||||||
|
- **WHEN** a visitor requests `/pacotes/essenza`
|
||||||
|
- **THEN** the response status MUST be 200
|
||||||
|
- **AND** the page MUST render hero title parts, benefits, included items, audience block and final CTA using CMS values
|
||||||
|
|
||||||
|
#### Scenario: Draft modality detail returns 404
|
||||||
|
- **GIVEN** a wedding modality saved as draft
|
||||||
|
- **WHEN** a visitor requests `/pacotes/{slug}` for that modality
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
|
||||||
|
#### Scenario: Cards link to the detail page
|
||||||
|
- **GIVEN** a published modality with a slug
|
||||||
|
- **WHEN** a visitor views the home or services wedding cards
|
||||||
|
- **THEN** a navigation control MUST link to `/pacotes/{slug}`
|
||||||
|
- **AND** the primary conversion CTA MUST remain the WhatsApp or briefing channel
|
||||||
29
openspec/changes/package-detail-pages/tasks.md
Normal file
29
openspec/changes/package-detail-pages/tasks.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## 1. Data model and domain catalog
|
||||||
|
|
||||||
|
- [x] 1.1 Migration additive em `wedding_packages`: `slug` unique (backfill a partir de `name`), campos de detalhe (eyebrow, title_line, title_emphasis, hero_lead, hero_image_*, benefits jsonb, included_items jsonb, audience_*, final_cta_*, meta_*), atualizar model `$fillable`/casts e factory. Evidência: `database/migrations/2026_08_12_120000_extend_wedding_packages_for_detail_pages.php` (roda limpo), `app/Models/WeddingPackage.php` (fillable+casts+booted slug), `database/factories/WeddingPackageFactory.php` (slug + campos de detalhe).
|
||||||
|
- [x] 1.2 `PackageIconCatalog` (chaves fechadas + labels pt-BR) e validação de `icon_key` em benefits/included_items. Evidência: `app/Domain/Marketing/PackageIconCatalog.php` (6 chaves: calendar/checklist/users/map/heart/spark), Select icon_key em `WeddingPackageForm` usa `PackageIconCatalog::labels()`.
|
||||||
|
- [x] 1.3 Feature/unit: factory published com slug; scope published inalterado; icon_key inválido rejeitado na validação admin/request path usado pelo Filament. Evidência: `WeddingPackageTest` (4 testes: scope published + seeder, factory slug autofill, icon catalog aceita/rejeita `paw`).
|
||||||
|
|
||||||
|
## 2. Application + public route
|
||||||
|
|
||||||
|
- [x] 2.1 Query `FindPublishedWeddingPackageBySlug` + teste 200/null para draft. Evidência: `app/Application/Queries/Marketing/FindPublishedWeddingPackageBySlug.php`; `FindPublishedWeddingPackageBySlugTest` (retorna published, null para draft/inexistente).
|
||||||
|
- [x] 2.2 `PackageController@show`, rota nomeada `packages.show` `/pacotes/{slug}`, PageMeta (meta_title/description fallback name/summary|hero_lead, OG hero). Evidência: `app/Http/Controllers/PublicSite/PackageController.php`, `routes/web.php` (`packages.show`), `PageMeta::forPackage` (title=meta_title??title_line??name, description=meta_description??summary, ogImageUrl=hero_image_path).
|
||||||
|
- [x] 2.3 Estender `GetSitemapEntries` com pacotes publicados; teste sitemap inclui/exclui draft. Evidência: `GetSitemapEntries` appends pacotes published; `SitemapTest::test_sitemap_includes_only_published_packages` (inclui `essenza`, exclui `rascunho-pacote`).
|
||||||
|
|
||||||
|
## 3. Blade detail (Heritage Editorial)
|
||||||
|
|
||||||
|
- [x] 3.1 View `pages/packages/show` + componentes `package-hero`, `package-benefits`, `package-included`, `package-audience`; layout `layouts.public`; tokens DESIGN.md (sem hex do mock). Evidência: `resources/views/pages/packages/show.blade.php` + `resources/views/components/public/package-{hero,benefits,included,audience}.blade.php` + `resources/views/components/package/icon.blade.php` (SVG de catálogo); classes `amare-*` e `text-hero-spread` (tokens), sem hex do mock.
|
||||||
|
- [x] 3.2 CTA final reutiliza builder WhatsApp/briefing dos cards; teste feature assert `wa.me` / `briefing?servico_interesse`. Evidência: `app/Support/PackageContactLink::for()` (helper compartilhado), `package-final-cta.blade.php` usa builder; `PackageDetailTest` (tests 4-5: wa.me com número, briefing fallback).
|
||||||
|
- [x] 3.3 Feature: render de seções a partir do CMS; 404 draft; guest sem auth. Evidência: `PackageDetailTest` (5 testes: render de seções CMS assertSee, 404 draft, 404 slug inexistente, CTA wa.me/briefing).
|
||||||
|
|
||||||
|
## 4. Filament + seeder + card links
|
||||||
|
|
||||||
|
- [x] 4.1 `WeddingPackageResource`: seções Hero, Benefícios, Inclusos, Audiência, CTA final, SEO; FileUpload imagens; Select icon_key do catálogo. Evidência: `app/Filament/Resources/WeddingPackages/Schemas/WeddingPackageForm.php` (sections Identificação/Hero/Diferenciais/Inclusos/Audiência/CTA final/SEO; `PublicImageUploadRules::fileUpload/altTextField`; `Select icon_key` options `PackageIconCatalog::labels()`); `WeddingPackagesTable` ganhou coluna `slug`.
|
||||||
|
- [x] 4.2 Seeder/update Essenza (copy mock), Conduzione e Grand Jour (estrutura paralela) com slugs `essenza`, `conduzione`, `grand-jour`. Evidência: `database/seeders/WeddingPackagesSeeder.php` (3 pacotes com slug + campos de detalhe completos); `WeddingPackageTest::test_content_seeder_creates_official_wedding_modalities` verde.
|
||||||
|
- [x] 4.3 Home + `/servicos` cards: link secundário para `packages.show`; CTA primário WA intacto; testes de regressão de CTA. Evidência: `home/packages.blade.php` + `home/wedding-packages.blade.php` + `services/index.blade.php` (link 'Conhecer esta modalidade' + CTA primário refatorado via `PackageContactLink`); `HomePageContentTest` (22 testes: 143 assertions, incl. href packages.show).
|
||||||
|
|
||||||
|
## 5. Verification gate
|
||||||
|
|
||||||
|
- [x] 5.1 `composer pint:check` + `composer phpstan` no escopo alterado. Evidência: `vendor/bin/pint --test` passou (fix `ordered_imports` em `routes/web.php`); `composer phpstan` (level 5, `--memory-limit=1G --debug`) = No errors.
|
||||||
|
- [x] 5.2 `composer test:unit` + `composer test:feature` verdes (incl. novos testes de pacote). Evidência: `test:unit` 42 passed (186 assertions); `test:feature` 193 passed (1182 assertions); `PackageDetailTest` (5), `FindPublishedWeddingPackageBySlugTest`, `SitemapTest` (3) verdes.
|
||||||
|
- [x] 5.3 `openspec validate package-detail-pages` e marcar tasks só com evidência. Evidência: `openspec validate package-detail-pages` = "Change 'package-detail-pages' is valid"; tasks 1.1-5.3 marcadas acima com referências.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The public site currently sends every commercial CTA to `/contato`, where the detailed event briefing lives. Its home chapters reflect the original editorial sequence, while the CMS only has generic `Service` records. Production seed gating already distinguishes environmental content but the public portfolio needs an explicit empty-acervo state so no demonstrative case can be mistaken for proof.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Keep the public surface server-rendered Blade with progressive vanilla JS and a single Heritage Editorial visual language.
|
||||||
|
- Make wedding modalities typed, publishable, ordered CMS content; derive their WhatsApp CTA server-side with an explicit briefing fallback.
|
||||||
|
- Split prospect and partner submissions at route, request, controller, mail and success-state boundaries.
|
||||||
|
- Keep public data minimised: partner submissions are mailed, never persisted.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- No WhatsApp API/integration, CRM Lead creation, partner directory, Corporate service inventory, Corporate case, invented evidence or page builder.
|
||||||
|
- No production migration of fixture cases into real proof; the authorized client content remains an operational prerequisite.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Typed `WeddingPackage` content instead of a JSON setting
|
||||||
|
|
||||||
|
`wedding_packages` has a dedicated model, migration, factory, policy, Filament resource and public query. This matches the existing publication/order model for Services and keeps content validation, ordering and authorization visible. A JSON array in `site_settings` would be faster initially but would weaken publication control and future editing.
|
||||||
|
|
||||||
|
### WhatsApp is a deeplink, not an integration
|
||||||
|
|
||||||
|
The setting stores digits-normalized `whatsapp_number`; a small value object builds `https://wa.me/<digits>?text=<encoded contextual message>`. If the setting is absent or invalid, the same CTA points to `/briefing?servico_interesse=<package>` so conversion still works. This offers the authorized official channel without violating SPEC §4.2's prohibition on an official integration.
|
||||||
|
|
||||||
|
### Separate partner boundary, not a mode on briefing
|
||||||
|
|
||||||
|
`PartnerInquiryRequest`, `PartnerInquiryController` and `PartnerInquiry` mailable have their own payload, honeypot key, throttle and session duplicate hash. It prevents event fields, marketing origin and confirmations from crossing into the partnership flow. A single controller with a discriminated input was rejected because it increases the chance of accidental routing or data disclosure.
|
||||||
|
|
||||||
|
### Honest public availability states
|
||||||
|
|
||||||
|
The home consumes `GetHomeContent`, which returns only actual published cases in production. The portfolio chapter itself remains present and shows an acervo-em-preparação note on empty data. Non-production visual fixtures continue to seed only for development/staging. Corporate remains copy plus a briefing CTA, deliberately without data-driven claims, media or a service list.
|
||||||
|
|
||||||
|
### Navigation links use canonical home fragments
|
||||||
|
|
||||||
|
Header/footer link to `/#amare`, `/#casamentos`, `/#corporate`, `/#portfolio` and `/#depoimentos` from other routes, and native fragments on the home. This preserves deep links and no-JS behavior; CSS scroll margin compensates for the sticky header.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Official WhatsApp number is not configured at deploy] → every package CTA has a deterministic briefing fallback and settings label calls out the operational requirement.
|
||||||
|
- [Client proof is still unavailable] → production renders no fixture cases, a visible acervo note and no Corporate cases.
|
||||||
|
- [New contact route changes incoming links] → `/contato` remains available for partners and header/footer continue to expose it; event CTA links move deliberately to `/briefing`.
|
||||||
|
- [Detailed forms duplicate markup] → shared field styles and narrow controllers are preferred over generic, condition-heavy form components.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Deploy the additive table/column migrations and code together.
|
||||||
|
2. Run the content seeder to install official wedding modalities; set the official WhatsApp number in Filament before promoting CTAs.
|
||||||
|
3. In production, leave portfolio cases unpublished until real authorized material is loaded; verify the empty-acervo state.
|
||||||
|
4. Rollback is code-safe because routes remain additive and the legacy setting data is untouched; if necessary, unset the new WhatsApp column and retain the package rows without public publication.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Corporate institutional copy and authorized proof (MAN-98/MAN-99) remain client dependencies; no placeholder claims will be added.
|
||||||
|
- The official WhatsApp number must be supplied before the campaign links are promoted.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
A home atual apresenta os conteúdos do CMS, mas não conduz com clareza as duas frentes da Amare nem separa o pedido de proposta da relação com fornecedores. A estrutura de lançamento precisa tornar Casamentos e Corporate compreensíveis sem inventar prova, preservar o acervo real como requisito de publicação e abrir um canal mínimo, privado e específico para parcerias.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Reorganizar a home em capítulos editoriais ancoráveis: apresentação, Amare, vertentes, Casamentos, Corporate, acervo, depoimentos e fechamento.
|
||||||
|
- Adicionar modalidades de casamento publicáveis e ordenáveis no CMS, com conteúdo inicial oficial para Essenza, Conduzione e Grand Jour.
|
||||||
|
- Adicionar número oficial de WhatsApp às configurações e tornar as CTAs das modalidades contextuais, com fallback para o briefing.
|
||||||
|
- Mover o formulário de pedido de proposta para `/briefing`; transformar `/contato` em uma jornada independente para fornecedores e parcerias.
|
||||||
|
- Enviar a consulta de parceiro por e-mail próprio, sem criar Lead ou persistir dados; manter CSRF, honeypot, rate limit, validação e aviso de privacidade proporcional à finalidade.
|
||||||
|
- Manter Corporate como apresentação institucional e CTA até haver copy, fotos e cases autorizados; em produção, não publicar fixtures de portfólio e informar que o acervo está em preparação.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `wedding-packages`: modalidades de casamento gerenciadas no CMS e CTAs contextuais de WhatsApp/briefing.
|
||||||
|
- `partner-inquiries`: formulário público mínimo, privado e não persistente para fornecedores e parcerias.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: nova estrutura editorial da home, âncoras e separação entre briefing e contato de parceiros.
|
||||||
|
- `service-catalog`: aprofundamento de serviços passa a apresentar as modalidades de casamento publicadas.
|
||||||
|
- `site-settings`: configurações tipadas passam a incluir o número oficial de WhatsApp.
|
||||||
|
- `transactional-email`: e-mail operacional separado para consultas de parceiros.
|
||||||
|
- `content-media`: fixtures de casos ficam limitadas a desenvolvimento e staging; produção só mostra acervo real publicado.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta rotas, controllers, requests, mailables, modelos/migrations/seeders, Filament, consultas de marketing, componentes Blade, navegação, estilos e testes feature/browser. A mudança atende WEB-01, WEB-02, WEB-03, WEB-05, WEB-06 e WEB-07, sem introduzir integração oficial com WhatsApp, CRM, persistência de leads, marketplace ou outros itens fora do MVP (SPEC.md §4.2).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Production never promotes visual fixture cases as Amare proof
|
||||||
|
The content seeder SHALL restrict visual fixture portfolio cases to non-production environments. Production public output SHALL only render cases that were explicitly loaded and published as authorized content.
|
||||||
|
|
||||||
|
#### Scenario: Production seed does not create fixture cases
|
||||||
|
- **WHEN** the content seeder runs with the production environment
|
||||||
|
- **THEN** no fixture portfolio case or fixture portfolio image is published
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Partners have a separate, non-persistent contact journey
|
||||||
|
The system SHALL expose `/contato` as a supplier and partnership form. It MUST collect name, optional professional/company name, e-mail, service/activity, coverage area, message, optional portfolio/social link and optional telephone. It SHALL send a dedicated internal e-mail and MUST NOT persist a Lead or partner record.
|
||||||
|
|
||||||
|
#### Scenario: Valid partner inquiry is delivered separately
|
||||||
|
- **WHEN** a visitor submits valid partner data on `/contato`
|
||||||
|
- **THEN** the configured internal recipient receives the dedicated partner inquiry e-mail
|
||||||
|
- **AND** no briefing confirmation or Lead is created
|
||||||
|
|
||||||
|
### Requirement: Partner inquiry is protected and transparent
|
||||||
|
The partner form SHALL enforce CSRF, server-side limits, honeypot, rate limiting and duplicate-click protection. It MUST show a purpose-specific privacy notice linking to the privacy policy.
|
||||||
|
|
||||||
|
#### Scenario: Honeypot blocks automated partner form submissions
|
||||||
|
- **WHEN** the partner honeypot is filled
|
||||||
|
- **THEN** the system reports generic success without sending an e-mail
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home presents the weddings and corporate editorial journey
|
||||||
|
The home SHALL expose anchored chapters for presentation, Amare, service directions, weddings, Corporate, portfolio/acervo, testimonials and final contact paths. It MUST retain the photographic hero, tonal fields, fine rules, EB Garamond and progressive/reduced-motion behavior. Corporate SHALL present only institutional copy and a briefing CTA until authorized copy and proof exist.
|
||||||
|
|
||||||
|
#### Scenario: Canonical anchor journey is reachable
|
||||||
|
- **WHEN** a visitor loads the home or follows a `/#chapter` link from another public page
|
||||||
|
- **THEN** the relevant chapter exists with a stable id and is reachable without JavaScript
|
||||||
|
|
||||||
|
#### Scenario: Corporate does not imply unauthorized proof
|
||||||
|
- **WHEN** a visitor loads the Corporate chapter before client proof is published
|
||||||
|
- **THEN** no Corporate service list, image, metric or case is rendered
|
||||||
|
|
||||||
|
### Requirement: Production portfolio remains truthful when no cases are published
|
||||||
|
The public portfolio chapter SHALL render a visible acervo-em-preparação note when no real published cases are available. Production seeders MUST NOT publish development fixture cases.
|
||||||
|
|
||||||
|
#### Scenario: Empty production acervo is explicit
|
||||||
|
- **GIVEN** production has no published portfolio cases
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the portfolio chapter displays the acervo preparation note and no case card
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
|
The `contact` route SHALL render the partner and supplier inquiry page using `site_settings` contact data. The detailed event proposal form SHALL be served by the `briefing` route at `/briefing`; public proposal CTAs MUST target that route. Neither public form MUST create a Lead in the launch scope.
|
||||||
|
|
||||||
|
#### Scenario: Contact page shows partner journey
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/contato`
|
||||||
|
- **THEN** partner-specific copy, fields and privacy notice MUST be displayed
|
||||||
|
- **AND** the detailed event briefing MUST NOT be rendered
|
||||||
|
|
||||||
|
#### Scenario: Briefing route remains reachable for prospects
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/briefing`
|
||||||
|
- **THEN** the detailed event proposal form MUST be displayed
|
||||||
|
- **AND** no Lead record MUST be created
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user