',
- esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
+ esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
);
}
- private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a): string
+ private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
{
return sprintf(
- '
+ '
',
- esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
+ esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
);
}
diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json
index 9a8bc781..e682c4b7 100644
--- a/Schemas/adsense-placement.json
+++ b/Schemas/adsense-placement.json
@@ -1,6 +1,6 @@
{
"component_name": "adsense-placement",
- "version": "1.4.0",
+ "version": "1.5.0",
"description": "Control de AdSense y Google Analytics - Con In-Content Ads Avanzado",
"groups": {
"visibility": {
@@ -423,6 +423,41 @@
"700": "700px (Debajo del fold)"
},
"description": "Distancia vertical desde el top del viewport"
+ },
+ "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)"
+ },
+ "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"
+ },
+ "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"
}
}
},
diff --git a/openspec/changes/refactor-adsense-lazy-loading/design.md b/openspec/changes/refactor-adsense-lazy-loading/design.md
new file mode 100644
index 00000000..a2d83893
--- /dev/null
+++ b/openspec/changes/refactor-adsense-lazy-loading/design.md
@@ -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 `
` contiene al menos un hijo
+2. **Y** ese hijo es un `