Files
roi-theme/openspec/changes/refactor-adsense-lazy-loading/specs/adsense-lazy-loading/spec.md
FrankZamora 179a83e9cd 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>
2025-12-10 15:48:20 -06:00

13 KiB

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)