- Usar CSS max() para evitar rail izquierdo cortado fuera del viewport - Agregar JavaScript inteligente para detectar navbar/hero dinámicamente - Rail ads se posicionan debajo del hero cuando es visible - Usar requestAnimationFrame para throttle de scroll - Eliminar dependencia de valores fijos en pixels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
444 lines
17 KiB
PHP
444 lines
17 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'] ?? 150);
|
|
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
|
|
|
// Dimensiones segun formato
|
|
$width = $format === 'half-page' ? 300 : 160;
|
|
$height = 600;
|
|
|
|
$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 - usar max() para evitar valores negativos
|
|
// Formula: max(10px, calc((100vw - 1320px) / 2 - (width + 20)px))
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-left', [
|
|
'left' => 'max(10px, calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px))',
|
|
]);
|
|
|
|
// Posicion rail derecho - usar max() para consistencia
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-right', [
|
|
'right' => 'max(10px, calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px))',
|
|
]);
|
|
|
|
// 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);
|
|
|
|
// JavaScript para posicionamiento inteligente de Rail Ads
|
|
// Detecta dinamicamente el header/hero y ajusta posicion sin valores fijos
|
|
$js = "
|
|
<script>
|
|
(function() {
|
|
var railAds = document.querySelectorAll('.roi-rail-ad');
|
|
if (!railAds.length) return;
|
|
|
|
// Buscar elementos de referencia (en orden de prioridad)
|
|
var navbar = document.querySelector('.navbar-fixed-top, .roi-navbar, .site-header, nav.navbar');
|
|
var hero = document.querySelector('.roi-hero, .hero-section, .hero, [class*=\"hero\"]');
|
|
var mainContent = document.querySelector('main, .site-main, .main-content, article');
|
|
|
|
function getNavbarHeight() {
|
|
if (navbar) {
|
|
var style = window.getComputedStyle(navbar);
|
|
if (style.position === 'fixed' || style.position === 'sticky') {
|
|
return navbar.offsetHeight;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function adjustRailPosition() {
|
|
var navHeight = getNavbarHeight();
|
|
var gap = 20; // Espacio minimo de separacion
|
|
var newTop = navHeight + gap;
|
|
|
|
// Si hay hero visible, posicionar debajo de el
|
|
if (hero) {
|
|
var heroRect = hero.getBoundingClientRect();
|
|
// Si el bottom del hero esta visible (> navbar height), rails debajo del hero
|
|
if (heroRect.bottom > navHeight) {
|
|
newTop = heroRect.bottom + gap;
|
|
}
|
|
}
|
|
|
|
// Limitar el top maximo para que no quede muy abajo
|
|
var maxTop = window.innerHeight * 0.25; // Max 25% del viewport
|
|
newTop = Math.min(newTop, maxTop);
|
|
|
|
// Asegurar minimo respetando navbar
|
|
newTop = Math.max(newTop, navHeight + gap);
|
|
|
|
railAds.forEach(function(rail) {
|
|
rail.style.top = newTop + 'px';
|
|
});
|
|
}
|
|
|
|
// Throttle para mejor rendimiento
|
|
var ticking = false;
|
|
function onScroll() {
|
|
if (!ticking) {
|
|
requestAnimationFrame(function() {
|
|
adjustRailPosition();
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
window.addEventListener('resize', adjustRailPosition, { passive: true });
|
|
|
|
// Ejecutar despues de que el DOM este listo
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', adjustRailPosition);
|
|
} else {
|
|
adjustRailPosition();
|
|
}
|
|
})();
|
|
</script>";
|
|
|
|
$html = "<style>{$css}</style>\n{$js}\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
|
|
);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
}
|