# Páginas Estáticas — Migração para Laravel Moderno (PHP 8.x / Laravel 11-12)

## 1. Schema de migração equivalente

```php
// database/migrations/2026_xx_xx_create_static_pages_table.php
Schema::create('static_pages', function (Blueprint $table) {
    $table->id();
    $table->string('key', 191)->nullable();
    $table->string('title', 191);
    $table->longText('text')->nullable();
    $table->string('access', 191)->nullable();
    $table->boolean('status_create')->default(false);   // página de sistema
    $table->boolean('galery')->default(false);
    $table->string('slug', 191)->unique();
    $table->unsignedInteger('clicks')->default(0);
    $table->unsignedTinyInteger('layout')->default(2);  // 1 = com sidebar, 2 = sem
    $table->boolean('is_external')->default(false);
    $table->timestamps();
    $table->softDeletes();
});
```

Mudanças propostas em relação ao legado:

| Mudança | Motivo |
|---|---|
| `slug` com índice **único** | O legado só garante unicidade na aplicação (e hoje há duplicatas: ids 6 e 8) |
| `clicks` com `default 0` e `NOT NULL` | O legado aceita NULL |
| `is_external` e `layout` com `NOT NULL` explícito | O schema já é assim; declarar evita divergência entre migração e banco |
| `key` e `access` mantidos | Colunas existentes; se o negócio não as usa, deprecar em vez de remover (risco de perda de dado) |
| `status_create` renomeado para `is_system` | Clareza semântica; exigiria migração de dados |

### Regras de migração de dados

| Origem | Destino | Regra |
|---|---|---|
| `static_pages` com `slug` duplicado | — | Resolver **antes** de criar o índice único (ids 6 e 8 no banco atual: manter uma, renomear a outra) |
| `clicks IS NULL` | `clicks = 0` | `COALESCE(clicks, 0)` |
| `layout IS NULL` | `layout = 2` | Na prática o NULL nunca ocorre (coluna NOT NULL) |
| `is_external = true` | — | Verificar se a página tem `links.link` preenchido; sem link, o redirecionamento quebra |
| `galery = true` sem `archives` | — | Marcar `galery = false` |

## 2. Modelos

```php
final class StaticPage extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'key', 'title', 'text', 'access', 'slug',
        'status_create', 'galery', 'layout', 'is_external',
    ];

    protected function casts(): array
    {
        return [
            'status_create' => 'boolean',
            'galery'        => 'boolean',
            'is_external'   => 'boolean',
            'layout'        => 'integer',
            'clicks'        => 'integer',
        ];
    }

    public function files(): HasMany      { return $this->hasMany(StaticPageFile::class); }
    public function gallery(): HasMany     { return $this->hasMany(StaticPageFile::class)->whereNull('highlight')->orderBy('order'); }
    public function inlinePdf(): HasOne    { return $this->hasOne(StaticPageFile::class)->where('view_pdf', true); }
    public function externalLink(): HasOne { return $this->hasOne(Link::class); }
    public function menuItems(): HasMany   { return $this->hasMany(MenuItem::class); }
}
```

### Enums

| Enum | Valores |
|---|---|
| `StaticPageLayout` | `WITH_SIDEBAR = 1`, `WITHOUT_SIDEBAR = 2` |
| `StaticPageKind` | `SYSTEM`, `CUSTOM` (substitui `status_create`) |
| `StaticPageFileRole` | `ATTACHMENT`, `GALLERY`, `INLINE_PDF` |

### Service

```php
final class StaticPageService
{
    public function create(array $data, ?UploadedFile $highlight = null): StaticPage
    public function update(StaticPage $page, array $data): StaticPage
    public function delete(StaticPage $page): void   // recusa páginas de sistema
    public function resolveBySlug(string $slug): ?StaticPage
}
```

A lógica de `status_create` (páginas de sistema não podem ser excluídas)
passa a viver no **Service**, não no controller.

## 3. Form Requests

```php
final class StoreStaticPageRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title'       => ['required', 'string', 'max:191'],
            'slug'        => ['required', 'string', 'max:191', Rule::unique('static_pages', 'slug')],
            'text'        => ['nullable', 'string'],
            'layout'      => ['required', Rule::enum(StaticPageLayout::class)],
            'is_external' => ['boolean'],
            'link'        => ['required_if:is_external,true', 'nullable', 'url', 'max:180'],
            'galery'      => ['boolean'],
            'files'       => ['nullable', 'array'],
            'files.*'     => ['file', 'mimes:rar,zip,doc,docx,pdf,odt,xls,xlsx,xlsb,xslm,ppt,pptx'],
        ];
    }
}
```

As regras condicionais (`ConditionalRequiredSlug` e
`ConditionalRequiredLink`) viram `required_if` nativas do Laravel — mais
simples e testáveis.

Slugs reservados (`diario-oficial`, `requerimento-de-diarias`,
`portal-da-transparencia`, `licitacoes`) passam para uma tabela
`reserved_slugs` ou para uma coluna `is_reserved` em `category_contents`,
consumida por uma `Rule` dedicada (`NotReservedSlug`).

## 4. Policies

