feat(api): implement javascript-first architecture for cache compatibility
- Add REST endpoint GET /roi-theme/v1/adsense-placement/visibility - Add Domain layer: UserContext, VisibilityDecision, AdsenseSettings VOs - Add Application layer: CheckAdsenseVisibilityUseCase - Add Infrastructure: AdsenseVisibilityChecker, Controller, Enqueuer - Add JavaScript controller with localStorage caching - Add test plan for production validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* ROI Theme - AdSense JavaScript-First Visibility Controller
|
||||
*
|
||||
* Mueve las decisiones de visibilidad de anuncios del servidor (PHP) al cliente (JS)
|
||||
* para permitir compatibilidad con cache de pagina mientras mantiene personalizacion
|
||||
* por usuario.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @see openspec/specs/adsense-javascript-first/spec.md
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const CACHE_KEY = 'roi_adsense_visibility';
|
||||
const CACHE_VERSION_KEY = 'roi_adsense_settings_version';
|
||||
|
||||
// Configuracion inyectada por PHP via wp_localize_script
|
||||
const config = window.roiAdsenseConfig || {};
|
||||
|
||||
/**
|
||||
* Logger condicional (solo en modo debug)
|
||||
*/
|
||||
function log(message, type = 'log') {
|
||||
if (!config.debug) return;
|
||||
const prefix = '[ROI AdSense v' + VERSION + ']';
|
||||
console[type](prefix, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta si el dispositivo es movil basado en viewport
|
||||
*/
|
||||
function isMobile() {
|
||||
return window.innerWidth < 992;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene decision cacheada de localStorage
|
||||
*/
|
||||
function getCachedDecision() {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
|
||||
|
||||
if (!cached) {
|
||||
log('No hay decision en cache');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Invalidar si la version de settings cambio
|
||||
if (cachedVersion !== config.settingsVersion) {
|
||||
log('Version de settings cambio, invalidando cache');
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_VERSION_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
|
||||
// Verificar expiracion
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expiresAt = data.timestamp + data.cache_seconds;
|
||||
|
||||
if (now > expiresAt) {
|
||||
log('Cache expirado');
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
log('Usando decision cacheada: ' + (data.show_ads ? 'MOSTRAR' : 'OCULTAR'));
|
||||
return data;
|
||||
} catch (e) {
|
||||
log('Error leyendo cache: ' + e.message, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda decision en localStorage
|
||||
*/
|
||||
function cacheDecision(decision) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(decision));
|
||||
localStorage.setItem(CACHE_VERSION_KEY, config.settingsVersion);
|
||||
log('Decision cacheada por ' + decision.cache_seconds + 's');
|
||||
} catch (e) {
|
||||
log('Error guardando cache: ' + e.message, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta el endpoint REST para obtener decision de visibilidad
|
||||
*/
|
||||
async function fetchVisibilityDecision() {
|
||||
const url = new URL(config.endpoint);
|
||||
url.searchParams.append('post_id', config.postId);
|
||||
|
||||
if (config.nonce) {
|
||||
url.searchParams.append('nonce', config.nonce);
|
||||
}
|
||||
|
||||
log('Consultando endpoint: ' + url.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa los anuncios (muestra placeholders, carga AdSense)
|
||||
*/
|
||||
function activateAds() {
|
||||
log('Activando anuncios');
|
||||
|
||||
// Remover clase de oculto de los containers
|
||||
document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) {
|
||||
el.classList.remove('roi-adsense-hidden');
|
||||
el.classList.add('roi-adsense-active');
|
||||
});
|
||||
|
||||
// Disparar evento para que otros scripts puedan reaccionar
|
||||
document.dispatchEvent(new CustomEvent('roiAdsenseActivated', {
|
||||
detail: { version: VERSION }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Desactiva los anuncios (oculta placeholders)
|
||||
*/
|
||||
function deactivateAds(reasons) {
|
||||
log('Desactivando anuncios. Razones: ' + reasons.join(', '));
|
||||
|
||||
// Agregar clase de oculto a los containers
|
||||
document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) {
|
||||
el.classList.add('roi-adsense-hidden');
|
||||
el.classList.remove('roi-adsense-active');
|
||||
});
|
||||
|
||||
// Disparar evento
|
||||
document.dispatchEvent(new CustomEvent('roiAdsenseDeactivated', {
|
||||
detail: { reasons: reasons, version: VERSION }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica decision de visibilidad
|
||||
*/
|
||||
function applyDecision(decision) {
|
||||
if (decision.show_ads) {
|
||||
activateAds();
|
||||
} else {
|
||||
deactivateAds(decision.reasons || []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja error segun estrategia de fallback configurada
|
||||
*/
|
||||
function handleError(error) {
|
||||
log('Error: ' + error.message, 'error');
|
||||
|
||||
const cached = getCachedDecision();
|
||||
|
||||
switch (config.fallbackStrategy) {
|
||||
case 'cached-or-show':
|
||||
if (cached) {
|
||||
log('Usando cache como fallback');
|
||||
applyDecision(cached);
|
||||
} else {
|
||||
log('Sin cache, mostrando ads por defecto (proteger revenue)');
|
||||
activateAds();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cached-or-hide':
|
||||
if (cached) {
|
||||
log('Usando cache como fallback');
|
||||
applyDecision(cached);
|
||||
} else {
|
||||
log('Sin cache, ocultando ads por defecto');
|
||||
deactivateAds(['fallback_no_cache']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'always-show':
|
||||
log('Fallback: siempre mostrar');
|
||||
activateAds();
|
||||
break;
|
||||
|
||||
default:
|
||||
log('Estrategia desconocida, mostrando ads');
|
||||
activateAds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Funcion principal de inicializacion
|
||||
*/
|
||||
async function init() {
|
||||
log('Inicializando...');
|
||||
|
||||
// Verificar que el feature este habilitado
|
||||
if (!config.featureEnabled) {
|
||||
log('Feature deshabilitado, usando modo legacy', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANTE: postId = 0 es valido (paginas de archivo, home, etc.)
|
||||
// Solo validar que endpoint exista y postId no sea undefined/null
|
||||
if (!config.endpoint || config.postId === undefined || config.postId === null) {
|
||||
log('Sin endpoint configurado, activando ads', 'warn');
|
||||
activateAds();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentar usar cache primero
|
||||
const cached = getCachedDecision();
|
||||
if (cached) {
|
||||
applyDecision(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// Consultar endpoint
|
||||
try {
|
||||
const decision = await fetchVisibilityDecision();
|
||||
log('Respuesta del servidor: ' + JSON.stringify(decision));
|
||||
|
||||
// Cachear decision
|
||||
cacheDecision(decision);
|
||||
|
||||
// Aplicar decision
|
||||
applyDecision(decision);
|
||||
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Ejecutar cuando el DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Exponer API publica para debugging
|
||||
window.roiAdsenseVisibility = {
|
||||
version: VERSION,
|
||||
getConfig: function() { return config; },
|
||||
getCachedDecision: getCachedDecision,
|
||||
clearCache: function() {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_VERSION_KEY);
|
||||
log('Cache limpiado');
|
||||
},
|
||||
forceRefresh: async function() {
|
||||
this.clearCache();
|
||||
await init();
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user