# Plan de mejora SEO — Home

- **Basado en:** [diagnostico.md](diagnostico.md) (2026-10-30)
- **Resultado:** se mide en [resultado.md](resultado.md) 10 días después del deploy
- **Alcance:** solo `/`. Lo que se construya para todo el sitio (el script de variantes
  y el componente de imagen) queda listo para reutilizar en las demás páginas, pero esas
  páginas no se migran en este plan.

## Objetivo

1. Que el título y el H1 digan qué hace el negocio y dónde, dentro de los límites que
   muestra Google.
2. Bajar el peso de las imágenes de la home de unos 2,5 MB a menos de 0,7 MB, sirviendo
   a cada dispositivo el formato y el recorte que corresponde.
3. Completar el schema de la organización con el catálogo de servicios.
4. No empeorar nada de lo que hoy está bien: LCP, CLS, contenido, links y FAQ.

## Decisiones pendientes (las define el owner antes de ejecutar)

| # | Decisión | Opciones | Recomendación |
|---|---|---|---|
| D1 | Título | **A.** `Pool Renovation & Pavers in Palm Beach & Broward \| PPD Inc.` (59) · **B.** `Pool Renovation & Paver Contractor in Boynton Beach, FL` (55) | **B**: la home es la página de destino de la ficha de Business Profile, y la marca ya sale 1–2 en las búsquedas de marca |
| D2 | H1 | Mantenerlo `sr-only` con texto nuevo, o hacerlo visible en el hero | Mantenerlo `sr-only` en esta tanda. Hacerlo visible es un cambio de diseño aparte |
| D3 | Coordenadas `geo` | Copiarlas de la ficha de Business Profile | Sumarlas en `config/company.php` si el owner las pasa; si no, queda el TODO que ya existe |
| D4 | Originales en mayor resolución | `why-1/2/3`, `florida-map` y fotos del hero de más de 1280 px | Si no existen, esas imágenes solo se recomprimen, sin ganar nitidez |
| D5 | Herramienta de variantes | `sharp` (devDependency de pnpm) o Pillow (el venv de claude-seo) | **`sharp`**: vive en el toolchain del repo y cualquiera lo puede reproducir. Pillow solo existe en esta máquina |

### Decisiones tomadas (2026-10-30)

| # | Decisión |
|---|---|
| D1 | **A:** `Pool Renovation & Pavers in Palm Beach & Broward \| PPD Inc.` (59 caracteres) |
| D2 | H1 `sr-only` con texto nuevo: `Pool Renovation & Paver Contractor in Boynton Beach, Palm Beach & Broward County`. Boynton Beach queda en el H1 aunque no esté en el título |
| D3 | El owner pasa las coordenadas de la ficha de Business Profile |
| D4 | No hay originales en mayor resolución: las imágenes actuales solo se recomprimen |
| D5 | `sharp` |

### Corrección de D1 y D2 (2026-10-30, después del deploy)

El title y el H1 de arriba ponían "Pool Renovation" adelante. Tenían tres problemas:

- **Canibalización:** competían con la página específica de pool renovation, que es la
  mejor respuesta para esa búsqueda.
- **Prioridad:** lo principal del negocio son los pavers, no pool renovation.
- **Ambigüedad:** "Pool Renovation & Pavers" se leía como renovación de pavers, y los
  pavers también se instalan desde cero.

Criterio nuevo: la home nombra las líneas del negocio, con pavers primero, y no pisa a
ninguna página de servicio. "Pools" va al final y sin "renovation", porque no se
construyen piscinas nuevas.

| # | Decisión |
|---|---|
| D1 | `Pavers, Patios & Pools in Palm Beach & Broward \| PPD Inc.` (57 caracteres) |
| D2 | H1 `sr-only`: `Pavers, Patios & Pools in Boynton Beach, Palm Beach & Broward County` |
| Description | `Premium paver driveways, patios and pool decks, pool renovations and concrete overlays for Palm Beach & Broward County homes. Licensed, insured, built to last.` (159 caracteres; arranca con pavers) |

## Tareas

El orden importa. T4.1 y T4.2 son **chores aparte** del feature (regla del repo: lo
compartido se crea primero y por separado, y la pantalla lo consume después).

### T1 — Title y meta description

- **Dónde:** `app/Services/PageSeoService.php` → `pages()['home']`.
- **Qué:**
  - Título según D1. El string hoy está repetido en `meta.title` y en `webPage.name`:
    se define una sola vez y se usa en los dos lugares.
  - Description en 160 caracteres o menos. Hoy tiene 161; basta con recortar una palabra.
