# admin-content-fixes Specification

## Requirements

### Requirement: Page section editor round-trip

The Filament page editor MUST present stored `Page.sections` in the nested Builder shape
(`{type, data}`) and MUST persist them back in the canonical flat shape
(`{type, ...fields}`), losslessly, for every supported block type: `heading`, `rich_text`,
`process_steps`, `differentiators` and `faq`. The database and public views MUST keep the flat shape.

#### Scenario: Stored flat sections are shown nested in the editor

- GIVEN a page whose `sections` are stored flat as `[{"type":"heading","content":"Hola"}]`
- WHEN an administrator opens the page edit form
- THEN the `sections` form state is `[{"type":"heading","data":{"content":"Hola"}}]`

#### Scenario: Saving nested sections stores them flat

- GIVEN the edit form holds `sections` as `[{"type":"process_steps","data":{"items":[{"title":"A","description":"B"}]}}]`
- WHEN the administrator saves the page
- THEN the page's stored `sections` are `[{"type":"process_steps","items":[{"title":"A","description":"B"}]}]`

#### Scenario: Round-trip preserves every block type

- GIVEN a page with one block of each type `heading`, `rich_text`, `process_steps`, `differentiators`, `faq`
- WHEN the page is opened in the editor and saved without changes
- THEN the stored `sections` are byte-for-byte equivalent to the original flat array

### Requirement: Public renderer supports every block type

The public page renderer MUST render all five block types, including `process_steps` and `faq`,
which MUST NOT be silently dropped.

#### Scenario: Process steps and FAQ render

- GIVEN a published page with a `process_steps` block and an `faq` block
- WHEN the page is requested
- THEN the process step titles/descriptions and the FAQ questions/answers appear in the HTML

### Requirement: Institutional pages render editable sections

The routes `/nosotros`, `/empresas` and `/contacto` MUST render their editable content from the
page's `sections` while keeping page-specific structural parts (the contact form and map on
`/contacto`, the corporate quote form on `/empresas`).

#### Scenario: Edited institutional content is reflected

- GIVEN the `nosotros` page's heading block content is changed to a sentinel value
- WHEN a visitor requests `/nosotros`
- THEN the response contains the sentinel value and still contains exactly one `<h1>`

#### Scenario: Page-specific forms remain

- GIVEN the `contacto` and `empresas` pages
- WHEN they are requested
- THEN `/contacto` still contains the contact form and map and `/empresas` still contains the quote form

### Requirement: Generic published-page route

A catch-all `GET /{slug}` route, registered after every specific route, MUST resolve a published
`Page` by slug and render it, and MUST return 404 for an unknown or unpublished slug. Reserved
prefixes (`admin`, `productos`, `blog`, `proyectos`, `buscar`, `sitemap.xml`, `robots.txt`,
`storage`, `images`, `build`, `dev`) MUST NOT be captured by it.

#### Scenario: A published page resolves

- GIVEN a published page with slug `promociones`
- WHEN a visitor requests `/promociones`
- THEN the response is 200 and renders the page content

#### Scenario: Unknown and unpublished pages 404

- GIVEN no published page exists with slug `no-existe`
- WHEN a visitor requests `/no-existe`
- THEN the response is 404

### Requirement: Sitemap lists published pages

The sitemap MUST include every published `Page` and MUST exclude unpublished pages, replacing the
three hardcoded institutional slugs.

#### Scenario: Published pages are listed

- GIVEN a published page with slug `promociones` and an unpublished page with slug `borrador`
- WHEN the sitemap is built
- THEN it contains `/promociones` and does not contain `/borrador`

### Requirement: Page SEO comes from the model

The public page routes MUST emit the `Page` model's SEO accessors (`seo_title`, `seo_description`,
`seo_keywords`, `seo_og_image`, `seo_canonical`, `seo_noindex`) with sensible fallbacks, so values
edited in the admin take effect. Hardcoded controller metadata MUST NOT override the model.

#### Scenario: Admin SEO metadata is emitted

- GIVEN a published page with a `SeoMeta` record whose `meta_title` and `meta_description` are set
- WHEN the page is requested
- THEN the `<title>` and meta description contain those values

#### Scenario: Noindex is respected

- GIVEN a published page whose `SeoMeta.noindex` is true
- WHEN the page is requested
- THEN the robots directive is `noindex, nofollow`

### Requirement: Blog cover is not wiped

Saving a post from the admin MUST NOT null the `cover_image` column. The cover MUST be stored and
read through the media `cover` collection.

#### Scenario: Saving a post preserves the legacy cover column

- GIVEN a post with `cover_image` set to `media/posts/legacy.jpg`
- WHEN the post is saved through `PostService` without a `cover_image` value
- THEN `cover_image` is still `media/posts/legacy.jpg`

### Requirement: Admin navigation polish

The admin panel MUST render navigation groups expanded and non-collapsible by default, MUST assign
unique sort values inside the `Contenido` group, and the claims resource MUST use a `Heroicon` enum
icon like the other resources.

#### Scenario: Contenido group has unique sorts

- GIVEN the Blog, Páginas and Biblioteca de medios navigation entries
- WHEN their navigation sort values are compared
- THEN no two share the same value

#### Scenario: Claims uses a Heroicon

- GIVEN the claims resource
- WHEN its navigation icon is inspected
- THEN it is an instance of the `Heroicon` enum
