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 "\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( '
', 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( '
', 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( '
', 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( '
', 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'; // Agregar 15px de margen para evitar solapamiento con hero $topOffset = (int)($settings['behavior']['rail_top_offset'] ?? 300) + 15; $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' => '0px', '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 = "\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( '
', $height, esc_attr($publisherId), esc_attr($slotId), $scriptType, $dataAttr ); } // Rail derecho if ($rightEnabled) { $html .= sprintf( '
', $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 .= " "; return $html; } /** * Renderiza Anchor Ads (anuncios fijos en top/bottom) * * @param array $settings Configuracion desde BD * @return string HTML de los anchor ads */ public function renderAnchorAds(array $settings): string { // Verificar si Anchor Ads estan habilitados if (!($settings['visibility']['is_enabled'] ?? false)) { return ''; } if (!($settings['anchor_ads']['anchor_enabled'] ?? false)) { return ''; } $publisherId = esc_attr($settings['content']['publisher_id'] ?? ''); $slotId = $settings['content']['slot_auto'] ?? ''; if (empty($publisherId) || empty($slotId)) { return ''; } // Configuracion de anchor $anchorConfig = $settings['anchor_ads'] ?? []; $position = $anchorConfig['anchor_position'] ?? 'bottom'; $height = (int)($anchorConfig['anchor_height'] ?? 90); $showMobile = ($anchorConfig['anchor_show_on_mobile'] ?? true) === true; $showWide = ($anchorConfig['anchor_show_on_wide_screens'] ?? false) === true; $collapsible = ($anchorConfig['anchor_collapsible_enabled'] ?? true) === true; $collapsedHeight = (int)($anchorConfig['anchor_collapsed_height'] ?? 24); $collapseText = esc_html($anchorConfig['anchor_collapse_button_text'] ?? 'Ver anuncio'); $closePosition = $anchorConfig['anchor_close_position'] ?? 'right'; $rememberState = ($anchorConfig['anchor_remember_state'] ?? true) === true; $rememberDuration = $anchorConfig['anchor_remember_duration'] ?? 'session'; $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true; $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript'; $dataAttr = $delayEnabled ? ' data-adsense-push' : ''; // === CSS via CSSGenerator === $cssRules = []; // Base anchor styles - FUERA DE PANTALLA por defecto (NO usar visibility:hidden) // Usamos transform para mover fuera de la pantalla, asi AdSense puede medir el slot $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad', [ 'position' => 'fixed', 'left' => '0', 'right' => '0', 'z-index' => '9999', 'background' => '#f8f9fa', 'border-color' => '#dee2e6', 'box-shadow' => '0 -2px 10px rgba(0,0,0,0.1)', 'transition' => 'height 0.3s ease, transform 0.3s ease', 'display' => 'flex', 'align-items' => 'center', 'justify-content' => 'center', ]); // Anchor TOP: fuera de pantalla hacia arriba, visible cuando se carga $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad-top', [ 'top' => '0', 'border-bottom-width' => '1px', 'border-bottom-style' => 'solid', 'transform' => 'translateY(-100%)', ]); // Anchor BOTTOM: fuera de pantalla hacia abajo, visible cuando se carga $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad-bottom', [ 'bottom' => '0', 'border-top-width' => '1px', 'border-top-style' => 'solid', 'transform' => 'translateY(100%)', ]); // Mostrar anchor cuando AdSense llena el slot (desliza a posicion visible) $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.ad-loaded', [ 'transform' => 'translateY(0)', ]); // Controls container $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls', [ 'position' => 'absolute', 'top' => '4px', 'display' => 'flex', 'gap' => '4px', 'z-index' => '10', ]); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-left', ['left' => '8px']); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-right', ['right' => '8px']); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-center', ['left' => '50%', 'transform' => 'translateX(-50%)']); // CRITICAL: El contenedor del anuncio DEBE tener width 100% para que AdSense pueda calcular el tamaño $cssRules[] = $this->cssGenerator->generate('.roi-anchor-content', [ 'width' => '100%', 'display' => 'flex', 'align-items' => 'center', 'justify-content' => 'center', ]); // Buttons $cssRules[] = $this->cssGenerator->generate('.roi-anchor-btn', [ 'width' => '28px', 'height' => '28px', 'border' => 'none', 'border-radius' => '4px', 'background' => 'rgba(0,0,0,0.1)', 'color' => '#333', 'cursor' => 'pointer', 'font-size' => '14px', 'display' => 'flex', 'align-items' => 'center', 'justify-content' => 'center', 'transition' => 'background 0.2s', ]); // Collapsed state $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed', [ 'height' => $collapsedHeight . 'px !important', ]); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed .roi-anchor-content', [ 'display' => 'none', ]); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-expand-bar', [ 'display' => 'none', 'width' => '100%', 'height' => '100%', 'background' => '#e9ecef', 'border' => 'none', 'cursor' => 'pointer', 'font-size' => '12px', 'color' => '#495057', ]); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed .roi-anchor-expand-bar', [ 'display' => 'flex', 'align-items' => 'center', 'justify-content' => 'center', ]); $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.hidden', [ 'display' => 'none !important', ]); // Media query para visibilidad if (!$showMobile && $showWide) { $cssRules[] = "@media (max-width: 999px) { .roi-anchor-ad { display: none !important; } }"; } elseif ($showMobile && !$showWide) { $cssRules[] = "@media (min-width: 1000px) { .roi-anchor-ad { display: none !important; } }"; } elseif (!$showMobile && !$showWide) { $cssRules[] = ".roi-anchor-ad { display: none !important; }"; } $css = implode("\n", $cssRules); $html = "\n"; // Renderizar anchor(s) $controlsClass = 'roi-anchor-controls roi-anchor-controls-' . esc_attr($closePosition); if ($position === 'top' || $position === 'both') { $html .= $this->buildAnchorHTML('top', $height, $publisherId, $slotId, $scriptType, $dataAttr, $controlsClass, $collapsible, $collapseText); } if ($position === 'bottom' || $position === 'both') { $html .= $this->buildAnchorHTML('bottom', $height, $publisherId, $slotId, $scriptType, $dataAttr, $controlsClass, $collapsible, $collapseText); } // Config para JavaScript (data attributes en lugar de inline JS) $jsConfig = json_encode([ 'rememberState' => $rememberState, 'rememberDuration' => $rememberDuration, 'collapsible' => $collapsible, ]); $html .= ''; return $html; } /** * Genera HTML para un anchor individual */ private function buildAnchorHTML( string $pos, int $height, string $client, string $slot, string $scriptType, string $dataAttr, string $controlsClass, bool $collapsible, string $collapseText ): string { $posClass = 'roi-anchor-ad-' . $pos; $collapseBtn = $collapsible ? '' : ''; return sprintf( '
%s
', esc_attr($posClass), esc_attr($pos), $height, esc_attr($controlsClass), $collapseBtn, $height - 10, esc_attr($client), esc_attr($slot), $scriptType, $dataAttr, esc_html($collapseText) ); } /** * Renderiza Vignette Ad (pantalla completa) * * @param array $settings Configuracion desde BD * @return string HTML del vignette ad */ public function renderVignetteAd(array $settings): string { // Verificar si Vignette Ads estan habilitados if (!($settings['visibility']['is_enabled'] ?? false)) { return ''; } if (!($settings['vignette_ads']['vignette_enabled'] ?? false)) { return ''; } $publisherId = esc_attr($settings['content']['publisher_id'] ?? ''); $slotId = $settings['content']['slot_display'] ?? $settings['content']['slot_auto'] ?? ''; if (empty($publisherId) || empty($slotId)) { return ''; } // Configuracion de vignette $vignetteConfig = $settings['vignette_ads'] ?? []; $trigger = $vignetteConfig['vignette_trigger'] ?? 'pageview'; $triggerDelay = (int)($vignetteConfig['vignette_trigger_delay'] ?? 5); $showMobile = ($vignetteConfig['vignette_show_on_mobile'] ?? true) === true; $showDesktop = ($vignetteConfig['vignette_show_on_desktop'] ?? true) === true; $size = $vignetteConfig['vignette_size'] ?? '300x250'; $overlayOpacity = (float)($vignetteConfig['vignette_overlay_opacity'] ?? 0.7); $closeDelay = (int)($vignetteConfig['vignette_close_button_delay'] ?? 0); $reshowEnabled = ($vignetteConfig['vignette_reshow_enabled'] ?? true) === true; $reshowTime = (int)($vignetteConfig['vignette_reshow_time'] ?? 5); $maxPerSession = $vignetteConfig['vignette_max_per_session'] ?? '3'; $maxPerPage = $vignetteConfig['vignette_max_per_page'] ?? '1'; // Calcular dimensiones list($adWidth, $adHeight) = $this->parseVignetteSize($size); $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true; $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript'; $dataAttr = $delayEnabled ? ' data-adsense-push' : ''; // === CSS via CSSGenerator === $cssRules = []; $cssRules[] = $this->cssGenerator->generate('.roi-vignette-overlay', [ 'position' => 'fixed', 'top' => '0', 'left' => '0', 'right' => '0', 'bottom' => '0', 'background' => 'rgba(0,0,0,' . $overlayOpacity . ')', 'z-index' => '99999', 'display' => 'none', 'align-items' => 'center', 'justify-content' => 'center', 'opacity' => '0', 'transition' => 'opacity 0.3s ease', ]); $cssRules[] = $this->cssGenerator->generate('.roi-vignette-overlay.active', [ 'display' => 'flex', 'opacity' => '1', ]); $cssRules[] = $this->cssGenerator->generate('.roi-vignette-modal', [ 'position' => 'relative', 'background' => '#fff', 'border-radius' => '8px', 'padding' => '20px', 'box-shadow' => '0 10px 40px rgba(0,0,0,0.3)', 'max-width' => '95vw', 'max-height' => '95vh', 'overflow' => 'auto', ]); $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close', [ 'position' => 'absolute', 'top' => '-12px', 'right' => '-12px', 'width' => '32px', 'height' => '32px', 'border' => 'none', 'border-radius' => '50%', 'background' => '#dc3545', 'color' => '#fff', 'cursor' => 'pointer', 'font-size' => '18px', 'display' => 'flex', 'align-items' => 'center', 'justify-content' => 'center', 'box-shadow' => '0 2px 8px rgba(0,0,0,0.2)', 'transition' => 'transform 0.2s, opacity 0.3s', ]); $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close.delayed', [ 'opacity' => '0', 'pointer-events' => 'none', ]); $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close:not(.delayed)', [ 'opacity' => '1', 'pointer-events' => 'auto', ]); // Media query para visibilidad if (!$showMobile && $showDesktop) { $cssRules[] = "@media (max-width: 991px) { .roi-vignette-overlay { display: none !important; } }"; } elseif ($showMobile && !$showDesktop) { $cssRules[] = "@media (min-width: 992px) { .roi-vignette-overlay { display: none !important; } }"; } elseif (!$showMobile && !$showDesktop) { $cssRules[] = ".roi-vignette-overlay { display: none !important; }"; } $css = implode("\n", $cssRules); $html = "\n"; // Determinar estilo de anuncio segun tamano if ($size === 'auto' || $size === 'responsive') { $adStyle = 'display:block;min-width:300px;min-height:250px'; $adFormat = ' data-ad-format="auto" data-full-width-responsive="true"'; } else { $adStyle = sprintf('display:inline-block;width:%dpx;height:%dpx', $adWidth, $adHeight); $adFormat = ''; } // Sin boton de cerrar - el usuario cierra haciendo clic fuera del modal $html .= sprintf( '
', $adStyle, esc_attr($publisherId), esc_attr($slotId), $adFormat, $scriptType, $dataAttr ); // Config para JavaScript $jsConfig = json_encode([ 'trigger' => $trigger, 'triggerDelay' => $triggerDelay, 'closeDelay' => $closeDelay, 'reshowEnabled' => $reshowEnabled, 'reshowTime' => $reshowTime, 'maxPerSession' => $maxPerSession, 'maxPerPage' => $maxPerPage, ]); $html .= ''; return $html; } /** * Parsea el tamano del vignette */ private function parseVignetteSize(string $size): array { return match($size) { '1280x720' => [1280, 720], '960x540' => [960, 540], '854x480' => [854, 480], '800x450' => [800, 450], '640x360' => [640, 360], '560x315' => [560, 315], '336x280' => [336, 280], '300x250' => [300, 250], 'auto', 'responsive' => [0, 0], // Tamaños dinamicos default => [960, 540], // Default a tamaño video qHD }; } }