- **Aceptación:**
  - `<title>` de 60 caracteres o menos.
  - Description de 160 o menos.
  - `og:title` y `twitter:title` heredan el valor nuevo.
  - `metadata_template.py` da `templated: false`.

### T2 — H1

- **Dónde:** `resources/views/home.blade.php` → `@section('page-heading')`.
- **Qué:** texto nuevo según D1 y D2. Propuesta: `Pool Renovation & Paver Contractor in
  Boynton Beach, Palm Beach & Broward County`, con el nombre legal desde
  `config('company.legal_name')` si se mantiene adelante, como hoy.
- **Aceptación:**
  - Un solo `<h1>` en la página.
  - Nombra el servicio principal y Boynton Beach.
  - Sin contar el nombre legal, el texto tiene menos de 80 caracteres. La propuesta tiene 74.

### T3 — Schema: catálogo de servicios (y `geo` si se resuelve D3)

- **Dónde:** `PageSeoService::organizationSchema()`.
- **Qué:**
  - Agregar `hasOfferCatalog` derivado de `ServiceItem::cases()`, con `label()` y
    `url()` de cada servicio. Nada de listas escritas a mano: un servicio nuevo en el
    enum entra solo al schema.
  - Agregar `geo` solo si D3 trae las coordenadas, y leerlas desde `config/company.php`.
  - Ojo: el nodo de la organización es global, así que el catálogo va a aparecer en
    todas las páginas. Es correcto, porque es la misma organización.
- **Aceptación:**
  - El hook `validate-schema` no da errores.
  - El Rich Results Test sobre la home sale sin errores nuevos.

### T4 — Imágenes

#### T4.1 (chore) — Script de variantes

- **Dónde:** `scripts/images/` (script nuevo) y `resources/js/app.js`.
- **Qué:**
  - Un script que, a partir de un archivo fuente y un **perfil**, genera las variantes
    en **AVIF, WebP y JPG** (JPG como respaldo), en varios anchos y, si el perfil lo
    pide, con un **recorte para mobile**.
  - Las salidas se commitean, así el deploy no depende de `sharp`.
  - Los perfiles se definen **en un solo lugar** (por ejemplo `config/images.php`), y
    los usan tanto el script como el componente de T4.2.
- **Perfiles iniciales:**

  | Perfil | Anchos | Recorte mobile | `sizes` |
  |---|---|---|---|
  | `hero` | 1280 (desktop) | 440×720 y 780×1280 si la fuente lo permite (relación 390:640) | `100vw` |
  | `card` | 700, 1400 | 732×640 (relación 366:320) | `(min-width: 768px) 50vw, 100vw` |
  | `tile` | 480, 960 | — | `(min-width: 768px) 17vw, 45vw` |
  | `small` | tamaño fuente | — | según componente |

- **También:**
  - Agregar `avif` al `import.meta.glob` de `resources/js/app.js`. Hoy dice
    `{jpg,jpeg,png,svg,webp,gif}`, y sin ese cambio `Vite::asset()` falla con los `.avif`.
  - **Riesgo:** el glob es `eager`, así que cada variante suma una entrada al bundle.
    Hay que medir el tamaño de `app-*.js` antes y después.
- **Aceptación:**
  - El script es idempotente: correrlo dos veces no cambia nada.
  - Cada variante AVIF pesa menos que su WebP, y cada WebP menos que su JPG.
  - `pnpm build` termina sin errores.

#### T4.2 (chore) — Componente compartido de imagen responsive

- **Dónde:** `resources/views/components/responsive-image.blade.php` (`<x-responsive-image>`).
  Hoy no existe ninguno: no hay ningún `srcset` en `resources/views`.
- **Qué:** genera un `<picture>` con:
  - un `<source media="(max-width: 767px)">` para el recorte mobile, si el perfil lo tiene;
  - un `<source type="image/avif">` y otro `<source type="image/webp">`, ambos con `srcset` y `sizes`;
  - un `<img>` en JPG con `width` y `height`, `alt`, `decoding="async"` y, según
    corresponda, `loading="lazy"` o `fetchpriority="high"`.

  Las clases del `<img>` pasan tal cual, porque el hero depende de `img.<theme>` en
  `:has(.swiper-slide-active img.<theme>)`.
- **Deuda (se anota y no se migra en este plan):** listar todas las `<img>` con
  `Vite::asset` de `resources/views` que deberían pasar al componente. Las heroes de
  servicio, por ejemplo, afectan a las 242 páginas.
- **Aceptación:** sin regresión visual frente a las capturas de `mediciones/20261030-antes/`.

**Deuda registrada (2026-10-30)**: `<img>` que todavía no pasan por `<x-responsive-image>`.

