feat(js): implement intersection observer lazy loading for adsense
- Add per-slot lazy loading with Intersection Observer API - Implement fill detection via MutationObserver and data-ad-status - Add configurable rootMargin and fillTimeout from database - Generate dynamic CSS based on lazy_loading_enabled setting - Add legacy mode fallback for browsers without IO support - Include backup of previous implementation (adsense-loader.legacy.js) - Add OpenSpec documentation with test plan (72 tests verified) Schema changes: - Add lazy_loading_enabled (boolean, default: true) - Add lazy_rootmargin (select: 0-500px, default: 200) - Add lazy_fill_timeout (select: 3000-10000ms, default: 5000) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
274
openspec/changes/refactor-adsense-lazy-loading/design.md
Normal file
274
openspec/changes/refactor-adsense-lazy-loading/design.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Design: AdSense Lazy Loading con Intersection Observer
|
||||
|
||||
## Context
|
||||
|
||||
### Problema Actual
|
||||
|
||||
El `adsense-loader.js` actual implementa un modelo "todo o nada":
|
||||
|
||||
1. Usuario interactua (scroll/click) O timeout 5s
|
||||
2. Se carga `adsbygoogle.js` (biblioteca principal)
|
||||
3. Se ejecutan TODOS los `push({})` simultaneamente
|
||||
4. Google intenta llenar TODOS los slots de una vez
|
||||
|
||||
**Consecuencias:**
|
||||
- Fill rate bajo: Google tiene limite de ads por pagina/sesion
|
||||
- Slots vacios visibles: No hay inventario para todos
|
||||
- Impresiones desperdiciadas: Ads below-the-fold nunca vistos
|
||||
- Impacto en Core Web Vitals: Carga masiva de recursos
|
||||
|
||||
### Solucion Propuesta
|
||||
|
||||
Cambiar a modelo "por demanda con visibilidad":
|
||||
|
||||
1. La biblioteca `adsbygoogle.js` se carga UNA vez (primer ad visible)
|
||||
2. Cada slot individual se activa al entrar en viewport
|
||||
3. Slots permanecen ocultos hasta que tengan contenido
|
||||
4. No hay timeout global, cada ad tiene su propio trigger
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
|
||||
- Mejorar fill rate cargando ads secuencialmente
|
||||
- Eliminar espacios en blanco de slots vacios
|
||||
- Reducir tiempo de carga inicial (menos JS ejecutado)
|
||||
- Mejorar Core Web Vitals (menor TBT, mejor LCP)
|
||||
- Cumplir politicas de Google AdSense
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Reciclar o eliminar ads ya cargados (viola politicas)
|
||||
- Implementar "infinite scroll" de ads
|
||||
- Cache de contenido de ads
|
||||
- Prefetch de ads futuros
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Extension del Modulo Existente AdsensePlacement
|
||||
|
||||
**Razon:** Mantener Clean Architecture del proyecto. No crear modulo nuevo.
|
||||
|
||||
**Ubicacion de archivos:**
|
||||
- Schema: `Schemas/adsense-placement.json` (nuevos campos en grupo `forms`)
|
||||
- Renderer: `Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php`
|
||||
- FormBuilder: `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php`
|
||||
- FieldMapper: `Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php`
|
||||
- Asset Enqueuer: `Public/AdsensePlacement/Infrastructure/Services/AdsenseAssetEnqueuer.php`
|
||||
- JavaScript: `Assets/Js/adsense-loader.js`
|
||||
|
||||
**Alternativas descartadas:**
|
||||
- Crear modulo nuevo `AdsenseLazyLoading`: Viola principio de cohesion, duplica logica
|
||||
|
||||
### Decision 2: Usar Intersection Observer API
|
||||
|
||||
**Razon:** API nativa del navegador, alto rendimiento, soporte >95% global.
|
||||
|
||||
**Alternativas consideradas:**
|
||||
- Scroll listener + getBoundingClientRect(): Mayor consumo de CPU
|
||||
- requestAnimationFrame loop: Complejo, mismo resultado
|
||||
- Third-party library (lozad.js): Dependencia innecesaria
|
||||
|
||||
### Decision 3: Ocultar slots por defecto con CSS Dinamico
|
||||
|
||||
**Razon:** Evitar layout shift (CLS) cuando un slot no recibe ad.
|
||||
|
||||
**Implementacion via CSSGeneratorService** (NO CSS estatico):
|
||||
```php
|
||||
// En AdsensePlacementRenderer.php
|
||||
$this->cssGenerator->generate([
|
||||
'.roi-ad-slot' => [
|
||||
'display' => $lazyEnabled ? 'none' : 'block',
|
||||
],
|
||||
'.roi-ad-slot.roi-ad-filled' => [
|
||||
'display' => 'block',
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
**Alternativas descartadas:**
|
||||
- CSS estatico en archivo: Viola arquitectura del tema
|
||||
- `visibility: hidden`: Ocupa espacio, causa CLS
|
||||
- `height: 0; overflow: hidden`: Hack, problemas con responsive
|
||||
- Remover del DOM: Viola politicas de AdSense
|
||||
|
||||
### Decision 4: Criterios Concretos de Fill Detection
|
||||
|
||||
**Razon:** Evitar ambiguedad sobre cuando un ad "tiene contenido".
|
||||
|
||||
**Criterios para marcar como `roi-ad-filled`:**
|
||||
1. El elemento `<ins class="adsbygoogle">` contiene al menos un hijo
|
||||
2. **Y** ese hijo es un `<iframe>` O un `<div>` con contenido
|
||||
3. **Y** el `<ins>` tiene `data-ad-status="filled"` (atributo que Google agrega)
|
||||
|
||||
**Criterios para marcar como `roi-ad-empty`:**
|
||||
1. Timeout de `lazy_fill_timeout` ms ha pasado sin cumplir criterios de fill
|
||||
2. **O** el `<ins>` tiene `data-ad-status="unfilled"`
|
||||
|
||||
**Implementacion con MutationObserver:**
|
||||
```javascript
|
||||
function checkAdFill(insElement) {
|
||||
const status = insElement.getAttribute('data-ad-status');
|
||||
if (status === 'filled') return 'filled';
|
||||
if (status === 'unfilled') return 'empty';
|
||||
|
||||
// Fallback: verificar contenido si no hay atributo
|
||||
if (insElement.children.length > 0) {
|
||||
const hasIframe = insElement.querySelector('iframe');
|
||||
const hasContent = insElement.querySelector('div[id]');
|
||||
if (hasIframe || hasContent) return 'filled';
|
||||
}
|
||||
return 'pending';
|
||||
}
|
||||
```
|
||||
|
||||
### Decision 5: rootMargin Configurable via Schema
|
||||
|
||||
**Razon:** Cargar ads antes de que sean visibles para UX fluida.
|
||||
|
||||
**Valor por defecto:** `200px 0px` (200px arriba/abajo, 0 laterales)
|
||||
|
||||
**Configuracion via BD** (no window object):
|
||||
- Campo `lazy_rootmargin` en schema JSON
|
||||
- Leido por `AdsenseAssetEnqueuer` desde BD
|
||||
- Pasado a JS via `wp_localize_script()`
|
||||
|
||||
### Decision 6: Configuracion Unica via Schema JSON
|
||||
|
||||
**Razon:** Seguir flujo de 5 fases del proyecto, evitar flags conflictivos.
|
||||
|
||||
**Campos nuevos en grupo `behavior` de `adsense-placement.json`:**
|
||||
|
||||
| Campo | Tipo | Default | Options | Descripcion |
|
||||
|-------|------|---------|---------|-------------|
|
||||
| `lazy_loading_enabled` | boolean | true | - | Habilitar lazy loading |
|
||||
| `lazy_rootmargin` | select | "200" | 0, 100, 200, 300, 400, 500 | Pixeles de pre-carga |
|
||||
| `lazy_fill_timeout` | select | "5000" | 3000, 5000, 7000, 10000 | Timeout en ms |
|
||||
|
||||
**Nota:** Se usa `select` en lugar de `number` porque el schema solo soporta: boolean, text, textarea, url, select, color. Los valores se parsean a entero en PHP. Ver `schema-changes.md` para definicion completa con labels.
|
||||
|
||||
**NO usar `window.roiAdsenseConfig`** - La configuracion viene de BD via `wp_localize_script()`:
|
||||
```php
|
||||
// En AdsenseAssetEnqueuer.php
|
||||
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||
'debug' => WP_DEBUG,
|
||||
]);
|
||||
```
|
||||
|
||||
### Decision 7: Manejo de Errores de Red
|
||||
|
||||
**Razon:** La biblioteca `adsbygoogle.js` puede fallar por red o bloqueo.
|
||||
|
||||
**Estrategia:**
|
||||
1. `onerror` callback en script de biblioteca
|
||||
2. Reintentar 1 vez despues de 2 segundos
|
||||
3. Si falla segundo intento, marcar todos los slots como `roi-ad-error`
|
||||
4. Log en consola si debug habilitado
|
||||
|
||||
```javascript
|
||||
newScript.onerror = function() {
|
||||
if (retryCount < 1) {
|
||||
retryCount++;
|
||||
setTimeout(() => loadLibrary(), 2000);
|
||||
} else {
|
||||
markAllSlotsAsError();
|
||||
debugLog('AdSense library failed to load after retry');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: Ads below-the-fold nunca cargan
|
||||
|
||||
**Mitigacion:** `rootMargin: '200px'` pre-carga. Usuario que scrollea vera ads.
|
||||
|
||||
**Trade-off aceptado:** Si usuario no scrollea, no ve ads below-fold. Esto es BUENO para el anunciante (no paga por impresion no vista).
|
||||
|
||||
### Risk 2: Adblockers detectan Intersection Observer
|
||||
|
||||
**Mitigacion:** Nula. Si adblocker activo, ads no cargan de todas formas.
|
||||
|
||||
### Risk 3: Navegadores antiguos sin soporte
|
||||
|
||||
**Mitigacion:** Fallback a carga tradicional (todos al inicio).
|
||||
|
||||
```javascript
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
// Fallback: usar modo legacy existente
|
||||
loadAllAdsLegacy();
|
||||
}
|
||||
```
|
||||
|
||||
### Risk 4: Slots sin ad permanecen ocultos siempre
|
||||
|
||||
**Mitigacion:** Timeout por slot configurable. Clase `roi-ad-empty` permite styling si necesario.
|
||||
|
||||
### Risk 5: Race condition en carga de biblioteca
|
||||
|
||||
**Mitigacion:** Ya resuelto en implementacion actual con callback `onload`. Documentado para mantener.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Fase 1: Schema JSON
|
||||
|
||||
1. Agregar campos `lazy_loading_enabled`, `lazy_rootmargin`, `lazy_fill_timeout` al grupo `behavior` de `adsense-placement.json`
|
||||
2. Ejecutar `wp roi-theme sync-component adsense-placement`
|
||||
3. Verificar campos en BD
|
||||
|
||||
### Fase 2: Renderer (BD → HTML + CSS)
|
||||
|
||||
1. Actualizar `AdsensePlacementRenderer.php` para CSS dinamico
|
||||
2. Actualizar `AdsenseAssetEnqueuer.php` para pasar config a JS
|
||||
3. Actualizar `AdsensePlacementFieldMapper.php` con nuevos campos
|
||||
|
||||
### Fase 3: FormBuilder (UI Admin)
|
||||
|
||||
1. Actualizar `AdsensePlacementFormBuilder.php` con UI para nuevos campos
|
||||
2. Agregar nota sobre necesidad de vaciar cache
|
||||
|
||||
### Fase 4: JavaScript (Infrastructure)
|
||||
|
||||
1. Refactorizar `adsense-loader.js` con Intersection Observer
|
||||
2. Implementar MutationObserver para fill detection
|
||||
3. Implementar fallback para navegadores sin soporte
|
||||
4. Mantener compatibilidad con `lazy_loading_enabled: false`
|
||||
|
||||
### Fase 5: Validacion y Testing
|
||||
|
||||
1. Ejecutar validador de arquitectura
|
||||
2. Probar en desarrollo con DevTools (Network throttling)
|
||||
3. Verificar que ads cargan al scroll
|
||||
4. Verificar que slots vacios NO se muestran
|
||||
5. Medir Core Web Vitals con Lighthouse
|
||||
|
||||
### Post-Implementacion: Deploy y Monitoreo
|
||||
|
||||
1. Commit con mensaje descriptivo
|
||||
2. Deploy a produccion
|
||||
3. Vaciar cache (Redis, W3TC)
|
||||
4. Verificar fill rate en AdSense dashboard (24-48h)
|
||||
|
||||
### Rollback
|
||||
|
||||
Si hay problemas:
|
||||
1. En admin, cambiar `lazy_loading_enabled` a false
|
||||
2. El sistema vuelve a modo legacy automaticamente
|
||||
3. No requiere deploy de codigo
|
||||
|
||||
## Open Questions - RESUELTOS
|
||||
|
||||
1. **Cual es el rootMargin optimo?**
|
||||
- **Resuelto:** 200px por defecto, configurable via admin
|
||||
|
||||
2. **Timeout por slot para "dar por vacio"?**
|
||||
- **Resuelto:** 5000ms por defecto, configurable via admin
|
||||
|
||||
3. **Como detectar fill de forma confiable?**
|
||||
- **Resuelto:** Usar `data-ad-status` de Google + fallback a children check
|
||||
|
||||
4. **Donde va la configuracion?**
|
||||
- **Resuelto:** Schema JSON → BD → wp_localize_script (NO window globals)
|
||||
54
openspec/changes/refactor-adsense-lazy-loading/proposal.md
Normal file
54
openspec/changes/refactor-adsense-lazy-loading/proposal.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Change: Refactorizar AdSense Lazy Loading con Intersection Observer
|
||||
|
||||
## Why
|
||||
|
||||
La implementacion actual carga TODOS los ads simultaneamente despues de interaccion del usuario o timeout de 5 segundos. Esto causa:
|
||||
|
||||
1. **Slots vacios visibles**: Cuando hay mas ads que inventario disponible, los slots vacios quedan visibles en la pagina creando espacios en blanco.
|
||||
2. **Sobrecarga inicial**: Cargar 20+ ads simultaneamente impacta el rendimiento y el fill rate de Google.
|
||||
3. **Desperdicio de impresiones**: Ads below-the-fold se cargan aunque el usuario nunca llegue a verlos.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: El comportamiento de carga cambia de "cargar todo" a "cargar por visibilidad"
|
||||
- Nuevos campos de configuracion en schema `adsense-placement.json` (grupo `forms`)
|
||||
- Extension del modulo `AdsensePlacement` existente (NO modulo nuevo)
|
||||
- Implementar Intersection Observer para detectar cuando un slot entra al viewport
|
||||
- Cargar cada ad individualmente cuando el usuario se aproxima (rootMargin configurable)
|
||||
- NO mostrar el contenedor `.roi-ad-slot` hasta que el ad tenga contenido real
|
||||
- Estilos generados via CSSGeneratorService (NO CSS estatico)
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: Extension de especificacion existente `adsense-placement`
|
||||
- Affected code:
|
||||
- `Schemas/adsense-placement.json` - Nuevos campos en grupo `forms`
|
||||
- `Assets/Js/adsense-loader.js` - Refactorizacion con Intersection Observer
|
||||
- `Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php` - Ajustar markup y estilos
|
||||
- `Public/AdsensePlacement/Infrastructure/Services/AdsenseAssetEnqueuer.php` - Pasar config a JS
|
||||
- `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php` - Nuevos campos UI
|
||||
- `Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php` - Mapping
|
||||
|
||||
## Arquitectura
|
||||
|
||||
Esta mejora se integra al modulo **existente** `AdsensePlacement`:
|
||||
|
||||
```
|
||||
Public/AdsensePlacement/
|
||||
├── Domain/ # Sin cambios (no hay logica de negocio nueva)
|
||||
├── Application/ # Sin cambios
|
||||
└── Infrastructure/
|
||||
├── Ui/
|
||||
│ └── AdsensePlacementRenderer.php # Genera CSS dinamico via CSSGenerator
|
||||
└── Services/
|
||||
└── AdsenseAssetEnqueuer.php # Enqueue JS con config desde BD
|
||||
|
||||
Admin/AdsensePlacement/
|
||||
├── Infrastructure/
|
||||
│ ├── Ui/
|
||||
│ │ └── AdsensePlacementFormBuilder.php # Nuevos campos lazy loading
|
||||
│ └── FieldMapping/
|
||||
│ └── AdsensePlacementFieldMapper.php # Mapping nuevos campos
|
||||
```
|
||||
|
||||
**NO se crea modulo nuevo** - es extension del componente existente.
|
||||
284
openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
Normal file
284
openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Pruebas Sanitarias - AdSense Lazy Loading
|
||||
|
||||
> **Objetivo:** Verificar funcionamiento basico en navegador despues de deploy
|
||||
> **Tiempo estimado:** 15-20 minutos
|
||||
> **Entorno:** analisisdepreciosunitarios.com (PRODUCCION)
|
||||
> **Flujo:** Local (desarrollo) → Deploy → Produccion (pruebas)
|
||||
|
||||
---
|
||||
|
||||
## PRE-REQUISITOS
|
||||
|
||||
### 0. Deploy Completado
|
||||
|
||||
- [ ] Cambios commiteados en local
|
||||
- [ ] Deploy a produccion ejecutado
|
||||
- [ ] `wp roi-theme sync-component adsense-placement` ejecutado en produccion
|
||||
- [ ] Cache vaciado (Redis, W3TC, Cloudflare si aplica)
|
||||
|
||||
### 1. Verificar Entorno Produccion
|
||||
|
||||
- [ ] Sitio accesible en https://analisisdepreciosunitarios.com/
|
||||
- [ ] DevTools abierto (F12)
|
||||
- [ ] Consola visible (para ver logs de debug)
|
||||
- [ ] Network tab visible (para ver requests de AdSense)
|
||||
|
||||
### 2. Verificar Configuracion en BD (Produccion)
|
||||
|
||||
```bash
|
||||
# Via SSH al VPS
|
||||
ssh VPSContabo
|
||||
cd /var/www/preciosunitarios/public_html
|
||||
wp db query "SELECT setting_key, setting_value FROM wp_roi_theme_component_settings WHERE component_name = 'adsense-placement' AND setting_key LIKE '%lazy%';" --allow-root
|
||||
```
|
||||
|
||||
**Valores esperados:**
|
||||
- `lazy_loading_enabled` = `1` (o `true`)
|
||||
- `lazy_rootmargin` = `200`
|
||||
- `lazy_fill_timeout` = `5000`
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 1: Carga Inicial (Lazy Enabled)
|
||||
|
||||
**Tiempo:** 3 min
|
||||
|
||||
### Pasos:
|
||||
1. Abrir DevTools > Console
|
||||
2. Navegar a un articulo con ads: https://analisisdepreciosunitarios.com/analisis-de-precios-unitarios/
|
||||
3. Observar consola
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST1.1** Aparece `[AdSense Lazy] Inicializando AdSense Lazy Loader v2.0`
|
||||
- [ ] **ST1.2** Aparece `[AdSense Lazy] Config: lazyEnabled=true, rootMargin=200px 0px, fillTimeout=5000`
|
||||
- [ ] **ST1.3** Aparece `[AdSense Lazy] Intersection Observer inicializado`
|
||||
- [ ] **ST1.4** Los slots `.roi-ad-slot` tienen `display: none` inicialmente (inspeccionar CSS)
|
||||
- [ ] **ST1.5** Solo slots en viewport muestran `[AdSense Lazy] Slot entro al viewport`
|
||||
|
||||
### Screenshot Console:
|
||||
```
|
||||
Pegar screenshot de consola aqui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 2: Activacion por Scroll
|
||||
|
||||
**Tiempo:** 3 min
|
||||
|
||||
### Pasos:
|
||||
1. Continuar en el mismo articulo
|
||||
2. Hacer scroll lento hacia abajo
|
||||
3. Observar consola mientras aparecen nuevos slots
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST2.1** Al scrollear, nuevos mensajes `[AdSense Lazy] Slot entro al viewport`
|
||||
- [ ] **ST2.2** Mensaje `[AdSense Lazy] Activando slot...` por cada slot visible
|
||||
- [ ] **ST2.3** En Network tab: requests a `pagead2.googlesyndication.com` aparecen progresivamente
|
||||
- [ ] **ST2.4** Slots activados reciben clase `roi-ad-filled` o `roi-ad-empty`
|
||||
|
||||
### Nota Fill Rate:
|
||||
```
|
||||
Slots activados: ___
|
||||
Slots filled: ___
|
||||
Slots empty: ___
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 3: Deteccion de Fill
|
||||
|
||||
**Tiempo:** 3 min
|
||||
|
||||
### Pasos:
|
||||
1. Inspeccionar un slot que recibio ad (clase `roi-ad-filled`)
|
||||
2. Inspeccionar un slot vacio (clase `roi-ad-empty`)
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST3.1** Slot filled tiene `display: block` (visible)
|
||||
- [ ] **ST3.2** Slot empty tiene `display: none` (oculto)
|
||||
- [ ] **ST3.3** Slot filled contiene `<ins>` con `data-ad-status="filled"`
|
||||
- [ ] **ST3.4** Consola muestra `[AdSense Lazy] Slot marcado como filled` o `empty`
|
||||
|
||||
### Screenshot Slot Filled:
|
||||
```
|
||||
Pegar screenshot del inspector aqui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 4: Timeout de Fill
|
||||
|
||||
**Tiempo:** 5 min (esperar timeout)
|
||||
|
||||
### Pasos:
|
||||
1. Bloquear requests de AdSense temporalmente:
|
||||
- DevTools > Network > Click derecho en request de googlesyndication
|
||||
- "Block request URL" o usar extension de bloqueo
|
||||
2. Recargar pagina
|
||||
3. Esperar 5 segundos (fillTimeout)
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST4.1** Slots muestran `[AdSense Lazy] Timeout alcanzado para slot`
|
||||
- [ ] **ST4.2** Slots reciben clase `roi-ad-empty`
|
||||
- [ ] **ST4.3** Slots permanecen ocultos (display: none)
|
||||
- [ ] **ST4.4** No hay errores JS en consola
|
||||
|
||||
### Desbloquear AdSense:
|
||||
- [ ] Remover bloqueo de AdSense despues del test
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 5: Modo Legacy (Lazy Disabled)
|
||||
|
||||
**Tiempo:** 4 min
|
||||
|
||||
### Pasos:
|
||||
1. Cambiar configuracion en BD (via SSH):
|
||||
```bash
|
||||
ssh VPSContabo
|
||||
cd /var/www/preciosunitarios/public_html
|
||||
wp db query "UPDATE wp_roi_theme_component_settings SET setting_value = '0' WHERE component_name = 'adsense-placement' AND setting_key = 'lazy_loading_enabled';" --allow-root
|
||||
```
|
||||
2. Vaciar cache:
|
||||
```bash
|
||||
wp cache flush --allow-root
|
||||
# Si usa W3TC: wp w3-total-cache flush all --allow-root
|
||||
```
|
||||
3. Recargar pagina (Ctrl+Shift+R)
|
||||
4. Observar consola
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST5.1** Consola muestra `[AdSense Lazy] Config: lazyEnabled=false`
|
||||
- [ ] **ST5.2** Consola muestra `[AdSense Lazy] Iniciando modo legacy`
|
||||
- [ ] **ST5.3** Los slots tienen `display: block` desde inicio
|
||||
- [ ] **ST5.4** Al hacer scroll o click, todos los ads cargan simultaneamente
|
||||
|
||||
### Restaurar (IMPORTANTE):
|
||||
```bash
|
||||
ssh VPSContabo
|
||||
cd /var/www/preciosunitarios/public_html
|
||||
wp db query "UPDATE wp_roi_theme_component_settings SET setting_value = '1' WHERE component_name = 'adsense-placement' AND setting_key = 'lazy_loading_enabled';" --allow-root
|
||||
wp cache flush --allow-root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 6: Ads Dinamicos (AJAX)
|
||||
|
||||
**Tiempo:** 3 min
|
||||
|
||||
### Pasos:
|
||||
1. Buscar pagina con carga dinamica de contenido (si existe)
|
||||
2. O simular en consola:
|
||||
```javascript
|
||||
// Simular nuevo slot dinamico
|
||||
var slot = document.createElement('div');
|
||||
slot.className = 'roi-ad-slot';
|
||||
slot.innerHTML = '<ins class="adsbygoogle" data-ad-client="ca-pub-xxx" data-ad-slot="123"></ins><script data-adsense-push type="text/plain">(adsbygoogle = window.adsbygoogle || []).push({});</script>';
|
||||
document.body.appendChild(slot);
|
||||
|
||||
// Disparar evento
|
||||
window.dispatchEvent(new Event('roi-adsense-activate'));
|
||||
```
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST6.1** Consola muestra `[AdSense Lazy] Evento roi-adsense-activate recibido`
|
||||
- [ ] **ST6.2** Nuevo slot es observado por Intersection Observer
|
||||
- [ ] **ST6.3** No hay errores JS
|
||||
|
||||
---
|
||||
|
||||
## SANITY TEST 7: Performance (Core Web Vitals)
|
||||
|
||||
**Tiempo:** 3 min
|
||||
|
||||
### Pasos:
|
||||
1. Abrir Lighthouse en DevTools
|
||||
2. Seleccionar "Performance" solamente
|
||||
3. Ejecutar audit en modo "Mobile"
|
||||
|
||||
### Verificar:
|
||||
|
||||
- [ ] **ST7.1** LCP (Largest Contentful Paint) < 2.5s
|
||||
- [ ] **ST7.2** FID (First Input Delay) < 100ms
|
||||
- [ ] **ST7.3** CLS (Cumulative Layout Shift) < 0.1
|
||||
- [ ] **ST7.4** No hay "Avoid enormous network payloads" warning por ads
|
||||
|
||||
### Scores:
|
||||
```
|
||||
Performance: ___
|
||||
LCP: ___
|
||||
FID: ___
|
||||
CLS: ___
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RESUMEN DE EJECUCION
|
||||
|
||||
| Test | Resultado | Notas |
|
||||
|------|-----------|-------|
|
||||
| ST1: Carga Inicial | [ ] PASS / [ ] FAIL | |
|
||||
| ST2: Scroll Activation | [ ] PASS / [ ] FAIL | |
|
||||
| ST3: Fill Detection | [ ] PASS / [ ] FAIL | |
|
||||
| ST4: Timeout | [ ] PASS / [ ] FAIL | |
|
||||
| ST5: Modo Legacy | [ ] PASS / [ ] FAIL | |
|
||||
| ST6: Ads Dinamicos | [ ] PASS / [ ] FAIL | |
|
||||
| ST7: Performance | [ ] PASS / [ ] FAIL | |
|
||||
|
||||
**Tests Passed:** ___/7
|
||||
**Tests Failed:** ___/7
|
||||
|
||||
---
|
||||
|
||||
## DECISION
|
||||
|
||||
- [ ] **APROBADO PARA DEPLOY** - Todos los tests pasan
|
||||
- [ ] **BLOQUEADO** - Tests criticos fallan (ST1-ST4)
|
||||
- [ ] **APROBADO CON OBSERVACIONES** - Tests no criticos fallan (ST5-ST7)
|
||||
|
||||
**Fecha:** ____________
|
||||
**Ejecutor:** ____________
|
||||
**Notas adicionales:**
|
||||
|
||||
```
|
||||
Escribir observaciones aqui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## COMANDOS UTILES
|
||||
|
||||
### Ver logs de consola filtrados:
|
||||
```javascript
|
||||
// En consola del navegador
|
||||
console.filter = '[AdSense';
|
||||
```
|
||||
|
||||
### Verificar config actual:
|
||||
```javascript
|
||||
console.log(window.roiAdsenseConfig);
|
||||
```
|
||||
|
||||
### Forzar recarga sin cache:
|
||||
```
|
||||
Ctrl + Shift + R (o Cmd + Shift + R en Mac)
|
||||
```
|
||||
|
||||
### Ver slots y su estado:
|
||||
```javascript
|
||||
document.querySelectorAll('.roi-ad-slot').forEach((slot, i) => {
|
||||
console.log(`Slot ${i}:`, {
|
||||
filled: slot.classList.contains('roi-ad-filled'),
|
||||
empty: slot.classList.contains('roi-ad-empty'),
|
||||
display: getComputedStyle(slot).display
|
||||
});
|
||||
});
|
||||
```
|
||||
111
openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
Normal file
111
openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Cambios al Schema: adsense-placement.json
|
||||
|
||||
## Resumen
|
||||
|
||||
Agregar 3 campos nuevos al grupo `behavior` para configurar el lazy loading de anuncios.
|
||||
|
||||
**Nota:** Los campos van en grupo `behavior` (priority 70) porque configuran el comportamiento del componente, no formularios de exclusion.
|
||||
|
||||
## Campos a Agregar
|
||||
|
||||
Ubicacion: `groups.behavior.fields`
|
||||
|
||||
### Campo 1: lazy_loading_enabled
|
||||
|
||||
```json
|
||||
"lazy_loading_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Lazy Loading de Anuncios",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Cargar anuncios individualmente al entrar al viewport (mejora fill rate)"
|
||||
}
|
||||
```
|
||||
|
||||
### Campo 2: lazy_rootmargin
|
||||
|
||||
```json
|
||||
"lazy_rootmargin": {
|
||||
"type": "select",
|
||||
"label": "Pre-carga (px antes del viewport)",
|
||||
"default": "200",
|
||||
"editable": true,
|
||||
"options": {
|
||||
"0": "0px (sin pre-carga)",
|
||||
"100": "100px",
|
||||
"200": "200px (recomendado)",
|
||||
"300": "300px",
|
||||
"400": "400px",
|
||||
"500": "500px"
|
||||
},
|
||||
"description": "Pixeles de anticipacion para iniciar carga de anuncio"
|
||||
}
|
||||
```
|
||||
|
||||
**Nota:** Tipo `select` en lugar de `number` porque el schema solo soporta: boolean, text, textarea, url, select, color.
|
||||
|
||||
### Campo 3: lazy_fill_timeout
|
||||
|
||||
```json
|
||||
"lazy_fill_timeout": {
|
||||
"type": "select",
|
||||
"label": "Timeout de llenado (ms)",
|
||||
"default": "5000",
|
||||
"editable": true,
|
||||
"options": {
|
||||
"3000": "3 segundos",
|
||||
"5000": "5 segundos (recomendado)",
|
||||
"7000": "7 segundos",
|
||||
"10000": "10 segundos"
|
||||
},
|
||||
"description": "Tiempo maximo para esperar contenido de Google antes de ocultar slot"
|
||||
}
|
||||
```
|
||||
|
||||
## Comando de Sincronizacion
|
||||
|
||||
Despues de actualizar el JSON:
|
||||
|
||||
```bash
|
||||
wp roi-theme sync-component adsense-placement
|
||||
```
|
||||
|
||||
## Version del Schema
|
||||
|
||||
Incrementar version de `1.4.0` a `1.5.0` para reflejar nueva funcionalidad.
|
||||
|
||||
## Relacion con delay_enabled
|
||||
|
||||
El campo `delay_enabled` (en grupo `forms`) controla si la **biblioteca** `adsbygoogle.js` se carga con retraso.
|
||||
|
||||
El campo `lazy_loading_enabled` (en grupo `behavior`) controla si los **slots individuales** se activan por visibilidad.
|
||||
|
||||
**Ambos pueden estar activos simultaneamente** - son complementarios:
|
||||
- `delay_enabled: true` = biblioteca no se carga hasta interaccion/timeout
|
||||
- `lazy_loading_enabled: true` = slots se activan individualmente por viewport
|
||||
|
||||
Si `lazy_loading_enabled: false`, el sistema usa el comportamiento actual (cargar todos los ads de una vez despues de que la biblioteca cargue).
|
||||
|
||||
## Interaccion con Cache
|
||||
|
||||
**Importante:** El CSS dinamico generado por `CSSGeneratorService` incluye `display: none` para `.roi-ad-slot` cuando lazy loading esta habilitado.
|
||||
|
||||
Si se cambia `lazy_loading_enabled` de true a false:
|
||||
1. El CSS dinamico cambiara en el siguiente render
|
||||
2. **Se DEBE vaciar cache** (Redis, W3TC, OPcache) para que el cambio surta efecto
|
||||
3. Usuarios con HTML cacheado veran slots ocultos hasta que su cache expire
|
||||
|
||||
**Recomendacion:** Agregar nota en FormBuilder indicando que cambios requieren vaciar cache.
|
||||
|
||||
## Parseo de Valores en PHP
|
||||
|
||||
Como los campos son tipo `select` con valores string, el `AdsenseAssetEnqueuer` debe parsear:
|
||||
|
||||
```php
|
||||
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||
'debug' => WP_DEBUG,
|
||||
]);
|
||||
```
|
||||
@@ -0,0 +1,360 @@
|
||||
# Especificacion: AdSense Lazy Loading
|
||||
|
||||
## Purpose
|
||||
|
||||
Define el comportamiento del sistema de carga diferida de anuncios AdSense usando Intersection Observer para cargar ads individualmente cuando entran al viewport, ocultando slots que no reciben contenido.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Carga Individual por Visibilidad
|
||||
|
||||
The system MUST load each AdSense ad slot individually when it enters the viewport, NOT all at once.
|
||||
|
||||
#### Scenario: Slot entra al viewport por primera vez
|
||||
|
||||
- **WHEN** un elemento `.roi-ad-slot[data-ad-lazy="true"]` entra al viewport (considerando rootMargin)
|
||||
- **THEN** el sistema DEBE ejecutar `adsbygoogle.push({})` SOLO para ese slot
|
||||
- **AND** el sistema DEBE marcar el slot como "activado" para no procesarlo de nuevo
|
||||
- **AND** el sistema DEBE observar el `<ins>` interno para detectar contenido
|
||||
|
||||
#### Scenario: Multiples slots en viewport inicial
|
||||
|
||||
- **GIVEN** la pagina tiene 3 slots visibles en el viewport inicial
|
||||
- **WHEN** la pagina termina de cargar
|
||||
- **THEN** el sistema DEBE activar los 3 slots en orden DOM (sin delay entre ellos)
|
||||
- **AND** la activacion es sincrona: push() → siguiente push() inmediatamente
|
||||
- **AND** el sistema NO DEBE activar slots que estan fuera del viewport
|
||||
|
||||
**Clarificacion:** "Secuencial" significa en orden DOM, uno tras otro sin delay artificial. NO hay setTimeout entre activaciones. El Intersection Observer dispara callbacks para todos los elementos visibles en el mismo frame.
|
||||
|
||||
#### Scenario: Usuario hace scroll rapido
|
||||
|
||||
- **GIVEN** el usuario hace scroll rapido pasando varios slots
|
||||
- **WHEN** los slots entran y salen del viewport rapidamente
|
||||
- **THEN** el sistema DEBE activar cada slot que entre al viewport
|
||||
- **AND** el sistema NO DEBE cancelar la activacion si el slot sale del viewport
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Biblioteca Cargada Una Sola Vez
|
||||
|
||||
The system MUST load the `adsbygoogle.js` library only once, when the first slot is activated.
|
||||
|
||||
#### Scenario: Primer slot activado
|
||||
|
||||
- **GIVEN** la biblioteca `adsbygoogle.js` NO ha sido cargada
|
||||
- **WHEN** el primer slot entra al viewport
|
||||
- **THEN** el sistema DEBE cargar la biblioteca
|
||||
- **AND** el sistema DEBE esperar a que la biblioteca cargue (onload callback)
|
||||
- **AND** ENTONCES ejecutar el push para ese slot
|
||||
|
||||
#### Scenario: Slots subsecuentes
|
||||
|
||||
- **GIVEN** la biblioteca `adsbygoogle.js` YA fue cargada
|
||||
- **WHEN** otro slot entra al viewport
|
||||
- **THEN** el sistema DEBE ejecutar el push inmediatamente
|
||||
- **AND** el sistema NO DEBE intentar cargar la biblioteca de nuevo
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Slots Ocultos por Defecto
|
||||
|
||||
The system MUST hide ad slots by default and show them only when they have content.
|
||||
|
||||
#### Scenario: Slot en estado inicial
|
||||
|
||||
- **WHEN** la pagina renderiza un `.roi-ad-slot[data-ad-lazy="true"]`
|
||||
- **THEN** el slot DEBE tener `display: none` via CSS dinamico
|
||||
- **AND** el slot NO DEBE ocupar espacio en el layout
|
||||
|
||||
#### Scenario: Slot recibe contenido de Google
|
||||
|
||||
- **GIVEN** un slot fue activado con push()
|
||||
- **WHEN** Google inyecta contenido dentro del `<ins class="adsbygoogle">`
|
||||
- **THEN** el sistema DEBE agregar clase `roi-ad-filled` al slot
|
||||
- **AND** el slot DEBE hacerse visible (`display: block`)
|
||||
|
||||
#### Scenario: Slot NO recibe contenido (timeout)
|
||||
|
||||
- **GIVEN** un slot fue activado con push()
|
||||
- **WHEN** pasa el tiempo configurado en `lazy_fill_timeout` sin que Google inyecte contenido
|
||||
- **THEN** el sistema DEBE agregar clase `roi-ad-empty` al slot
|
||||
- **AND** el slot DEBE permanecer oculto
|
||||
- **AND** el sistema DEBE dejar de observar ese slot
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pre-carga con rootMargin
|
||||
|
||||
The system MUST pre-load ads before they enter the visible viewport to ensure smooth UX.
|
||||
|
||||
#### Scenario: Configuracion de rootMargin
|
||||
|
||||
- **WHEN** se inicializa el Intersection Observer
|
||||
- **THEN** DEBE usar el valor de `lazy_rootmargin` desde configuracion
|
||||
- **AND** el formato DEBE ser `'{value}px 0px'`
|
||||
|
||||
#### Scenario: Slot dentro del rootMargin
|
||||
|
||||
- **GIVEN** un slot esta 150px debajo del viewport visible
|
||||
- **AND** `lazy_rootmargin` es 200
|
||||
- **WHEN** el Intersection Observer evalua visibilidad
|
||||
- **THEN** el slot DEBE considerarse "visible" y activarse
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Deteccion de Contenido con Criterios Concretos
|
||||
|
||||
The system MUST use specific criteria to determine when an ad slot has been filled.
|
||||
|
||||
#### Scenario: Google agrega atributo data-ad-status="filled"
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **WHEN** Google agrega `data-ad-status="filled"` al `<ins>`
|
||||
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-filled`
|
||||
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||
|
||||
#### Scenario: Google agrega atributo data-ad-status="unfilled"
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **WHEN** Google agrega `data-ad-status="unfilled"` al `<ins>`
|
||||
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-empty`
|
||||
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||
|
||||
#### Scenario: Fallback - Google inyecta iframe sin atributo
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||
- **WHEN** Google agrega un `<iframe>` dentro del `<ins>`
|
||||
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||
|
||||
#### Scenario: Fallback - Google agrega div con id
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||
- **WHEN** Google agrega un `<div id="...">` dentro del `<ins>`
|
||||
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||
|
||||
#### Scenario: Limpieza de observadores
|
||||
|
||||
- **GIVEN** un slot fue marcado como `roi-ad-filled` o `roi-ad-empty`
|
||||
- **WHEN** el estado final es determinado
|
||||
- **THEN** el sistema DEBE desconectar el MutationObserver de ese slot
|
||||
- **AND** el sistema DEBE desconectar el IntersectionObserver de ese slot
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Manejo de Errores de Red
|
||||
|
||||
The system MUST handle network errors when loading the AdSense library.
|
||||
|
||||
#### Scenario: Error de carga de biblioteca - primer intento
|
||||
|
||||
- **GIVEN** el sistema intenta cargar `adsbygoogle.js`
|
||||
- **WHEN** la carga falla (onerror)
|
||||
- **THEN** el sistema DEBE esperar 2 segundos
|
||||
- **AND** el sistema DEBE reintentar la carga UNA vez
|
||||
|
||||
#### Scenario: Error de carga de biblioteca - segundo intento fallido
|
||||
|
||||
- **GIVEN** el primer intento de carga fallo
|
||||
- **AND** el segundo intento tambien falla
|
||||
- **WHEN** el onerror se dispara por segunda vez
|
||||
- **THEN** el sistema DEBE marcar TODOS los slots como `roi-ad-error`
|
||||
- **AND** el sistema DEBE registrar error en consola si debug habilitado
|
||||
- **AND** el sistema NO DEBE intentar mas recargas
|
||||
|
||||
#### Scenario: Slots permanecen ocultos tras error
|
||||
|
||||
- **GIVEN** la biblioteca fallo en cargar
|
||||
- **WHEN** los slots tienen clase `roi-ad-error`
|
||||
- **THEN** los slots DEBEN permanecer ocultos
|
||||
- **AND** NO DEBEN mostrar espacios vacios en la pagina
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fallback para Navegadores Sin Soporte
|
||||
|
||||
The system MUST provide fallback for browsers without Intersection Observer support.
|
||||
|
||||
#### Scenario: Navegador sin Intersection Observer
|
||||
|
||||
- **GIVEN** `window.IntersectionObserver` es undefined
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar el modo legacy (cargar todos despues de interaccion/timeout)
|
||||
- **AND** el sistema DEBE registrar un mensaje de debug indicando fallback
|
||||
|
||||
#### Scenario: Navegador con soporte parcial
|
||||
|
||||
- **GIVEN** el navegador soporta Intersection Observer pero no MutationObserver
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar Intersection Observer para activacion
|
||||
- **AND** el sistema DEBE usar timeout fijo para determinar fill (sin deteccion dinamica)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Compatibilidad con Ads Dinamicos
|
||||
|
||||
The system MUST support ads injected dynamically after page load.
|
||||
|
||||
#### Scenario: Contenido cargado via AJAX
|
||||
|
||||
- **GIVEN** la pagina carga contenido adicional via AJAX con nuevos slots
|
||||
- **WHEN** el evento `roi-adsense-activate` es disparado
|
||||
- **THEN** el sistema DEBE buscar nuevos slots `.roi-ad-slot[data-ad-lazy="true"]` no observados
|
||||
- **AND** el sistema DEBE agregarlos al Intersection Observer
|
||||
|
||||
#### Scenario: Infinite scroll
|
||||
|
||||
- **GIVEN** la pagina implementa infinite scroll
|
||||
- **WHEN** nuevos slots son agregados al DOM
|
||||
- **THEN** el sistema DEBE detectarlos automaticamente (MutationObserver en body)
|
||||
- **OR** esperar evento `roi-adsense-activate` para procesarlos
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Configuracion desde Base de Datos
|
||||
|
||||
The system MUST read configuration from database via wp_localize_script, NOT from hardcoded values.
|
||||
|
||||
#### Scenario: Configuracion disponible en JS
|
||||
|
||||
- **WHEN** el script `adsense-loader.js` se ejecuta
|
||||
- **THEN** DEBE leer configuracion de `window.roiAdsenseConfig`
|
||||
- **AND** los valores DEBEN incluir:
|
||||
- `lazyEnabled` (boolean) - desde campo `lazy_loading_enabled`
|
||||
- `rootMargin` (string) - desde campo `lazy_rootmargin` + 'px 0px'
|
||||
- `fillTimeout` (number) - desde campo `lazy_fill_timeout`
|
||||
- `debug` (boolean) - desde WP_DEBUG
|
||||
|
||||
#### Scenario: Modo lazy deshabilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.lazyEnabled` es false
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar el modo legacy (cargar todos al inicio)
|
||||
- **AND** los slots DEBEN ser visibles por defecto (sin display:none)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No Manipular Ads Cargados
|
||||
|
||||
The system MUST NOT remove, recycle, or manipulate ads after they are loaded.
|
||||
|
||||
#### Scenario: Usuario scrollea pasando un ad
|
||||
|
||||
- **GIVEN** un ad fue cargado y mostrado
|
||||
- **WHEN** el usuario scrollea y el ad sale del viewport
|
||||
- **THEN** el sistema NO DEBE remover el ad del DOM
|
||||
- **AND** el sistema NO DEBE ocultar el ad
|
||||
- **AND** el sistema NO DEBE intentar "reciclar" el slot
|
||||
|
||||
#### Scenario: Ad permanece en pagina
|
||||
|
||||
- **GIVEN** un ad fue cargado exitosamente
|
||||
- **WHEN** la sesion del usuario continua
|
||||
- **THEN** el ad DEBE permanecer en su posicion original
|
||||
- **AND** el ad DEBE mantener su contenido intacto
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Logging de Debug Condicional
|
||||
|
||||
The system MUST provide debug logging only when enabled via WP_DEBUG.
|
||||
|
||||
#### Scenario: Debug habilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.debug` es true
|
||||
- **WHEN** ocurre cualquier evento significativo
|
||||
- **THEN** el sistema DEBE registrar en console.log con prefijo `[AdSense Lazy]`
|
||||
- **AND** los eventos incluyen: inicializacion, activacion de slot, deteccion de fill, timeout, error
|
||||
|
||||
#### Scenario: Debug deshabilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.debug` es false
|
||||
- **WHEN** el script ejecuta
|
||||
- **THEN** el sistema NO DEBE generar output en consola
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CSS Generado Dinamicamente
|
||||
|
||||
The system MUST generate CSS via CSSGeneratorService, NOT static CSS files.
|
||||
|
||||
#### Scenario: Lazy loading habilitado
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` es true en BD
|
||||
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||
- **THEN** DEBE usar `CSSGeneratorService` para generar:
|
||||
- `.roi-ad-slot { display: none }`
|
||||
- `.roi-ad-slot.roi-ad-filled { display: block }`
|
||||
- `.roi-ad-slot.roi-ad-empty { display: none }`
|
||||
|
||||
#### Scenario: Lazy loading deshabilitado
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` es false en BD
|
||||
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||
- **THEN** NO DEBE agregar `display: none` a `.roi-ad-slot`
|
||||
- **AND** los slots DEBEN ser visibles por defecto
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Integracion con Schema JSON
|
||||
|
||||
The system MUST store lazy loading configuration in the existing adsense-placement.json schema.
|
||||
|
||||
#### Scenario: Campos en grupo behavior
|
||||
|
||||
- **WHEN** el schema `adsense-placement.json` es leido
|
||||
- **THEN** el grupo `behavior` DEBE contener:
|
||||
- `lazy_loading_enabled` (boolean, default: true)
|
||||
- `lazy_rootmargin` (select, default: "200")
|
||||
- `lazy_fill_timeout` (select, default: "5000")
|
||||
|
||||
#### Scenario: Sincronizacion a BD
|
||||
|
||||
- **WHEN** se ejecuta `wp roi-theme sync-component adsense-placement`
|
||||
- **THEN** los campos de lazy loading DEBEN crearse en BD
|
||||
- **AND** los valores default DEBEN aplicarse si no existen
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Accesibilidad de Slots Ocultos
|
||||
|
||||
The system MUST ensure hidden ad slots do not interfere with assistive technologies.
|
||||
|
||||
#### Scenario: Slot oculto no interfiere con lectores de pantalla
|
||||
|
||||
- **GIVEN** un slot tiene `display: none` (estado inicial o roi-ad-empty)
|
||||
- **WHEN** un lector de pantalla procesa la pagina
|
||||
- **THEN** el slot NO DEBE ser anunciado ni navegable
|
||||
- **AND** el contenido oculto NO DEBE aparecer en el arbol de accesibilidad
|
||||
|
||||
#### Scenario: Slot visible es accesible
|
||||
|
||||
- **GIVEN** un slot fue marcado como `roi-ad-filled`
|
||||
- **WHEN** el slot se hace visible (`display: block`)
|
||||
- **THEN** el contenido del ad DEBE ser accesible para lectores de pantalla
|
||||
- **AND** el iframe de Google conserva su propia accesibilidad
|
||||
|
||||
**Nota tecnica:** `display: none` automaticamente remueve elementos del arbol de accesibilidad. No se requiere `aria-hidden` adicional.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Interaccion con Cache
|
||||
|
||||
The system MUST document cache implications when lazy loading settings change.
|
||||
|
||||
#### Scenario: Cambio de configuracion requiere cache flush
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` cambia de true a false (o viceversa)
|
||||
- **WHEN** el administrador guarda la configuracion
|
||||
- **THEN** el FormBuilder DEBE mostrar aviso de que se requiere vaciar cache
|
||||
- **AND** el CSS dinamico cambiara en el proximo render sin cache
|
||||
|
||||
#### Scenario: Usuario con cache obsoleto
|
||||
|
||||
- **GIVEN** un usuario tiene HTML cacheado con `display: none` en slots
|
||||
- **AND** el admin deshabilito lazy loading
|
||||
- **WHEN** el usuario visita la pagina
|
||||
- **THEN** los slots permaneceran ocultos hasta que el cache expire
|
||||
- **AND** esto es comportamiento esperado (no es un bug)
|
||||
150
openspec/changes/refactor-adsense-lazy-loading/tasks.md
Normal file
150
openspec/changes/refactor-adsense-lazy-loading/tasks.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Tasks: Refactorizar AdSense Lazy Loading
|
||||
|
||||
> **Nota:** Las tareas siguen el flujo de 5 fases del proyecto. Pasos adicionales (FieldMapper, Asset Enqueuer, JS) son subtareas de infraestructura.
|
||||
|
||||
---
|
||||
|
||||
## FASE 1: Schema JSON
|
||||
|
||||
### 1.1 Actualizar adsense-placement.json
|
||||
|
||||
- [x] Incrementar version de `1.4.0` a `1.5.0`
|
||||
- [x] Agregar campo `lazy_loading_enabled` al grupo `behavior`:
|
||||
```json
|
||||
"lazy_loading_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Lazy Loading de Anuncios",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Cargar anuncios individualmente al entrar al viewport (mejora fill rate)"
|
||||
}
|
||||
```
|
||||
- [x] Agregar campo `lazy_rootmargin` al grupo `behavior` (tipo select, default "200")
|
||||
- [x] Agregar campo `lazy_fill_timeout` al grupo `behavior` (tipo select, default "5000")
|
||||
|
||||
### 1.2 Sincronizar a BD
|
||||
|
||||
- [x] Ejecutar `wp roi-theme sync-component adsense-placement`
|
||||
- [x] Verificar campos creados en BD con valores default
|
||||
|
||||
---
|
||||
|
||||
## FASE 2: Renderer (BD → HTML + CSS)
|
||||
|
||||
### 2.1 Actualizar AdsensePlacementRenderer.php
|
||||
|
||||
- [x] Leer `lazy_loading_enabled` desde settings
|
||||
- [x] Generar CSS dinamico via `CSSGeneratorService`:
|
||||
```php
|
||||
if ($settings['lazy_loading_enabled']) {
|
||||
$this->cssGenerator->generate([
|
||||
'.roi-ad-slot' => ['display' => 'none'],
|
||||
'.roi-ad-slot.roi-ad-filled' => ['display' => 'block'],
|
||||
'.roi-ad-slot.roi-ad-empty' => ['display' => 'none'],
|
||||
]);
|
||||
}
|
||||
```
|
||||
- [x] Agregar `data-ad-lazy="true"` al markup del slot si lazy enabled
|
||||
- [x] Mantener compatibilidad con `lazy_loading_enabled: false`
|
||||
|
||||
### 2.2 Actualizar enqueue-scripts.php (AssetEnqueuer)
|
||||
|
||||
- [x] Leer settings de lazy loading desde BD
|
||||
- [x] Usar `wp_localize_script()` para pasar config a JS:
|
||||
```php
|
||||
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||
'debug' => WP_DEBUG,
|
||||
]);
|
||||
```
|
||||
- [x] Remover cualquier configuracion hardcodeada existente
|
||||
|
||||
### 2.3 Actualizar AdsensePlacementFieldMapper.php
|
||||
|
||||
- [x] Agregar `lazy_loading_enabled` al array de mappings
|
||||
- [x] Agregar `lazy_rootmargin` al array de mappings
|
||||
- [x] Agregar `lazy_fill_timeout` al array de mappings
|
||||
- [x] Verificar tipos correctos (boolean, string, string → parseados en Enqueuer)
|
||||
|
||||
---
|
||||
|
||||
## FASE 3: FormBuilder (UI Admin)
|
||||
|
||||
### 3.1 Actualizar AdsensePlacementFormBuilder.php
|
||||
|
||||
- [x] Agregar seccion "Lazy Loading" dentro del grupo Exclusions/Forms
|
||||
- [x] Agregar toggle para `lazy_loading_enabled`
|
||||
- [x] Agregar select para `lazy_rootmargin` (label: "Pre-carga (px)")
|
||||
- [x] Agregar select para `lazy_fill_timeout` (label: "Timeout fill (ms)")
|
||||
- [x] Agregar nota indicando que cambios requieren vaciar cache
|
||||
|
||||
---
|
||||
|
||||
## FASE 4: JavaScript (Infrastructure)
|
||||
|
||||
### 4.1 Backup
|
||||
|
||||
- [x] Crear backup `adsense-loader.legacy.js`
|
||||
|
||||
### 4.2 Refactorizar adsense-loader.js
|
||||
|
||||
- [x] Refactorizar para leer config de `window.roiAdsenseConfig`
|
||||
- [x] Implementar deteccion de soporte Intersection Observer
|
||||
- [x] Implementar `observeAdSlots()` con Intersection Observer
|
||||
- [x] Implementar `activateAdSlot(slot)` para activacion individual
|
||||
- [x] Implementar MutationObserver para detectar contenido en `<ins>`
|
||||
- [x] Implementar `checkAdFill()` con criterios concretos:
|
||||
- Verificar `data-ad-status` primero
|
||||
- Fallback a verificar children (iframe, div[id])
|
||||
- [x] Implementar timeout por slot para marcar como vacio
|
||||
- [x] Implementar manejo de error de red con retry (2s delay, max 1 retry)
|
||||
- [x] Implementar fallback para navegadores sin soporte
|
||||
- [x] Mantener carga diferida de `adsbygoogle.js` (primera activacion)
|
||||
- [x] Mantener compatibilidad con `lazyEnabled: false` (modo legacy)
|
||||
|
||||
---
|
||||
|
||||
## FASE 5: Validacion
|
||||
|
||||
### 5.1 Validacion de Arquitectura
|
||||
|
||||
- [x] Ejecutar `php Shared/Infrastructure/Scripts/validate-architecture.php adsense-placement`
|
||||
- [x] Verificar que no hay CSS estatico nuevo
|
||||
- [x] Verificar que config viene de BD, no hardcodeada
|
||||
- [x] Verificar que FieldMapper tiene todos los campos
|
||||
|
||||
### 5.2 Testing Local
|
||||
|
||||
- [ ] Probar con lazy_loading_enabled: true
|
||||
- [ ] Verificar ads cargan al scroll (DevTools Network)
|
||||
- [ ] Verificar slots vacios NO se muestran
|
||||
- [ ] Probar con lazy_loading_enabled: false (modo legacy)
|
||||
- [ ] Verificar fallback en navegador sin Intersection Observer
|
||||
- [ ] Medir Core Web Vitals con Lighthouse (antes/despues)
|
||||
|
||||
---
|
||||
|
||||
## POST-IMPLEMENTACION
|
||||
|
||||
### Deploy
|
||||
|
||||
- [ ] Commit con mensaje descriptivo
|
||||
- [ ] Deploy a produccion
|
||||
- [ ] Ejecutar sync-component en produccion
|
||||
- [ ] Vaciar cache (Redis, W3TC)
|
||||
- [ ] Verificar funcionamiento en produccion
|
||||
|
||||
### Monitoreo (24-48h)
|
||||
|
||||
- [ ] Monitorear fill rate en AdSense dashboard
|
||||
- [ ] Verificar no hay errores en consola de usuarios
|
||||
- [ ] Comparar Core Web Vitals antes/despues
|
||||
|
||||
### Cleanup
|
||||
|
||||
- [ ] Remover `debug: true` de adsense-loader.js (ya pendiente)
|
||||
- [ ] Remover debug de ContentAdInjector.php (ya pendiente)
|
||||
- [ ] Remover `adsense-loader.legacy.js` si todo funciona (7+ dias)
|
||||
- [ ] Archivar esta especificacion en `openspec/archive/`
|
||||
1118
openspec/changes/refactor-adsense-lazy-loading/test-plan.md
Normal file
1118
openspec/changes/refactor-adsense-lazy-loading/test-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user