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:
@@ -56,6 +56,11 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'],
|
||||
'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'],
|
||||
|
||||
// BEHAVIOR (Lazy Loading)
|
||||
'adsense-placementLazyLoadingEnabled' => ['group' => 'behavior', 'attribute' => 'lazy_loading_enabled'],
|
||||
'adsense-placementLazyRootmargin' => ['group' => 'behavior', 'attribute' => 'lazy_rootmargin'],
|
||||
'adsense-placementLazyFillTimeout' => ['group' => 'behavior', 'attribute' => 'lazy_fill_timeout'],
|
||||
|
||||
// LAYOUT (Archive/Global locations + formats)
|
||||
'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'],
|
||||
'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'],
|
||||
|
||||
@@ -1182,6 +1182,55 @@ final class AdsensePlacementFormBuilder
|
||||
$delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000');
|
||||
$html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout);
|
||||
|
||||
// Lazy Loading settings
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #198754;"></i>';
|
||||
$html .= ' Lazy Loading (Intersection Observer)';
|
||||
$html .= ' <span class="badge bg-success ms-1">Nuevo</span>';
|
||||
$html .= '</p>';
|
||||
$html .= '<div class="alert alert-info small py-2 mb-2">';
|
||||
$html .= ' <i class="bi bi-lightbulb me-1"></i>';
|
||||
$html .= ' Carga anuncios individualmente al entrar al viewport. Mejora fill rate y reduce CLS.';
|
||||
$html .= '</div>';
|
||||
|
||||
$lazyEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_loading_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'LazyLoadingEnabled', 'Activar Lazy Loading', $lazyEnabled, 'bi-eye');
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$lazyRootmargin = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_rootmargin', '200');
|
||||
$html .= $this->buildSelect($cid . 'LazyRootmargin', 'Pre-carga (px)',
|
||||
(string)$lazyRootmargin,
|
||||
[
|
||||
'0' => '0px (sin pre-carga)',
|
||||
'100' => '100px',
|
||||
'200' => '200px (recomendado)',
|
||||
'300' => '300px',
|
||||
'400' => '400px',
|
||||
'500' => '500px'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$lazyFillTimeout = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_fill_timeout', '5000');
|
||||
$html .= $this->buildSelect($cid . 'LazyFillTimeout', 'Timeout fill (ms)',
|
||||
(string)$lazyFillTimeout,
|
||||
[
|
||||
'3000' => '3 segundos',
|
||||
'5000' => '5 segundos (recomendado)',
|
||||
'7000' => '7 segundos',
|
||||
'10000' => '10 segundos'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="alert alert-warning small py-2 mt-2 mb-0">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' <strong>Nota:</strong> Cambios requieren vaciar cache (Redis, W3TC) para aplicarse.';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
@@ -1,306 +1,639 @@
|
||||
/**
|
||||
* Cargador Retrasado de AdSense
|
||||
* AdSense Lazy Loader con Intersection Observer
|
||||
*
|
||||
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
||||
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
||||
* Carga anuncios AdSense individualmente cuando entran al viewport,
|
||||
* detecta si reciben contenido, y oculta slots vacios.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
* @since 1.5.0
|
||||
* @version 2.0.0 - Refactorizado con Intersection Observer
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuración
|
||||
const CONFIG = {
|
||||
timeout: 5000, // Timeout de fallback en milisegundos
|
||||
loadedClass: 'adsense-loaded',
|
||||
debug: true // TEMPORAL: Habilitado para diagnóstico
|
||||
// =========================================================================
|
||||
// CONFIGURACION
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Configuracion por defecto, sobrescrita por window.roiAdsenseConfig
|
||||
*/
|
||||
var DEFAULT_CONFIG = {
|
||||
lazyEnabled: true,
|
||||
rootMargin: '200px 0px',
|
||||
fillTimeout: 5000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
// Estado
|
||||
let adsenseLoaded = false;
|
||||
let loadTimeout = null;
|
||||
/**
|
||||
* Obtiene configuracion desde wp_localize_script o usa defaults
|
||||
*/
|
||||
function getConfig() {
|
||||
var wpConfig = window.roiAdsenseConfig || {};
|
||||
return {
|
||||
lazyEnabled: typeof wpConfig.lazyEnabled !== 'undefined' ? wpConfig.lazyEnabled : DEFAULT_CONFIG.lazyEnabled,
|
||||
rootMargin: wpConfig.rootMargin || DEFAULT_CONFIG.rootMargin,
|
||||
fillTimeout: typeof wpConfig.fillTimeout !== 'undefined' ? parseInt(wpConfig.fillTimeout, 10) : DEFAULT_CONFIG.fillTimeout,
|
||||
debug: typeof wpConfig.debug !== 'undefined' ? wpConfig.debug : DEFAULT_CONFIG.debug
|
||||
};
|
||||
}
|
||||
|
||||
var CONFIG = getConfig();
|
||||
|
||||
// =========================================================================
|
||||
// ESTADO GLOBAL
|
||||
// =========================================================================
|
||||
|
||||
var libraryLoaded = false;
|
||||
var libraryLoading = false;
|
||||
var libraryLoadFailed = false;
|
||||
var loadRetryCount = 0;
|
||||
var MAX_LOAD_RETRIES = 1;
|
||||
var RETRY_DELAY = 2000;
|
||||
|
||||
/** @type {IntersectionObserver|null} */
|
||||
var slotObserver = null;
|
||||
|
||||
/** @type {Map<Element, MutationObserver>} */
|
||||
var fillObservers = new Map();
|
||||
|
||||
/** @type {Map<Element, number>} */
|
||||
var fillTimeouts = new Map();
|
||||
|
||||
/** @type {Set<Element>} */
|
||||
var activatedSlots = new Set();
|
||||
|
||||
/** @type {Array<Function>} */
|
||||
var pendingActivations = [];
|
||||
|
||||
// =========================================================================
|
||||
// LOGGING
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Registra mensajes de debug si el modo debug está habilitado
|
||||
* @param {string} message - El mensaje a registrar
|
||||
* Log condicional basado en CONFIG.debug
|
||||
* @param {string} message
|
||||
* @param {string} [level='log'] - 'log', 'warn', 'error'
|
||||
*/
|
||||
function debugLog(message) {
|
||||
if (CONFIG.debug && typeof console !== 'undefined') {
|
||||
console.log('[AdSense Loader] ' + message);
|
||||
function debugLog(message, level) {
|
||||
if (!CONFIG.debug || typeof console === 'undefined') {
|
||||
return;
|
||||
}
|
||||
level = level || 'log';
|
||||
var prefix = '[AdSense Lazy] ';
|
||||
if (level === 'error') {
|
||||
console.error(prefix + message);
|
||||
} else if (level === 'warn') {
|
||||
console.warn(prefix + message);
|
||||
} else {
|
||||
console.log(prefix + message);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DETECCION DE SOPORTE
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Carga los scripts de AdSense e inicializa los ads
|
||||
* Verifica si el navegador soporta Intersection Observer
|
||||
*/
|
||||
function loadAdSense() {
|
||||
// Prevenir múltiples cargas
|
||||
if (adsenseLoaded) {
|
||||
debugLog('AdSense ya fue cargado, omitiendo...');
|
||||
function hasIntersectionObserverSupport() {
|
||||
return typeof window.IntersectionObserver !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el navegador soporta MutationObserver
|
||||
*/
|
||||
function hasMutationObserverSupport() {
|
||||
return typeof window.MutationObserver !== 'undefined';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CARGA DE BIBLIOTECA ADSENSE
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Carga la biblioteca adsbygoogle.js
|
||||
* @param {Function} onSuccess
|
||||
* @param {Function} onError
|
||||
*/
|
||||
function loadAdSenseLibrary(onSuccess, onError) {
|
||||
if (libraryLoaded) {
|
||||
debugLog('Biblioteca ya cargada');
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
adsenseLoaded = true;
|
||||
debugLog('Cargando scripts de AdSense...');
|
||||
|
||||
// Limpiar el timeout si existe
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = null;
|
||||
if (libraryLoading) {
|
||||
debugLog('Biblioteca en proceso de carga, encolando callback');
|
||||
pendingActivations.push(onSuccess);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remover event listeners para prevenir múltiples triggers
|
||||
removeEventListeners();
|
||||
libraryLoading = true;
|
||||
debugLog('Cargando biblioteca adsbygoogle.js...');
|
||||
|
||||
// Cargar etiquetas de script de AdSense y esperar a que cargue
|
||||
// IMPORTANTE: Debe esperar a que adsbygoogle.js cargue antes de ejecutar push
|
||||
loadAdSenseScripts(function() {
|
||||
debugLog('Biblioteca AdSense cargada, ejecutando push scripts...');
|
||||
var scriptTags = document.querySelectorAll('script[data-adsense-script]');
|
||||
if (scriptTags.length === 0) {
|
||||
debugLog('No se encontro script[data-adsense-script]', 'warn');
|
||||
libraryLoading = false;
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ejecutar scripts de push de AdSense
|
||||
executeAdSensePushScripts();
|
||||
var oldScript = scriptTags[0];
|
||||
var newScript = document.createElement('script');
|
||||
newScript.src = oldScript.src;
|
||||
newScript.async = true;
|
||||
|
||||
// Agregar clase loaded al body
|
||||
document.body.classList.add(CONFIG.loadedClass);
|
||||
if (oldScript.getAttribute('crossorigin')) {
|
||||
newScript.crossOrigin = oldScript.getAttribute('crossorigin');
|
||||
}
|
||||
|
||||
debugLog('Carga de AdSense completada');
|
||||
});
|
||||
}
|
||||
newScript.onload = function() {
|
||||
debugLog('Biblioteca cargada exitosamente');
|
||||
libraryLoaded = true;
|
||||
libraryLoading = false;
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
/**
|
||||
* Encuentra y carga todas las etiquetas de script de AdSense retrasadas
|
||||
* @param {Function} callback - Función a ejecutar cuando la biblioteca cargue
|
||||
*/
|
||||
function loadAdSenseScripts(callback) {
|
||||
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
|
||||
|
||||
if (delayedScripts.length === 0) {
|
||||
debugLog('No se encontraron scripts retrasados de AdSense');
|
||||
// Ejecutar callback de todas formas (puede haber ads sin script principal)
|
||||
if (typeof callback === 'function') {
|
||||
// Ejecutar callbacks pendientes
|
||||
onSuccess();
|
||||
while (pendingActivations.length > 0) {
|
||||
var callback = pendingActivations.shift();
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
newScript.onerror = function() {
|
||||
debugLog('Error cargando biblioteca (intento ' + (loadRetryCount + 1) + ')', 'error');
|
||||
libraryLoading = false;
|
||||
|
||||
if (loadRetryCount < MAX_LOAD_RETRIES) {
|
||||
loadRetryCount++;
|
||||
debugLog('Reintentando en ' + RETRY_DELAY + 'ms...');
|
||||
setTimeout(function() {
|
||||
loadAdSenseLibrary(onSuccess, onError);
|
||||
}, RETRY_DELAY);
|
||||
} else {
|
||||
debugLog('Maximo de reintentos alcanzado', 'error');
|
||||
libraryLoadFailed = true;
|
||||
onError();
|
||||
}
|
||||
};
|
||||
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca todos los slots como error cuando la biblioteca falla
|
||||
*/
|
||||
function markAllSlotsAsError() {
|
||||
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||
slots.forEach(function(slot) {
|
||||
slot.classList.add('roi-ad-error');
|
||||
cleanupSlot(slot);
|
||||
});
|
||||
debugLog('Todos los slots marcados como error', 'error');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ACTIVACION DE SLOTS
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Activa un slot individual ejecutando adsbygoogle.push()
|
||||
* @param {Element} slot
|
||||
*/
|
||||
function activateSlot(slot) {
|
||||
if (activatedSlots.has(slot)) {
|
||||
debugLog('Slot ya activado, omitiendo');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Se encontraron ' + delayedScripts.length + ' script(s) retrasado(s) de AdSense');
|
||||
if (libraryLoadFailed) {
|
||||
debugLog('Biblioteca fallida, marcando slot como error');
|
||||
slot.classList.add('roi-ad-error');
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptsLoaded = 0;
|
||||
var totalScripts = delayedScripts.length;
|
||||
activatedSlots.add(slot);
|
||||
|
||||
delayedScripts.forEach(function(oldScript) {
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
// Copiar atributos
|
||||
if (oldScript.src) {
|
||||
newScript.src = oldScript.src;
|
||||
var doActivation = function() {
|
||||
var ins = slot.querySelector('ins.adsbygoogle');
|
||||
if (!ins) {
|
||||
debugLog('No se encontro <ins> en slot', 'warn');
|
||||
slot.classList.add('roi-ad-empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Establecer atributo async
|
||||
newScript.async = true;
|
||||
debugLog('Activando slot: ' + (ins.getAttribute('data-ad-slot') || 'unknown'));
|
||||
|
||||
// Copiar crossorigin si está presente
|
||||
if (oldScript.getAttribute('crossorigin')) {
|
||||
newScript.crossorigin = oldScript.getAttribute('crossorigin');
|
||||
// Ejecutar push
|
||||
try {
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
window.adsbygoogle.push({});
|
||||
} catch (e) {
|
||||
debugLog('Error en push: ' + e.message, 'error');
|
||||
slot.classList.add('roi-ad-error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Esperar a que cargue antes de ejecutar callback
|
||||
newScript.onload = function() {
|
||||
scriptsLoaded++;
|
||||
debugLog('Script cargado (' + scriptsLoaded + '/' + totalScripts + '): ' + newScript.src.substring(0, 50) + '...');
|
||||
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
// Iniciar observacion de llenado
|
||||
startFillDetection(slot, ins);
|
||||
};
|
||||
|
||||
newScript.onerror = function() {
|
||||
scriptsLoaded++;
|
||||
debugLog('Error cargando script: ' + newScript.src);
|
||||
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
// Si la biblioteca ya cargo, activar inmediatamente
|
||||
if (libraryLoaded) {
|
||||
doActivation();
|
||||
} else {
|
||||
// Cargar biblioteca y luego activar
|
||||
loadAdSenseLibrary(doActivation, function() {
|
||||
markAllSlotsAsError();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reemplazar script viejo con el nuevo
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
// =========================================================================
|
||||
// DETECCION DE LLENADO
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Inicia la deteccion de llenado para un slot
|
||||
* @param {Element} slot
|
||||
* @param {Element} ins
|
||||
*/
|
||||
function startFillDetection(slot, ins) {
|
||||
// Verificar inmediatamente si ya tiene contenido
|
||||
if (checkFillStatus(slot, ins)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configurar timeout
|
||||
var timeoutId = setTimeout(function() {
|
||||
debugLog('Timeout de llenado alcanzado');
|
||||
markSlotEmpty(slot);
|
||||
}, CONFIG.fillTimeout);
|
||||
fillTimeouts.set(slot, timeoutId);
|
||||
|
||||
// Configurar MutationObserver si hay soporte
|
||||
if (hasMutationObserverSupport()) {
|
||||
var mutationObserver = new MutationObserver(function(mutations) {
|
||||
if (checkFillStatus(slot, ins)) {
|
||||
// Ya procesado en checkFillStatus
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(ins, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributeFilter: ['data-ad-status']
|
||||
});
|
||||
|
||||
fillObservers.set(slot, mutationObserver);
|
||||
} else {
|
||||
// Sin MutationObserver, solo usar timeout
|
||||
debugLog('Sin soporte MutationObserver, usando solo timeout');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica el estado de llenado de un slot
|
||||
* @param {Element} slot
|
||||
* @param {Element} ins
|
||||
* @returns {boolean} true si el estado fue determinado (filled o empty)
|
||||
*/
|
||||
function checkFillStatus(slot, ins) {
|
||||
// Criterio 1: data-ad-status attribute
|
||||
var status = ins.getAttribute('data-ad-status');
|
||||
if (status === 'filled') {
|
||||
debugLog('Slot llenado (data-ad-status=filled)');
|
||||
markSlotFilled(slot);
|
||||
return true;
|
||||
}
|
||||
if (status === 'unfilled') {
|
||||
debugLog('Slot vacio (data-ad-status=unfilled)');
|
||||
markSlotEmpty(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Criterio 2 (fallback): iframe presente
|
||||
var iframe = ins.querySelector('iframe');
|
||||
if (iframe) {
|
||||
debugLog('Slot llenado (iframe detectado)');
|
||||
markSlotFilled(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Criterio 3 (fallback): div con id presente
|
||||
var divWithId = ins.querySelector('div[id]');
|
||||
if (divWithId) {
|
||||
debugLog('Slot llenado (div con id detectado)');
|
||||
markSlotFilled(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca un slot como llenado
|
||||
* @param {Element} slot
|
||||
*/
|
||||
function markSlotFilled(slot) {
|
||||
slot.classList.remove('roi-ad-empty', 'roi-ad-error');
|
||||
slot.classList.add('roi-ad-filled');
|
||||
cleanupSlot(slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca un slot como vacio
|
||||
* @param {Element} slot
|
||||
*/
|
||||
function markSlotEmpty(slot) {
|
||||
slot.classList.remove('roi-ad-filled', 'roi-ad-error');
|
||||
slot.classList.add('roi-ad-empty');
|
||||
cleanupSlot(slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia observadores y timeouts de un slot
|
||||
* @param {Element} slot
|
||||
*/
|
||||
function cleanupSlot(slot) {
|
||||
// Limpiar timeout
|
||||
if (fillTimeouts.has(slot)) {
|
||||
clearTimeout(fillTimeouts.get(slot));
|
||||
fillTimeouts.delete(slot);
|
||||
}
|
||||
|
||||
// Limpiar MutationObserver
|
||||
if (fillObservers.has(slot)) {
|
||||
fillObservers.get(slot).disconnect();
|
||||
fillObservers.delete(slot);
|
||||
}
|
||||
|
||||
// Dejar de observar con IntersectionObserver
|
||||
if (slotObserver) {
|
||||
slotObserver.unobserve(slot);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// INTERSECTION OBSERVER
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Inicializa el Intersection Observer para slots
|
||||
*/
|
||||
function initIntersectionObserver() {
|
||||
if (!hasIntersectionObserverSupport()) {
|
||||
debugLog('Sin soporte Intersection Observer, usando modo legacy', 'warn');
|
||||
return false;
|
||||
}
|
||||
|
||||
var options = {
|
||||
root: null,
|
||||
rootMargin: CONFIG.rootMargin,
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
slotObserver = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.isIntersecting) {
|
||||
var slot = entry.target;
|
||||
debugLog('Slot entro al viewport');
|
||||
activateSlot(slot);
|
||||
}
|
||||
});
|
||||
}, options);
|
||||
|
||||
debugLog('Intersection Observer inicializado con rootMargin: ' + CONFIG.rootMargin);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observa todos los slots lazy en la pagina
|
||||
*/
|
||||
function observeAllSlots() {
|
||||
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||
debugLog('Encontrados ' + slots.length + ' slots para observar');
|
||||
|
||||
slots.forEach(function(slot) {
|
||||
if (!activatedSlots.has(slot)) {
|
||||
slotObserver.observe(slot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta scripts de push de AdSense retrasados
|
||||
* Observa nuevos slots agregados dinamicamente
|
||||
*/
|
||||
function executeAdSensePushScripts() {
|
||||
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||
function observeNewSlots() {
|
||||
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||
var newCount = 0;
|
||||
|
||||
if (delayedPushScripts.length === 0) {
|
||||
debugLog('No se encontraron scripts de push retrasados de AdSense');
|
||||
slots.forEach(function(slot) {
|
||||
if (!activatedSlots.has(slot)) {
|
||||
slotObserver.observe(slot);
|
||||
newCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (newCount > 0) {
|
||||
debugLog('Agregados ' + newCount + ' nuevos slots al observer');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MODO LEGACY (FALLBACK)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Variables para modo legacy
|
||||
*/
|
||||
var legacyLoaded = false;
|
||||
var legacyTimeout = null;
|
||||
|
||||
/**
|
||||
* Carga todos los ads en modo legacy (sin Intersection Observer)
|
||||
*/
|
||||
function loadAllAdsLegacy() {
|
||||
if (legacyLoaded) {
|
||||
return;
|
||||
}
|
||||
legacyLoaded = true;
|
||||
debugLog('Modo legacy: Cargando todos los ads');
|
||||
|
||||
debugLog('Se encontraron ' + delayedPushScripts.length + ' script(s) de push retrasado(s)');
|
||||
if (legacyTimeout) {
|
||||
clearTimeout(legacyTimeout);
|
||||
}
|
||||
removeLegacyEventListeners();
|
||||
|
||||
loadAdSenseLibrary(function() {
|
||||
executeAllPushScripts();
|
||||
}, function() {
|
||||
debugLog('Error en modo legacy', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta todos los scripts de push en modo legacy
|
||||
*/
|
||||
function executeAllPushScripts() {
|
||||
var pushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||
debugLog('Ejecutando ' + pushScripts.length + ' scripts de push');
|
||||
|
||||
// Inicializar array adsbygoogle si no existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
delayedPushScripts.forEach(function(oldScript) {
|
||||
const scriptContent = oldScript.innerHTML;
|
||||
|
||||
// Crear y ejecutar nuevo script
|
||||
const newScript = document.createElement('script');
|
||||
newScript.innerHTML = scriptContent;
|
||||
pushScripts.forEach(function(oldScript) {
|
||||
var newScript = document.createElement('script');
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
newScript.type = 'text/javascript';
|
||||
|
||||
// Reemplazar script viejo con el nuevo
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
|
||||
document.body.classList.add('adsense-loaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejador de eventos para interacciones del usuario
|
||||
* Event handler para modo legacy
|
||||
*/
|
||||
function handleUserInteraction() {
|
||||
debugLog('Interacción del usuario detectada');
|
||||
loadAdSense();
|
||||
function handleLegacyInteraction() {
|
||||
debugLog('Interaccion detectada (modo legacy)');
|
||||
loadAllAdsLegacy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remueve todos los event listeners
|
||||
* Agrega listeners para modo legacy
|
||||
*/
|
||||
function removeEventListeners() {
|
||||
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('click', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
|
||||
function addLegacyEventListeners() {
|
||||
window.addEventListener('scroll', handleLegacyInteraction, { passive: true, once: true });
|
||||
window.addEventListener('mousemove', handleLegacyInteraction, { passive: true, once: true });
|
||||
window.addEventListener('touchstart', handleLegacyInteraction, { passive: true, once: true });
|
||||
window.addEventListener('click', handleLegacyInteraction, { passive: true, once: true });
|
||||
window.addEventListener('keydown', handleLegacyInteraction, { passive: true, once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega event listeners para interacciones del usuario
|
||||
* Remueve listeners de modo legacy
|
||||
*/
|
||||
function addEventListeners() {
|
||||
debugLog('Agregando event listeners para interacción del usuario');
|
||||
|
||||
// Evento de scroll - cargar en primer scroll
|
||||
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Movimiento de mouse - cargar cuando el usuario mueve el mouse
|
||||
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos táctiles - cargar en primer toque (móviles)
|
||||
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos de click - cargar en primer click
|
||||
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos de teclado - cargar en primera pulsación de tecla
|
||||
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
|
||||
function removeLegacyEventListeners() {
|
||||
window.removeEventListener('scroll', handleLegacyInteraction, { passive: true });
|
||||
window.removeEventListener('mousemove', handleLegacyInteraction, { passive: true });
|
||||
window.removeEventListener('touchstart', handleLegacyInteraction, { passive: true });
|
||||
window.removeEventListener('click', handleLegacyInteraction, { passive: true });
|
||||
window.removeEventListener('keydown', handleLegacyInteraction, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece timeout de fallback para cargar AdSense después del tiempo especificado
|
||||
* Inicia modo legacy con listeners de interaccion
|
||||
*/
|
||||
function setTimeoutFallback() {
|
||||
debugLog('Estableciendo timeout de fallback (' + CONFIG.timeout + 'ms)');
|
||||
function initLegacyMode() {
|
||||
debugLog('Iniciando modo legacy');
|
||||
addLegacyEventListeners();
|
||||
|
||||
loadTimeout = setTimeout(function() {
|
||||
debugLog('Timeout alcanzado, cargando AdSense');
|
||||
loadAdSense();
|
||||
}, CONFIG.timeout);
|
||||
legacyTimeout = setTimeout(function() {
|
||||
debugLog('Timeout legacy alcanzado');
|
||||
loadAllAdsLegacy();
|
||||
}, CONFIG.fillTimeout);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EVENTO DINAMICO
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
* Configura listener para ads dinamicos
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
if (CONFIG.lazyEnabled && slotObserver) {
|
||||
observeNewSlots();
|
||||
} else if (!legacyLoaded) {
|
||||
loadAllAdsLegacy();
|
||||
} else {
|
||||
// Ya cargado en legacy, ejecutar nuevos push
|
||||
activateDynamicSlotsLegacy();
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
* Activa slots dinamicos en modo legacy
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
function activateDynamicSlotsLegacy() {
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slots dinamicos (legacy)');
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
debugLog('Error activando slot dinamico: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// INICIALIZACION
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
* Inicializa el sistema
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
// Siempre configurar listener para ads dinamicos
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
debugLog('Listener dinamico configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
// Verificar si delay esta habilitado globalmente
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
debugLog('Delay global no habilitado');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Inicializando cargador retrasado de AdSense');
|
||||
debugLog('Inicializando AdSense Lazy Loader v2.0');
|
||||
debugLog('Config: lazyEnabled=' + CONFIG.lazyEnabled + ', rootMargin=' + CONFIG.rootMargin + ', fillTimeout=' + CONFIG.fillTimeout);
|
||||
|
||||
// Verificar si la página ya está interactiva o completa
|
||||
// Decidir modo de operacion
|
||||
if (!CONFIG.lazyEnabled) {
|
||||
debugLog('Lazy loading deshabilitado, usando modo legacy');
|
||||
initLegacyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentar inicializar Intersection Observer
|
||||
var observerInitialized = initIntersectionObserver();
|
||||
|
||||
if (!observerInitialized) {
|
||||
// Fallback a modo legacy
|
||||
initLegacyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Esperar a que el DOM este listo
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
debugLog('Página ya cargada, iniciando listeners');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
observeAllSlots();
|
||||
} else {
|
||||
// Esperar a que el DOM esté listo
|
||||
debugLog('Esperando a DOMContentLoaded');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
debugLog('DOMContentLoaded disparado');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
observeAllSlots();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar inicialización
|
||||
// Iniciar
|
||||
init();
|
||||
|
||||
})();
|
||||
|
||||
306
Assets/Js/adsense-loader.legacy.js
Normal file
306
Assets/Js/adsense-loader.legacy.js
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Cargador Retrasado de AdSense
|
||||
*
|
||||
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
||||
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuración
|
||||
const CONFIG = {
|
||||
timeout: 5000, // Timeout de fallback en milisegundos
|
||||
loadedClass: 'adsense-loaded',
|
||||
debug: true // TEMPORAL: Habilitado para diagnóstico
|
||||
};
|
||||
|
||||
// Estado
|
||||
let adsenseLoaded = false;
|
||||
let loadTimeout = null;
|
||||
|
||||
/**
|
||||
* Registra mensajes de debug si el modo debug está habilitado
|
||||
* @param {string} message - El mensaje a registrar
|
||||
*/
|
||||
function debugLog(message) {
|
||||
if (CONFIG.debug && typeof console !== 'undefined') {
|
||||
console.log('[AdSense Loader] ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga los scripts de AdSense e inicializa los ads
|
||||
*/
|
||||
function loadAdSense() {
|
||||
// Prevenir múltiples cargas
|
||||
if (adsenseLoaded) {
|
||||
debugLog('AdSense ya fue cargado, omitiendo...');
|
||||
return;
|
||||
}
|
||||
|
||||
adsenseLoaded = true;
|
||||
debugLog('Cargando scripts de AdSense...');
|
||||
|
||||
// Limpiar el timeout si existe
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = null;
|
||||
}
|
||||
|
||||
// Remover event listeners para prevenir múltiples triggers
|
||||
removeEventListeners();
|
||||
|
||||
// Cargar etiquetas de script de AdSense y esperar a que cargue
|
||||
// IMPORTANTE: Debe esperar a que adsbygoogle.js cargue antes de ejecutar push
|
||||
loadAdSenseScripts(function() {
|
||||
debugLog('Biblioteca AdSense cargada, ejecutando push scripts...');
|
||||
|
||||
// Ejecutar scripts de push de AdSense
|
||||
executeAdSensePushScripts();
|
||||
|
||||
// Agregar clase loaded al body
|
||||
document.body.classList.add(CONFIG.loadedClass);
|
||||
|
||||
debugLog('Carga de AdSense completada');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encuentra y carga todas las etiquetas de script de AdSense retrasadas
|
||||
* @param {Function} callback - Función a ejecutar cuando la biblioteca cargue
|
||||
*/
|
||||
function loadAdSenseScripts(callback) {
|
||||
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
|
||||
|
||||
if (delayedScripts.length === 0) {
|
||||
debugLog('No se encontraron scripts retrasados de AdSense');
|
||||
// Ejecutar callback de todas formas (puede haber ads sin script principal)
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Se encontraron ' + delayedScripts.length + ' script(s) retrasado(s) de AdSense');
|
||||
|
||||
var scriptsLoaded = 0;
|
||||
var totalScripts = delayedScripts.length;
|
||||
|
||||
delayedScripts.forEach(function(oldScript) {
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
// Copiar atributos
|
||||
if (oldScript.src) {
|
||||
newScript.src = oldScript.src;
|
||||
}
|
||||
|
||||
// Establecer atributo async
|
||||
newScript.async = true;
|
||||
|
||||
// Copiar crossorigin si está presente
|
||||
if (oldScript.getAttribute('crossorigin')) {
|
||||
newScript.crossorigin = oldScript.getAttribute('crossorigin');
|
||||
}
|
||||
|
||||
// Esperar a que cargue antes de ejecutar callback
|
||||
newScript.onload = function() {
|
||||
scriptsLoaded++;
|
||||
debugLog('Script cargado (' + scriptsLoaded + '/' + totalScripts + '): ' + newScript.src.substring(0, 50) + '...');
|
||||
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
newScript.onerror = function() {
|
||||
scriptsLoaded++;
|
||||
debugLog('Error cargando script: ' + newScript.src);
|
||||
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
// Reemplazar script viejo con el nuevo
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta scripts de push de AdSense retrasados
|
||||
*/
|
||||
function executeAdSensePushScripts() {
|
||||
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||
|
||||
if (delayedPushScripts.length === 0) {
|
||||
debugLog('No se encontraron scripts de push retrasados de AdSense');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Se encontraron ' + delayedPushScripts.length + ' script(s) de push retrasado(s)');
|
||||
|
||||
// Inicializar array adsbygoogle si no existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
delayedPushScripts.forEach(function(oldScript) {
|
||||
const scriptContent = oldScript.innerHTML;
|
||||
|
||||
// Crear y ejecutar nuevo script
|
||||
const newScript = document.createElement('script');
|
||||
newScript.innerHTML = scriptContent;
|
||||
newScript.type = 'text/javascript';
|
||||
|
||||
// Reemplazar script viejo con el nuevo
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejador de eventos para interacciones del usuario
|
||||
*/
|
||||
function handleUserInteraction() {
|
||||
debugLog('Interacción del usuario detectada');
|
||||
loadAdSense();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remueve todos los event listeners
|
||||
*/
|
||||
function removeEventListeners() {
|
||||
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('click', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega event listeners para interacciones del usuario
|
||||
*/
|
||||
function addEventListeners() {
|
||||
debugLog('Agregando event listeners para interacción del usuario');
|
||||
|
||||
// Evento de scroll - cargar en primer scroll
|
||||
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Movimiento de mouse - cargar cuando el usuario mueve el mouse
|
||||
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos táctiles - cargar en primer toque (móviles)
|
||||
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos de click - cargar en primer click
|
||||
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Eventos de teclado - cargar en primera pulsación de tecla
|
||||
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece timeout de fallback para cargar AdSense después del tiempo especificado
|
||||
*/
|
||||
function setTimeoutFallback() {
|
||||
debugLog('Estableciendo timeout de fallback (' + CONFIG.timeout + 'ms)');
|
||||
|
||||
loadTimeout = setTimeout(function() {
|
||||
debugLog('Timeout alcanzado, cargando AdSense');
|
||||
loadAdSense();
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Inicializando cargador retrasado de AdSense');
|
||||
|
||||
// Verificar si la página ya está interactiva o completa
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
debugLog('Página ya cargada, iniciando listeners');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
} else {
|
||||
// Esperar a que el DOM esté listo
|
||||
debugLog('Esperando a DOMContentLoaded');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
debugLog('DOMContentLoaded disparado');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar inicialización
|
||||
init();
|
||||
|
||||
})();
|
||||
@@ -489,6 +489,18 @@ function roi_enqueue_adsense_loader() {
|
||||
'strategy' => 'defer',
|
||||
)
|
||||
);
|
||||
|
||||
// Pasar configuración de lazy loading a JavaScript
|
||||
$lazy_enabled = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_loading_enabled', true);
|
||||
$lazy_rootmargin = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_rootmargin', '200');
|
||||
$lazy_fill_timeout = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_fill_timeout', '5000');
|
||||
|
||||
wp_localize_script('roi-adsense-loader', 'roiAdsenseConfig', array(
|
||||
'lazyEnabled' => (bool) $lazy_enabled,
|
||||
'rootMargin' => (int) $lazy_rootmargin . 'px 0px',
|
||||
'fillTimeout' => (int) $lazy_fill_timeout,
|
||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
||||
));
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_adsense_loader', 10);
|
||||
|
||||
@@ -64,10 +64,12 @@ final class AdsensePlacementRenderer
|
||||
}
|
||||
|
||||
// 4. Generar CSS (usando CSSGeneratorService)
|
||||
$lazyEnabled = ($settings['behavior']['lazy_loading_enabled'] ?? true) === true;
|
||||
|
||||
$css = $this->cssGenerator->generate(
|
||||
".roi-ad-slot",
|
||||
[
|
||||
'display' => 'block',
|
||||
'display' => $lazyEnabled ? 'none' : 'block',
|
||||
'width' => '100%',
|
||||
'min_width' => '300px',
|
||||
'margin_top' => '1.5rem',
|
||||
@@ -76,6 +78,12 @@ final class AdsensePlacementRenderer
|
||||
]
|
||||
);
|
||||
|
||||
// CSS para slots con lazy loading que reciben contenido
|
||||
if ($lazyEnabled) {
|
||||
$css .= $this->cssGenerator->generate('.roi-ad-slot.roi-ad-filled', ['display' => 'block']);
|
||||
$css .= $this->cssGenerator->generate('.roi-ad-slot.roi-ad-empty', ['display' => 'none']);
|
||||
}
|
||||
|
||||
// 5. Generar HTML del anuncio
|
||||
$html = $this->buildAdHTML(
|
||||
$settings,
|
||||
@@ -161,6 +169,7 @@ final class AdsensePlacementRenderer
|
||||
{
|
||||
$publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
|
||||
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
||||
$lazyEnabled = ($settings['behavior']['lazy_loading_enabled'] ?? true) === true;
|
||||
|
||||
if (empty($publisherId)) {
|
||||
return '';
|
||||
@@ -174,9 +183,10 @@ final class AdsensePlacementRenderer
|
||||
|
||||
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
||||
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
||||
$lazyAttr = $lazyEnabled ? ' data-ad-lazy="true"' : '';
|
||||
$locationClass = 'roi-ad-' . esc_attr(str_replace('_', '-', $location));
|
||||
|
||||
return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr);
|
||||
return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr, $lazyAttr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,68 +227,69 @@ final class AdsensePlacementRenderer
|
||||
string $locationClass,
|
||||
string $visClasses,
|
||||
string $scriptType,
|
||||
string $dataAttr
|
||||
string $dataAttr,
|
||||
string $lazyAttr = ''
|
||||
): string {
|
||||
$allClasses = trim("{$locationClass} {$visClasses}");
|
||||
|
||||
return match($format) {
|
||||
'display' => $this->adDisplay($client, $slot, 728, 90, $allClasses, $scriptType, $dataAttr),
|
||||
'display-large' => $this->adDisplay($client, $slot, 970, 250, $allClasses, $scriptType, $dataAttr),
|
||||
'display-square' => $this->adDisplay($client, $slot, 300, 250, $allClasses, $scriptType, $dataAttr),
|
||||
'in-article' => $this->adInArticle($client, $slot, $allClasses, $scriptType, $dataAttr),
|
||||
'autorelaxed' => $this->adAutorelaxed($client, $slot, $allClasses, $scriptType, $dataAttr),
|
||||
default => $this->adAuto($client, $slot, $allClasses, $scriptType, $dataAttr),
|
||||
'display' => $this->adDisplay($client, $slot, 728, 90, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
'display-large' => $this->adDisplay($client, $slot, 970, 250, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
'display-square' => $this->adDisplay($client, $slot, 300, 250, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
'in-article' => $this->adInArticle($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
'autorelaxed' => $this->adAutorelaxed($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
default => $this->adAuto($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||
};
|
||||
}
|
||||
|
||||
private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a): string
|
||||
private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a, string $lazy = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="roi-ad-slot %s">
|
||||
'<div class="roi-ad-slot %s"%s>
|
||||
<ins class="adsbygoogle" style="display:inline-block;width:%dpx;height:%dpx"
|
||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||
</div>',
|
||||
esc_attr($cl), $w, $h, esc_attr($c), esc_attr($s), $t, $a
|
||||
esc_attr($cl), $lazy, $w, $h, esc_attr($c), esc_attr($s), $t, $a
|
||||
);
|
||||
}
|
||||
|
||||
private function adAuto(string $c, string $s, string $cl, string $t, string $a): string
|
||||
private function adAuto(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="roi-ad-slot %s">
|
||||
'<div class="roi-ad-slot %s"%s>
|
||||
<ins class="adsbygoogle" style="display:block;min-height:250px"
|
||||
data-ad-client="%s" data-ad-slot="%s"
|
||||
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||
</div>',
|
||||
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 adInArticle(string $c, string $s, string $cl, string $t, string $a): string
|
||||
private function adInArticle(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="roi-ad-slot %s">
|
||||
'<div class="roi-ad-slot %s"%s>
|
||||
<ins class="adsbygoogle" style="display:block;text-align:center;min-height:200px"
|
||||
data-ad-layout="in-article" data-ad-format="fluid"
|
||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||
</div>',
|
||||
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(
|
||||
'<div class="roi-ad-slot %s">
|
||||
'<div class="roi-ad-slot %s"%s>
|
||||
<ins class="adsbygoogle" style="display:block;min-height:280px"
|
||||
data-ad-format="autorelaxed"
|
||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||
</div>',
|
||||
esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
|
||||
esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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