```php
final class StaticPagePolicy
{
    public function viewAny(User $u): bool             { return $u->can('pagina-estatica.view'); }
    public function create(User $u): bool              { return $u->can('pagina-estatica.create'); }
    public function update(User $u, StaticPage $p): bool { return $u->can('pagina-estatica.edit'); }
    public function delete(User $u, StaticPage $p): bool
    {
        return $u->can('pagina-estatica.delete') && ! $p->status_create;
    }
    public function deleteAny(User $u): bool            { return $u->can('pagina-estatica.delete'); }
}
```

## 5. Endpoints

### Portal

| Método | URI | Nome | Observação |
|---|---|---|---|
| GET | `/paginas/{staticPage:slug}` | `static-pages.show` | Rota canônica |
| GET | `/{slug}` | `static-pages.legacy` | **Redirect 301** para a rota canônica |
| GET | `/contato`, `/ouvidoria`, ... | — | Mantidas as rotas específicas já existentes |

A rota legada `/{slug}` deve ser registrada **por último** e devolver
`redirect()->route('static-pages.show', $slug, 301)`, o que elimina o
problema do nome de rota duplicado e do 200 em página inexistente.

### CMS

| Método | URI | Nome |
|---|---|---|
| GET | `/cms/paginas-estaticas` | `cms.static-pages.index` |
| GET | `/cms/paginas-estaticas/create` | `cms.static-pages.create` |
| POST | `/cms/paginas-estaticas` | `cms.static-pages.store` |
| GET | `/cms/paginas-estaticas/{staticPage}/edit` | `cms.static-pages.edit` |
| PUT | `/cms/paginas-estaticas/{staticPage}` | `cms.static-pages.update` |
| DELETE | `/cms/paginas-estaticas/{staticPage}` | `cms.static-pages.destroy` |
| POST | `/cms/paginas-estaticas/destroy-bulk` | `cms.static-pages.destroy-bulk` |
| POST | `/cms/paginas-estaticas/{staticPage}/gallery` | `cms.static-pages.gallery.store` |
| DELETE | `/cms/paginas-estaticas/{staticPage}/gallery/{file}` | `cms.static-pages.gallery.destroy` |

> A rota `pagina-estatica.show` (sem método) deve ser removida.

## 6. Recursos de API

O módulo não tem API no legado. Proposta:

```php
final class StaticPageResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'      => $this->id,
            'title'   => $this->title,
            'slug'    => $this->slug,
            'content' => $this->text,
            'layout'  => $this->layout,
            'files'   => ArchiveResource::collection($this->files),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}
```

Exposta em `GET /api/v1/paginas-estaticas` (somente páginas **não**
externas e **não** de sistema, se o negócio permitir).

## 7. Views Blade modernas

| Legado | Novo |
|---|---|
| `prefecture/static-pages/index.blade.php` | `<x-admin.datatable resource="static-pages" />` |
| `prefecture/static-pages/form.blade.php` | `<x-admin.static-page-form />` |
| `portal/statics/show.blade.php` | `<x-portal.static-page :page="$page" :modelo="$modelo" />` |
| `portal/components/view_pdf/files-list.blade.php` | `<x-portal.file-list :files="$files" />` |
| `portal/components/galerias/index.blade.php` | `<x-portal.gallery :items="$gallery" />` |

## 8. Fila, cache e storage

| Aspecto | Legado | Proposto |
|---|---|---|
| Galeria | Upload síncrono, nome aleatório | Upload com nome determinístico (`{id}/{uuid}.{ext}`), em disco S3 |
| Compressão | não há | job `CompressGalleryImage` |
| Cliques | `increment('clicks')` (atômico, OK) | Manter + cache de 60s para não ir ao banco a cada visita |
| Slug único | validação em PHP | índice único no banco |
| 404 | 200 com mensagem | `abort(404)` + view de erro com layout do portal |
| Cache da página | nenhum | `Cache::remember("static:{$slug}", 300, ...)` invalidado no `saved` |
| Menu | consultas a cada render | cache do menu por layout |

## 9. Riscos e decisões de equivalência funcional

| Risco | Decisão |
|---|---|
| Migrar a URL da raiz para `/paginas/{slug}` | **Não** sem plano de redirecionamento: as páginas estão indexadas e o CMS gera `url('/') . '/' . $slug`. Manter `/{slug}` como rota canônica **e** adicionar `/paginas/{slug}` como alias |
| Perder o nome de rota `static.show` | Manter o alias `static.show` apontando para a rota canônica para não quebrar o breadcrumb |
| Devolver 404 em vez de 200 | Mudança observável; alinhada com SEO e monitoração — **avaliar com o negócio** |
| Slug duplicado (ids 6 e 8) | Resolver antes de criar o índice único; a página id 8 tem 17 cliques e hoje é inalcançável |
| Páginas de sistema | Preservar a regra "não excluir" (migrar `status_create` -> `is_system`) |
| Redirecionamento externo | Manter `is_external` + `links.link`; garantir `link` obrigatório quando `is_external` |
| Métrica de cliques em redirecionamentos | Manter o comportamento atual (contar) ou mover a contagem para depois do `switch` — **decisão do negócio** |
| Galeria sem metadados | Manter; ou adicionar título/descrição (mudança de escopo) |
| `key` e `access` sem uso | Manter as colunas; deprecar com aviso no log |
