Files
roi-theme/Assets/Js/adsense-loader.js
FrankZamora a2dfd10f9e fix(js): implement google official css for unfilled adsense slots
Remove eager loading, revert to Intersection Observer with 600px
rootMargin. Use google css: ins[data-ad-status=unfilled]{display:none}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 17:29:44 -06:00

664 lines
22 KiB
JavaScript

/**
* AdSense Lazy Loader con Intersection Observer
*
* Carga anuncios AdSense individualmente cuando entran al viewport,
* detecta si reciben contenido, y oculta slots vacios.
*
* @package ROI_Theme
* @since 1.5.0
* @version 2.0.0 - Refactorizado con Intersection Observer
*/
(function() {
'use strict';
// =========================================================================
// CONFIGURACION
// =========================================================================
/**
* Configuracion por defecto, sobrescrita por window.roiAdsenseConfig
*
* rootMargin: 600px precarga slots 600px antes de entrar al viewport.
* Esto da tiempo suficiente para que AdSense cargue el anuncio antes
* de que el usuario llegue al slot, evitando layout shift.
* Basado en best practices: https://support.google.com/adsense/answer/10762946
*/
var DEFAULT_CONFIG = {
lazyEnabled: true,
rootMargin: '600px 0px',
fillTimeout: 5000,
debug: false
};
/**
* 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
// =========================================================================
/**
* Log condicional basado en CONFIG.debug
* @param {string} message
* @param {string} [level='log'] - 'log', 'warn', 'error'
*/
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
// =========================================================================
/**
* Verifica si el navegador soporta Intersection Observer
*/
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;
}
if (libraryLoading) {
debugLog('Biblioteca en proceso de carga, encolando callback');
pendingActivations.push(onSuccess);
return;
}
libraryLoading = true;
debugLog('Cargando biblioteca adsbygoogle.js...');
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;
}
var oldScript = scriptTags[0];
var newScript = document.createElement('script');
newScript.src = oldScript.src;
newScript.async = true;
if (oldScript.getAttribute('crossorigin')) {
newScript.crossOrigin = oldScript.getAttribute('crossorigin');
}
newScript.onload = function() {
debugLog('Biblioteca cargada exitosamente');
libraryLoaded = true;
libraryLoading = false;
window.adsbygoogle = window.adsbygoogle || [];
// 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;
}
if (libraryLoadFailed) {
debugLog('Biblioteca fallida, marcando slot como error');
slot.classList.add('roi-ad-error');
return;
}
activatedSlots.add(slot);
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;
}
debugLog('Activando slot: ' + (ins.getAttribute('data-ad-slot') || 'unknown'));
// 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;
}
// Iniciar observacion de llenado
startFillDetection(slot, ins);
};
// Si la biblioteca ya cargo, activar inmediatamente
if (libraryLoaded) {
doActivation();
} else {
// Cargar biblioteca y luego activar
loadAdSenseLibrary(doActivation, function() {
markAllSlotsAsError();
});
}
}
// =========================================================================
// DETECCION DE LLENADO
// =========================================================================
/** @type {Map<Element, number>} */
var pollIntervals = new Map();
/** Intervalo de polling rapido en ms */
var POLL_INTERVAL = 50;
/** Maximo de intentos de polling (50ms * 60 = 3 segundos max) */
var MAX_POLL_ATTEMPTS = 60;
/**
* 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;
}
// Estrategia: Polling rapido (50ms) para detectar data-ad-status lo antes posible.
// AdSense establece data-ad-status muy rapido despues de inyectar el iframe,
// pero MutationObserver a veces no lo detecta inmediatamente.
var pollCount = 0;
var pollId = setInterval(function() {
pollCount++;
if (checkFillStatus(slot, ins)) {
// Estado detectado, limpiar polling
clearInterval(pollId);
pollIntervals.delete(slot);
return;
}
// Si alcanzamos el maximo de intentos, marcar como vacio
if (pollCount >= MAX_POLL_ATTEMPTS) {
debugLog('Polling timeout alcanzado (' + (pollCount * POLL_INTERVAL) + 'ms)');
clearInterval(pollId);
pollIntervals.delete(slot);
markSlotEmpty(slot);
}
}, POLL_INTERVAL);
pollIntervals.set(slot, pollId);
}
/**
* 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) {
// IMPORTANTE: Solo data-ad-status es confiable para determinar el estado final.
// AdSense inyecta iframe ANTES de establecer data-ad-status, por lo que
// la presencia de iframe NO indica que el anuncio fue llenado.
var status = ins.getAttribute('data-ad-status');
// Estado definitivo: filled
if (status === 'filled') {
debugLog('Slot llenado (data-ad-status=filled)');
markSlotFilled(slot);
return true;
}
// Estado definitivo: unfilled (sin anuncio disponible)
if (status === 'unfilled') {
debugLog('Slot vacio (data-ad-status=unfilled)');
markSlotEmpty(slot);
return true;
}
// Si no hay data-ad-status, AdSense aun no ha respondido.
// NO usar iframe como criterio porque AdSense inyecta iframe incluso para unfilled.
// El MutationObserver seguira observando hasta que data-ad-status aparezca o timeout.
debugLog('Esperando data-ad-status...');
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, timeouts e intervalos de un slot
* @param {Element} slot
*/
function cleanupSlot(slot) {
// Limpiar polling interval
if (pollIntervals.has(slot)) {
clearInterval(pollIntervals.get(slot));
pollIntervals.delete(slot);
}
// Limpiar timeout (legacy)
if (fillTimeouts.has(slot)) {
clearTimeout(fillTimeouts.get(slot));
fillTimeouts.delete(slot);
}
// Limpiar MutationObserver (legacy)
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);
}
});
}
/**
* Observa nuevos slots agregados dinamicamente
*/
function observeNewSlots() {
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
var newCount = 0;
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');
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');
window.adsbygoogle = window.adsbygoogle || [];
pushScripts.forEach(function(oldScript) {
var newScript = document.createElement('script');
newScript.innerHTML = oldScript.innerHTML;
newScript.type = 'text/javascript';
oldScript.parentNode.replaceChild(newScript, oldScript);
});
document.body.classList.add('adsense-loaded');
}
/**
* Event handler para modo legacy
*/
function handleLegacyInteraction() {
debugLog('Interaccion detectada (modo legacy)');
loadAllAdsLegacy();
}
/**
* Agrega listeners para modo legacy
*/
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 });
}
/**
* Remueve listeners de modo legacy
*/
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 });
}
/**
* Inicia modo legacy con listeners de interaccion
*/
function initLegacyMode() {
debugLog('Iniciando modo legacy');
addLegacyEventListeners();
legacyTimeout = setTimeout(function() {
debugLog('Timeout legacy alcanzado');
loadAllAdsLegacy();
}, CONFIG.fillTimeout);
}
// =========================================================================
// EVENTO DINAMICO
// =========================================================================
/**
* Configura listener para ads dinamicos
*/
function setupDynamicAdsListener() {
window.addEventListener('roi-adsense-activate', function() {
debugLog('Evento roi-adsense-activate recibido');
if (CONFIG.lazyEnabled && slotObserver) {
observeNewSlots();
} else if (!legacyLoaded) {
loadAllAdsLegacy();
} else {
// Ya cargado en legacy, ejecutar nuevos push
activateDynamicSlotsLegacy();
}
});
}
/**
* Activa slots dinamicos en modo legacy
*/
function activateDynamicSlotsLegacy() {
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
if (pendingPushScripts.length === 0) {
return;
}
debugLog('Activando ' + pendingPushScripts.length + ' slots dinamicos (legacy)');
window.adsbygoogle = window.adsbygoogle || [];
pendingPushScripts.forEach(function(oldScript) {
try {
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.innerHTML = oldScript.innerHTML;
oldScript.parentNode.replaceChild(newScript, oldScript);
} catch (e) {
debugLog('Error activando slot dinamico: ' + e.message, 'error');
}
});
}
// =========================================================================
// INICIALIZACION
// =========================================================================
/**
* Inicializa el sistema
*
* ESTRATEGIA v2.2 (basada en documentacion oficial de Google):
* - Los slots NO estan ocultos inicialmente (Google puede no ejecutar requests para slots ocultos)
* - Usamos Intersection Observer con rootMargin grande (600px) para precargar
* - Google automaticamente oculta slots unfilled via CSS: ins[data-ad-status="unfilled"]
* - Nuestro CSS colapsa el contenedor .roi-ad-slot cuando el ins tiene unfilled
* - Esto funciona MEJOR que eager loading porque no satura AdSense con requests simultaneos
*/
function init() {
// Siempre configurar listener para ads dinamicos
setupDynamicAdsListener();
debugLog('Listener dinamico configurado');
// Verificar si delay esta habilitado globalmente
if (!window.roiAdsenseDelayed) {
debugLog('Delay global no habilitado');
return;
}
debugLog('Inicializando AdSense Lazy Loader v2.2 (IO + Google Official CSS)');
debugLog('Config: lazyEnabled=' + CONFIG.lazyEnabled + ', rootMargin=' + CONFIG.rootMargin + ', fillTimeout=' + CONFIG.fillTimeout);
// Decidir modo de operacion
if (!CONFIG.lazyEnabled) {
debugLog('Lazy loading deshabilitado, usando modo legacy');
initLegacyMode();
return;
}
// Verificar soporte para Intersection Observer
if (!hasIntersectionObserverSupport()) {
debugLog('Sin soporte IO, usando modo legacy', 'warn');
initLegacyMode();
return;
}
// Inicializar Intersection Observer
if (!initIntersectionObserver()) {
debugLog('Fallo inicializando IO, usando modo legacy', 'warn');
initLegacyMode();
return;
}
// Esperar a que el DOM este listo y observar slots
if (document.readyState === 'interactive' || document.readyState === 'complete') {
observeAllSlots();
} else {
document.addEventListener('DOMContentLoaded', function() {
observeAllSlots();
});
}
}
// Iniciar
init();
})();