feat: ship public site with SEO and visuals

Publish CMS content on public routes with responsive media,
accessibility checks, and deterministic visual baselines.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 08:36:59 -03:00
parent 7a858b52af
commit 27f3cee855
94 changed files with 3925 additions and 60 deletions

View File

@@ -0,0 +1,90 @@
## ADDED 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 MUST use `meta_title`/`meta_description` when filled and fall back to title/summary 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: 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 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: 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 and every published case 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: 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: Robots references the sitemap
- **WHEN** `/robots.txt` is requested
- **THEN** the response MUST be `text/plain`
- **AND** MUST contain the absolute `/sitemap.xml` URL
### Requirement: Basic structured data is emitted where applicable
The system SHALL emit JSON-LD structured data: `Organization` on the home using `site_settings`, and `Article` or equivalent creative work on the case detail.
#### Scenario: Home exposes organization data
- **WHEN** the home is rendered
- **THEN** a JSON-LD block of type `Organization` MUST be present with brand name and contact data
#### Scenario: Structured data is valid JSON
- **WHEN** any public page emits JSON-LD
- **THEN** the script content MUST parse as valid JSON
### Requirement: Analytics script is injected only when explicitly enabled
The system SHALL render the analytics snippet from `site_settings` only when `analytics_enabled` is true and the script field is non-empty (WEB-06). Analytics MUST be disabled by default.
#### Scenario: Analytics disabled emits nothing
- **GIVEN** `analytics_enabled` is false
- **WHEN** any public page is rendered
- **THEN** the analytics snippet MUST NOT appear in the HTML
#### Scenario: Analytics enabled injects the configured snippet
- **GIVEN** `analytics_enabled` is true and a snippet is configured
- **WHEN** a public page is rendered
- **THEN** the snippet MUST be present exactly once