| Archivo | Dónde se ve | Prioridad |
|---|---|---|
| `components/heroes/service.blade.php` | Hero de las 242 páginas de servicio (es su LCP) | Alta |
| `components/home/centered-photo-feature-v2.blade.php`, `centered-photo-feature.blade.php` | Secciones de las páginas de servicio | Alta |
| `components/home/image-with-checklist.blade.php`, `why-choose-us.blade.php` | Secciones de las páginas de servicio | Alta |
| `components/heroes/gallery.blade.php`, `gallery-v2.blade.php`, `gallery/index.blade.php` | Galería | Media |
| `about.blade.php`, `contact.blade.php`, `components/video-display.blade.php` (póster) | About, contacto, estimate | Media |
| `components/home/gallery-feature.blade.php` | v1 sin uso (la home usa la v2) | Ninguna |

No se migran: `nav` y `footer` (logos SVG), `review-card` (avatar) y
`emails/contact-form-submission` (un mail necesita `<img>` simple).

#### T4.3 — Aplicar el componente en la home

| Bloque | Archivo | Perfil | Carga | Notas |
|---|---|---|---|---|
| Hero (5 slides) | `components/heroes/home.blade.php` | `hero` | slide 1 con `fetchpriority="high"`; el resto `lazy` (como hoy) | Revisar que el `<picture>` no rompa el `absolute inset-0` del slide |
| Tarjetas (4) | `home.blade.php` y `components/home/services` | `card` | las 2 primeras sin lazy, la 3 y la 4 con `lazy` | Generar desde la fuente de mejor calidad, no desde los `.webp` actuales, que están mal comprimidos |
| Galería: fotos grandes (4) | `components/home/gallery-feature-v2.blade.php` | `tile` | `lazy` (como hoy) | Es el mayor ahorro: de unos 946 KB a unos 60 KB |
| Galería: `why-1/2/3` | ídem | `small` | `lazy` | Si D4 no trae originales, solo se recomprimen |
| Mapa | `components/home/service-locations.blade.php` | `small` | `lazy` | Si existe un SVG, reemplazar el PNG |

- **Aceptación:**
  - Peso de imágenes (`medir-pagina.py`) **menor a 700 KB** en desktop y en mobile.
  - En Chromium, todas las imágenes raster se sirven en AVIF.

### T5 — Verificación con Playwright (antes de deploy, en el sitio local de lerd)

1. Correr `_scripts/medir-pagina.py` contra el sitio local y guardar el resultado en
   `mediciones/AAAAMMDD-local/`.
2. Revisar en `medicion.json`, para desktop, tablet y mobile:
   - `format` es `avif` en todas las imágenes raster.
   - Ninguna imagen se sirve más grande de lo necesario: el ancho natural no supera
     2 veces el ancho pintado multiplicado por el DPR.
   - En mobile, el hero y las tarjetas usan el recorte (relación de aspecto vertical).
   - El LCP sigue siendo `slide-1`, y el CLS no supera el de la línea base en más de 0,01.
3. Comparar las capturas una al lado de la otra con las de `mediciones/20261030-antes/`,
   para confirmar el encuadre de los recortes, que no haya imágenes estiradas y que el
   slider siga funcionando.
4. Correr `/seo-page` sobre la versión local: un solo H1, título de 60 caracteres o
   menos y JSON-LD válido.
5. Comparar el tamaño de `app-*.js` antes y después (el riesgo de T4.1).

### T6 — Deploy y seguimiento

1. Deploy a producción y registrar la **fecha de deploy** en `resultado.md`.
2. Repetir `medir-pagina.py` contra producción y guardarlo en `mediciones/AAAAMMDD-despues/`.
3. En Search Console, desde la interfaz: **URL Inspection → Request indexing** para `/`.
   La Indexing API no sirve para esto: es solo para JobPosting y BroadcastEvent.
4. A los 10 días, completar `resultado.md`.

## Fuera del código (owner)

- **Ficha de Business Profile:** cambiar el link a
  `https://www.patiospoolsanddriveways.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp`.
  **Importante para medir:** con este cambio, las impresiones que hoy se atribuyen a
  `http://…/` van a pasar a `https://www.…/`. En `resultado.md` hay que leer las dos
  URLs juntas.
- **`robots.txt`:** commitear y deployar el cambio local de `public/robots.txt` (sitemap
  en www), que existe sin commitear desde el 2026-10-27. Afecta a todo el sitio.

## Commits sugeridos

1. `chore(images): script de variantes AVIF/WebP/JPG con perfiles`
2. `chore(ui): componente <x-responsive-image>`
3. `feat(seo): title, description y H1 de la home`
4. `feat(seo): hasOfferCatalog en el schema de la organización`
5. `perf(home): imágenes responsive con recortes mobile`
