- Changed Rail Ads positioning from container-width-based formula to fixed 15px from viewport edges - Rails no longer move inward when container width is reduced - Fixes overlap issue when layout width setting changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
507 lines
19 KiB
PHP
507 lines
19 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;
|
|
|
|
// Dimensiones segun formato
|
|
// Opciones de 160px de ancho con diferentes alturas + opciones anchas
|
|
[$width, $height] = match($format) {
|
|
'slim-small' => [160, 300],
|
|
'slim-medium' => [160, 400],
|
|
'slim-large' => [160, 500],
|
|
'slim-xlarge' => [160, 700],
|
|
'wide-skyscraper' => [160, 800],
|
|
'half-page' => [300, 600],
|
|
'large-skyscraper' => [300, 1050],
|
|
default => [160, 600], // skyscraper
|
|
};
|
|
|
|
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
|
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
|
|
|
// === CSS via CSSGenerator (NO hardcodeado) ===
|
|
$cssRules = [];
|
|
|
|
// Estilos base para Rail Ads
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad', [
|
|
'position' => 'fixed',
|
|
'top' => $topOffset . 'px',
|
|
'width' => $width . 'px',
|
|
'z-index' => '100',
|
|
'transition' => 'top 0.2s ease-out',
|
|
]);
|
|
|
|
// Posicion rail izquierdo - FIJO en el borde del viewport
|
|
// Los Rails NO dependen del container width, siempre estan a 15px del borde
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-left', [
|
|
'left' => '15px',
|
|
]);
|
|
|
|
// Posicion rail derecho - FIJO en el borde del viewport
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-right', [
|
|
'right' => '15px',
|
|
]);
|
|
|
|
// Media query para ocultar en pantallas < 1600px
|
|
// NOTA: Media queries se escriben directamente (patron consistente con FeaturedImageRenderer)
|
|
$cssRules[] = "@media (max-width: 1599px) {
|
|
.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
|
|
* Los atributos style="display:inline-block;width:Xpx;height:Xpx" son
|
|
* especificacion de Google y NO pueden generarse via CSSGenerator.
|
|
* 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:%dpx;height:%dpx"
|
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
|
</div>',
|
|
$width, $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:%dpx;height:%dpx"
|
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
|
</div>',
|
|
$width, $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');
|
|
|
|
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;
|
|
|
|
// 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);
|
|
|
|
// Aplicar posicion a todos los rails
|
|
for (var i = 0; i < railAds.length; i++) {
|
|
railAds[i].style.top = Math.round(newTop) + 'px';
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|