553 lines
22 KiB
PHP
553 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
|
|
|
/**
|
|
* Renderer para slots de AdSense
|
|
*
|
|
* Responsabilidad:
|
|
* - Generar HTML de bloques de anuncios
|
|
* - Aplicar visibilidad desktop/mobile
|
|
* - NO hardcodear CSS (usar CSSGeneratorInterface)
|
|
*/
|
|
final class AdsensePlacementRenderer
|
|
{
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator
|
|
) {}
|
|
|
|
/**
|
|
* Identifica el componente que soporta
|
|
*/
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'adsense-placement';
|
|
}
|
|
|
|
/**
|
|
* Renderiza un slot de anuncio
|
|
*
|
|
* @param array $settings Configuracion desde BD
|
|
* @param string $location Ubicacion (post-top, sidebar, etc.)
|
|
* @return string HTML del anuncio o vacio
|
|
*/
|
|
public function renderSlot(array $settings, string $location): string
|
|
{
|
|
// 1. Validar is_enabled
|
|
if (!($settings['visibility']['is_enabled'] ?? false)) {
|
|
return '';
|
|
}
|
|
|
|
// 2. Calcular clases de visibilidad
|
|
$visibilityClasses = $this->getVisibilityClasses(
|
|
$settings['visibility']['show_on_desktop'] ?? true,
|
|
$settings['visibility']['show_on_mobile'] ?? true
|
|
);
|
|
|
|
if ($visibilityClasses === null) {
|
|
return ''; // Ambos false = no renderizar
|
|
}
|
|
|
|
// 3. Obtener configuracion de la ubicacion
|
|
$locationConfig = $this->getLocationConfig($settings, $location);
|
|
if (!$locationConfig['enabled']) {
|
|
return '';
|
|
}
|
|
|
|
// 4. Generar CSS (usando CSSGeneratorService)
|
|
$css = $this->cssGenerator->generate(
|
|
".roi-ad-slot",
|
|
[
|
|
'display' => 'block',
|
|
'width' => '100%',
|
|
'min_width' => '300px',
|
|
'margin_top' => '1.5rem',
|
|
'margin_bottom' => '1.5rem',
|
|
'text_align' => 'center',
|
|
]
|
|
);
|
|
|
|
// 5. Generar HTML del anuncio
|
|
$html = $this->buildAdHTML(
|
|
$settings,
|
|
$locationConfig['format'],
|
|
$location,
|
|
$visibilityClasses
|
|
);
|
|
|
|
return "<style>{$css}</style>\n{$html}";
|
|
}
|
|
|
|
/**
|
|
* Tabla de decision Bootstrap para visibilidad
|
|
*/
|
|
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
|
{
|
|
if (!$desktop && !$mobile) {
|
|
return null;
|
|
}
|
|
if (!$desktop && $mobile) {
|
|
return 'd-lg-none';
|
|
}
|
|
if ($desktop && !$mobile) {
|
|
return 'd-none d-lg-block';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Obtiene configuracion de una ubicacion especifica
|
|
*/
|
|
private function getLocationConfig(array $settings, string $location): array
|
|
{
|
|
$locationKey = str_replace('-', '_', $location);
|
|
|
|
// Manejar ubicaciones de in-content (post_content_1, post_content_2, etc.)
|
|
if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) {
|
|
// In-content ads heredan la configuracion de post_content
|
|
return [
|
|
'enabled' => $settings['behavior']['post_content_enabled'] ?? false,
|
|
'format' => $settings['behavior']['post_content_format'] ?? 'in-article',
|
|
];
|
|
}
|
|
|
|
// Mapeo de ubicaciones a grupos y campos
|
|
$locationMap = [
|
|
'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'],
|
|
'post_bottom' => ['group' => 'behavior', 'enabled' => 'post_bottom_enabled', 'format' => 'post_bottom_format'],
|
|
'after_related' => ['group' => 'behavior', 'enabled' => 'after_related_enabled', 'format' => 'after_related_format'],
|
|
'archive_top' => ['group' => 'layout', 'enabled' => 'archive_top_enabled', 'format' => 'archive_format'],
|
|
'archive_between' => ['group' => 'layout', 'enabled' => 'archive_between_enabled', 'format' => 'archive_format'],
|
|
'archive_bottom' => ['group' => 'layout', 'enabled' => 'archive_bottom_enabled', 'format' => 'archive_format'],
|
|
'header_below' => ['group' => 'layout', 'enabled' => 'header_below_enabled', 'format' => 'global_format'],
|
|
'footer_above' => ['group' => 'layout', 'enabled' => 'footer_above_enabled', 'format' => 'global_format'],
|
|
];
|
|
|
|
if (!isset($locationMap[$locationKey])) {
|
|
return ['enabled' => false, 'format' => 'auto'];
|
|
}
|
|
|
|
$map = $locationMap[$locationKey];
|
|
$group = $settings[$map['group']] ?? [];
|
|
|
|
return [
|
|
'enabled' => $group[$map['enabled']] ?? false,
|
|
'format' => $group[$map['format']] ?? 'auto',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Genera HTML del bloque de anuncio
|
|
*/
|
|
private function buildAdHTML(array $settings, string $format, string $location, string $visClasses): string
|
|
{
|
|
$publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
|
|
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
|
|
|
if (empty($publisherId)) {
|
|
return '';
|
|
}
|
|
|
|
// Obtener slot segun formato
|
|
$slotId = $this->getSlotByFormat($settings, $format);
|
|
if (empty($slotId)) {
|
|
return '';
|
|
}
|
|
|
|
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
|
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
|
$locationClass = 'roi-ad-' . esc_attr(str_replace('_', '-', $location));
|
|
|
|
return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr);
|
|
}
|
|
|
|
/**
|
|
* Obtiene el slot ID segun el formato
|
|
*/
|
|
private function getSlotByFormat(array $settings, string $format): string
|
|
{
|
|
$content = $settings['content'] ?? [];
|
|
|
|
return match($format) {
|
|
'display', 'display-large', 'display-square' => $content['slot_display'] ?? '',
|
|
'in-article' => $content['slot_inarticle'] ?? '',
|
|
'autorelaxed' => $content['slot_autorelaxed'] ?? '',
|
|
default => $content['slot_auto'] ?? '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Genera el markup HTML segun formato de anuncio
|
|
*
|
|
* EXCEPCION DOCUMENTADA: CSS inline requerido por Google AdSense
|
|
* ----------------------------------------------------------------
|
|
* Los atributos style="display:inline-block", style="display:block",
|
|
* style="text-align:center", etc. son ESPECIFICACION DE GOOGLE y NO
|
|
* pueden generarse via CSSGenerator.
|
|
*
|
|
* Documentacion oficial:
|
|
* - https://support.google.com/adsense/answer/9274516
|
|
* - https://support.google.com/adsense/answer/9183460
|
|
*
|
|
* Estos estilos son necesarios para que AdSense funcione correctamente
|
|
* y son inyectados tal como Google los especifica en su documentacion.
|
|
*/
|
|
private function generateAdMarkup(
|
|
string $format,
|
|
string $client,
|
|
string $slot,
|
|
string $locationClass,
|
|
string $visClasses,
|
|
string $scriptType,
|
|
string $dataAttr
|
|
): 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),
|
|
};
|
|
}
|
|
|
|
private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a): string
|
|
{
|
|
return sprintf(
|
|
'<div class="roi-ad-slot %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
|
|
);
|
|
}
|
|
|
|
private function adAuto(string $c, string $s, string $cl, string $t, string $a): string
|
|
{
|
|
return sprintf(
|
|
'<div class="roi-ad-slot %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
|
|
);
|
|
}
|
|
|
|
private function adInArticle(string $c, string $s, string $cl, string $t, string $a): string
|
|
{
|
|
return sprintf(
|
|
'<div class="roi-ad-slot %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
|
|
);
|
|
}
|
|
|
|
private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a): string
|
|
{
|
|
return sprintf(
|
|
'<div class="roi-ad-slot %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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renderiza Rail Ads (anuncios fijos en margenes laterales)
|
|
* Se inyectan via wp_footer y usan position: fixed
|
|
*/
|
|
public function renderRailAds(array $settings): string
|
|
{
|
|
// Verificar si Rail Ads estan habilitados
|
|
if (!($settings['visibility']['is_enabled'] ?? false)) {
|
|
return '';
|
|
}
|
|
if (!($settings['behavior']['rail_ads_enabled'] ?? false)) {
|
|
return '';
|
|
}
|
|
|
|
$publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
|
|
$slotId = $settings['content']['slot_skyscraper'] ?? '';
|
|
|
|
if (empty($publisherId) || empty($slotId)) {
|
|
return '';
|
|
}
|
|
|
|
$leftEnabled = ($settings['behavior']['rail_left_enabled'] ?? true) === true;
|
|
$rightEnabled = ($settings['behavior']['rail_right_enabled'] ?? true) === true;
|
|
$format = $settings['behavior']['rail_format'] ?? 'skyscraper';
|
|
$topOffset = (int)($settings['behavior']['rail_top_offset'] ?? 300);
|
|
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
|
|
|
// Altura del rail segun formato seleccionado
|
|
// El ancho es responsive (se ajusta automaticamente al espacio disponible)
|
|
$height = match($format) {
|
|
'h250' => 250,
|
|
'h300' => 300,
|
|
'h400' => 400,
|
|
'h500' => 500,
|
|
'h600' => 600,
|
|
'h700' => 700,
|
|
'h800' => 800,
|
|
'h1050' => 1050,
|
|
// Legacy keys para compatibilidad con valores anteriores
|
|
'skyscraper', 'slim-large', 'w130-h600', 'w140-h600', 'w150-h600' => 600,
|
|
'slim-small', 'w130-h300', 'w140-h300', 'w150-h300' => 300,
|
|
'slim-medium', 'w130-h400', 'w140-h400', 'w150-h400' => 400,
|
|
'slim-xlarge' => 700,
|
|
'wide-skyscraper' => 800,
|
|
'w300-h250' => 250,
|
|
'half-page' => 600,
|
|
'large-skyscraper' => 1050,
|
|
default => 600,
|
|
};
|
|
|
|
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
|
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
|
|
|
// === CSS via CSSGenerator (NO hardcodeado) ===
|
|
$cssRules = [];
|
|
|
|
// Estilos base para Rail Ads - RESPONSIVE
|
|
// El ancho se calcula automaticamente para llenar el espacio disponible
|
|
// Formula: (viewport - container) / 2 - margen_exterior(10px) - gap_interior(10px)
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad', [
|
|
'position' => 'fixed',
|
|
'top' => $topOffset . 'px',
|
|
'width' => 'calc((100vw - var(--roi-container-width-numeric, 1320px)) / 2 - 20px)',
|
|
'height' => $height . 'px',
|
|
'display' => 'flex',
|
|
'align-items' => 'flex-start',
|
|
'z-index' => '100',
|
|
'transition' => 'top 0.2s ease-out, opacity 0.3s ease-out',
|
|
]);
|
|
|
|
// Rail izquierdo
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-left', [
|
|
'left' => '0px',
|
|
'justify-content' => 'flex-end',
|
|
'padding-left' => '5px',
|
|
]);
|
|
|
|
// Rail derecho
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-right', [
|
|
'right' => '10px',
|
|
'justify-content' => 'flex-start',
|
|
'padding-right' => '5px',
|
|
]);
|
|
|
|
// Asegurar que el anuncio no desborde el container
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad ins.adsbygoogle', [
|
|
'max-width' => '100%',
|
|
]);
|
|
|
|
// Media query para ocultar en pantallas donde no hay espacio suficiente
|
|
// Mostrar a partir de 1400px para dar mas flexibilidad
|
|
$cssRules[] = "@media (max-width: 1399px) {
|
|
.roi-rail-ad { display: none !important; }
|
|
}";
|
|
|
|
$css = implode("\n", $cssRules);
|
|
|
|
// HTML primero (CSS), JavaScript se añade AL FINAL
|
|
$html = "<style>{$css}</style>\n";
|
|
|
|
/**
|
|
* EXCEPCION DOCUMENTADA: CSS inline requerido por Google AdSense
|
|
* El anuncio usa tamaño fijo (160xHeight) centrado en el container responsive.
|
|
* Ref: https://support.google.com/adsense/answer/9274516
|
|
*/
|
|
|
|
// Rail izquierdo
|
|
if ($leftEnabled) {
|
|
$html .= sprintf(
|
|
'<div class="roi-rail-ad roi-rail-ad-left">
|
|
<ins class="adsbygoogle" style="display:inline-block;width:160px;height:%dpx"
|
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
|
</div>',
|
|
$height, esc_attr($publisherId), esc_attr($slotId), $scriptType, $dataAttr
|
|
);
|
|
}
|
|
|
|
// Rail derecho
|
|
if ($rightEnabled) {
|
|
$html .= sprintf(
|
|
'<div class="roi-rail-ad roi-rail-ad-right">
|
|
<ins class="adsbygoogle" style="display:inline-block;width:160px;height:%dpx"
|
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
|
</div>',
|
|
$height, esc_attr($publisherId), esc_attr($slotId), $scriptType, $dataAttr
|
|
);
|
|
}
|
|
|
|
// JavaScript para posicionamiento inteligente de Rail Ads
|
|
// IMPORTANTE: Se añade DESPUES de los divs para garantizar que existan en el DOM
|
|
$html .= "
|
|
<script id=\"roi-rail-ads-positioning\">
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Configuracion
|
|
var CONFIG = {
|
|
defaultTop: {$topOffset},
|
|
gap: 30,
|
|
maxTopPercent: 0.45,
|
|
retryDelay: 100,
|
|
maxRetries: 10
|
|
};
|
|
|
|
var retryCount = 0;
|
|
|
|
function initRailAds() {
|
|
var railAds = document.querySelectorAll('.roi-rail-ad');
|
|
|
|
// Si no hay rails, reintentar (puede que el DOM no este listo)
|
|
if (!railAds.length) {
|
|
if (retryCount < CONFIG.maxRetries) {
|
|
retryCount++;
|
|
setTimeout(initRailAds, CONFIG.retryDelay);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Selectores especificos del tema ROI
|
|
var navbar = document.querySelector('nav.navbar');
|
|
var hero = document.querySelector('.hero-section, .roi-hero, [class*=\"hero\"]');
|
|
var featuredImage = document.querySelector('.featured-image-container, .post-thumbnail, .entry-image');
|
|
var footer = document.querySelector('footer, .site-footer, #footer, [role=\"contentinfo\"]');
|
|
|
|
function getNavbarHeight() {
|
|
if (navbar) {
|
|
var style = window.getComputedStyle(navbar);
|
|
if (style.position === 'fixed' || style.position === 'sticky') {
|
|
return navbar.offsetHeight || 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function adjustRailPosition() {
|
|
var navHeight = getNavbarHeight();
|
|
var newTop = CONFIG.defaultTop;
|
|
var shouldHide = false;
|
|
|
|
// Detectar si el footer esta visible y los rails lo tocarian
|
|
if (footer) {
|
|
var footerRect = footer.getBoundingClientRect();
|
|
var railHeight = railAds[0] ? railAds[0].offsetHeight : {$height};
|
|
|
|
// Si el footer esta en la pantalla y el rail lo tocaria, ocultar
|
|
if (footerRect.top < window.innerHeight && footerRect.top < (newTop + railHeight + 50)) {
|
|
shouldHide = true;
|
|
}
|
|
}
|
|
|
|
// Buscar el elemento mas bajo visible en pantalla
|
|
var elementsToCheck = [hero, featuredImage].filter(Boolean);
|
|
|
|
if (elementsToCheck.length > 0) {
|
|
var lowestBottom = 0;
|
|
|
|
elementsToCheck.forEach(function(el) {
|
|
if (el) {
|
|
var rect = el.getBoundingClientRect();
|
|
// Solo considerar si el elemento esta visible (bottom > 0)
|
|
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
|
lowestBottom = Math.max(lowestBottom, rect.bottom);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Si hay un elemento visible, posicionar debajo de el
|
|
if (lowestBottom > navHeight) {
|
|
newTop = lowestBottom + CONFIG.gap;
|
|
} else {
|
|
// Si no hay hero visible, usar navbar + gap o default
|
|
newTop = Math.max(navHeight + CONFIG.gap, CONFIG.defaultTop);
|
|
}
|
|
}
|
|
|
|
// Limitar el top maximo
|
|
var maxTop = window.innerHeight * CONFIG.maxTopPercent;
|
|
newTop = Math.min(newTop, maxTop);
|
|
|
|
// Asegurar minimo respetando navbar
|
|
newTop = Math.max(newTop, navHeight + CONFIG.gap);
|
|
|
|
// Re-verificar colision con footer con la nueva posicion
|
|
if (footer && !shouldHide) {
|
|
var footerRect = footer.getBoundingClientRect();
|
|
var railHeight = railAds[0] ? railAds[0].offsetHeight : {$height};
|
|
if (footerRect.top < (newTop + railHeight + 30)) {
|
|
shouldHide = true;
|
|
}
|
|
}
|
|
|
|
// Aplicar posicion y visibilidad a todos los rails
|
|
for (var i = 0; i < railAds.length; i++) {
|
|
railAds[i].style.top = Math.round(newTop) + 'px';
|
|
railAds[i].style.opacity = shouldHide ? '0' : '1';
|
|
railAds[i].style.pointerEvents = shouldHide ? 'none' : 'auto';
|
|
}
|
|
}
|
|
|
|
// Throttle para scroll
|
|
var ticking = false;
|
|
function onScroll() {
|
|
if (!ticking) {
|
|
requestAnimationFrame(function() {
|
|
adjustRailPosition();
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
window.addEventListener('resize', function() {
|
|
adjustRailPosition();
|
|
}, { passive: true });
|
|
|
|
// Ejecutar inmediatamente
|
|
adjustRailPosition();
|
|
|
|
// Ejecutar varias veces para asegurar que las imagenes cargaron
|
|
setTimeout(adjustRailPosition, 200);
|
|
setTimeout(adjustRailPosition, 500);
|
|
setTimeout(adjustRailPosition, 1000);
|
|
|
|
// Tambien cuando las imagenes cargan
|
|
window.addEventListener('load', adjustRailPosition);
|
|
}
|
|
|
|
// Iniciar
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initRailAds);
|
|
} else {
|
|
initRailAds();
|
|
}
|
|
})();
|
|
</script>";
|
|
|
|
return $html;
|
|
}
|
|
}
|