fix(wrappers): eliminar wrappers vacíos y corregir exclusiones AdSense (Plan 99.15)

## Problema
- Componentes deshabilitados/excluidos dejaban wrappers HTML vacíos
  (navbar 32px, sidebar col-lg-3 294px)
- AdSense ignoraba exclusiones por URL pattern en grupo _exclusions

## Solución Plan 99.15 (Clean Architecture)

### Domain Layer
- WrapperVisibilityCheckerInterface: contrato para verificar visibilidad

### Application Layer
- CheckWrapperVisibilityUseCase: orquesta verificaciones de visibilidad

### Infrastructure Layer
- WordPressComponentVisibilityRepository: consulta BD + PageVisibilityHelper
- WrapperVisibilityService: facade estático para templates
- BodyClassHooksRegistrar: agrega clases CSS failsafe al body

### Templates modificados
- header.php: renderizado condicional de <nav> wrapper
- page.php/single.php: lógica dinámica col-lg-9/col-lg-12 según sidebar

### CSS Failsafe
- css-global-utilities.css: reglas body.roi-hide-* como respaldo

## Fix AdSense (Inc/adsense-placement.php)
- Agregado PageVisibilityHelper::shouldShow() a todas las funciones:
  roi_render_ad_slot, roi_render_rail_ads, roi_enqueue_adsense_script,
  roi_inject_content_ads, roi_render_anchor_ads, roi_render_vignette_ad,
  roi_enqueue_anchor_vignette_scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-12-04 11:46:21 -06:00
parent 23339e3349
commit 36d5cf56de
12 changed files with 617 additions and 12 deletions

View File

@@ -88,3 +88,43 @@
.transition-none {
transition: none !important;
}
/* ========================================
COMPONENT VISIBILITY FAILSAFE (Plan 99.15)
CSS failsafe: Oculta wrappers de componentes
cuando body tiene clases roi-hide-*
Estas clases se agregan via BodyClassHooksRegistrar
cuando los componentes están deshabilitados/excluidos.
======================================== */
/* Navbar hidden */
body.roi-hide-navbar .navbar {
display: none !important;
}
/* Table of Contents hidden */
body.roi-hide-toc .roi-toc-container {
display: none !important;
}
/* CTA Sidebar hidden */
body.roi-hide-cta-sidebar .roi-cta-box {
display: none !important;
}
/* Generic sidebar hidden */
body.roi-hide-sidebar .sidebar-sticky {
display: none !important;
}
/* When ALL sidebar components are hidden, expand main column */
body.roi-sidebar-empty .col-lg-9 {
flex: 0 0 100% !important;
max-width: 100% !important;
}
body.roi-sidebar-empty .col-lg-3 {
display: none !important;
}

View File

@@ -25,6 +25,8 @@ if (!defined('ABSPATH')) {
exit;
}
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* Renderiza un slot de anuncio en una ubicacion
*
@@ -52,11 +54,16 @@ function roi_render_ad_slot(string $location): string
return '';
}
// Verificar exclusiones
// Verificar exclusiones legacy (forms group)
if (roi_is_ad_excluded($settings)) {
return '';
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
// Obtener renderer desde DIContainer (DIP compliant)
$renderer = $container->getAdsensePlacementRenderer();
@@ -143,11 +150,16 @@ function roi_render_rail_ads(): string
return '';
}
// Verificar exclusiones
// Verificar exclusiones legacy (forms group)
if (roi_is_ad_excluded($settings)) {
return '';
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
// Obtener renderer desde DIContainer (DIP compliant)
$renderer = $container->getAdsensePlacementRenderer();
@@ -193,6 +205,11 @@ function roi_enqueue_adsense_script(): void
return;
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return;
}
$publisherId = $settings['content']['publisher_id'] ?? '';
if (empty($publisherId)) {
return;
@@ -246,11 +263,16 @@ function roi_inject_content_ads(string $content): string
return $content;
}
// Verificar exclusiones
// Verificar exclusiones legacy (forms group)
if (roi_is_ad_excluded($settings)) {
return $content;
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return $content;
}
$renderer = $container->getAdsensePlacementRenderer();
// Inyectar anuncio al inicio (post-top)
@@ -446,11 +468,16 @@ function roi_render_anchor_ads(): string
return '';
}
// Verificar exclusiones
// Verificar exclusiones legacy (forms group)
if (roi_is_ad_excluded($settings)) {
return '';
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
// Obtener renderer desde DIContainer (DIP compliant)
$renderer = $container->getAdsensePlacementRenderer();
@@ -490,11 +517,16 @@ function roi_render_vignette_ad(): string
return '';
}
// Verificar exclusiones
// Verificar exclusiones legacy (forms group)
if (roi_is_ad_excluded($settings)) {
return '';
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
// Obtener renderer desde DIContainer (DIP compliant)
$renderer = $container->getAdsensePlacementRenderer();
@@ -556,6 +588,11 @@ function roi_enqueue_anchor_vignette_scripts(): void
return;
}
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return;
}
// Encolar script
wp_enqueue_script(
'roi-anchor-vignette',

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases;
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
/**
* UseCase: Verificar si un wrapper de componente debe renderizarse
*
* Responsabilidad: Orquestar la lógica de verificación de visibilidad
* combinando múltiples criterios:
* 1. Componente habilitado (is_enabled)
* 2. Visible en dispositivo actual (show_on_mobile/desktop)
* 3. No excluido por reglas (categoría, post ID, URL, page visibility)
*
* @package ROITheme\Shared\Application\UseCases
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
final class CheckWrapperVisibilityUseCase
{
public function __construct(
private readonly WrapperVisibilityCheckerInterface $visibilityChecker
) {}
/**
* Ejecuta la verificación de visibilidad del wrapper
*
* @param string $componentName Nombre del componente (kebab-case)
* @param bool $isMobile True si es dispositivo móvil
* @return bool True si el wrapper debe renderizarse
*/
public function execute(string $componentName, bool $isMobile): bool
{
// Criterio 1: Debe estar habilitado
if (!$this->visibilityChecker->isEnabled($componentName)) {
return false;
}
// Criterio 2: Debe ser visible en el dispositivo actual
if (!$this->visibilityChecker->isVisibleOnDevice($componentName, $isMobile)) {
return false;
}
// Criterio 3: No debe estar excluido
if (!$this->visibilityChecker->isNotExcluded($componentName)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Contracts;
/**
* Interface para verificar visibilidad de wrappers de componentes
*
* Responsabilidad: Definir contrato para determinar si un wrapper
* de componente debe renderizarse basándose en:
* - Estado habilitado/deshabilitado
* - Visibilidad por dispositivo
* - Reglas de exclusión (categoría, post ID, URL pattern, page visibility)
*
* @package ROITheme\Shared\Domain\Contracts
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
interface WrapperVisibilityCheckerInterface
{
/**
* Verifica si el componente está habilitado globalmente
*
* @param string $componentName Nombre del componente (kebab-case)
* @return bool True si is_enabled = true en BD
*/
public function isEnabled(string $componentName): bool;
/**
* Verifica si el componente es visible en el dispositivo actual
*
* @param string $componentName Nombre del componente (kebab-case)
* @param bool $isMobile True si es dispositivo móvil
* @return bool True si show_on_mobile/show_on_desktop según corresponda
*/
public function isVisibleOnDevice(string $componentName, bool $isMobile): bool;
/**
* Verifica si el componente NO está excluido para la página actual
*
* Evalúa todas las reglas de exclusión:
* - Exclusión por categoría
* - Exclusión por post ID
* - Exclusión por URL pattern
* - Page visibility (home, posts, pages, archives, search)
*
* @param string $componentName Nombre del componente (kebab-case)
* @return bool True si el componente NO está excluido
*/
public function isNotExcluded(string $componentName): bool;
}

View File

@@ -37,6 +37,11 @@ use ROITheme\Shared\Infrastructure\Services\WordPressPageContextProvider;
use ROITheme\Shared\Infrastructure\Services\WordPressServerRequestProvider;
use ROITheme\Shared\Application\UseCases\EvaluateExclusions\EvaluateExclusionsUseCase;
use ROITheme\Shared\Application\UseCases\EvaluateComponentVisibility\EvaluateComponentVisibilityUseCase;
// Wrapper Visibility System (Plan 99.15)
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentVisibilityRepository;
use ROITheme\Shared\Application\UseCases\CheckWrapperVisibilityUseCase;
use ROITheme\Shared\Infrastructure\Wordpress\BodyClassHooksRegistrar;
/**
* DIContainer - Contenedor de Inyección de Dependencias
@@ -443,4 +448,49 @@ final class DIContainer
}
return $this->instances['evaluateComponentVisibilityUseCase'];
}
// ===============================
// Wrapper Visibility System (Plan 99.15)
// ===============================
/**
* Obtiene el repositorio de visibilidad de wrappers
*
* Implementa WrapperVisibilityCheckerInterface
*/
public function getWrapperVisibilityChecker(): WrapperVisibilityCheckerInterface
{
if (!isset($this->instances['wrapperVisibilityChecker'])) {
$this->instances['wrapperVisibilityChecker'] = new WordPressComponentVisibilityRepository($this->wpdb);
}
return $this->instances['wrapperVisibilityChecker'];
}
/**
* Obtiene el caso de uso para verificar visibilidad de wrappers
*
* Usado por WrapperVisibilityService para templates
*/
public function getCheckWrapperVisibilityUseCase(): CheckWrapperVisibilityUseCase
{
if (!isset($this->instances['checkWrapperVisibilityUseCase'])) {
$this->instances['checkWrapperVisibilityUseCase'] = new CheckWrapperVisibilityUseCase(
$this->getWrapperVisibilityChecker()
);
}
return $this->instances['checkWrapperVisibilityUseCase'];
}
/**
* Obtiene el registrador de hooks para body_class
*
* CSS failsafe: Agrega clases cuando componentes están ocultos
*/
public function getBodyClassHooksRegistrar(): BodyClassHooksRegistrar
{
if (!isset($this->instances['bodyClassHooksRegistrar'])) {
$this->instances['bodyClassHooksRegistrar'] = new BodyClassHooksRegistrar();
}
return $this->instances['bodyClassHooksRegistrar'];
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* Implementación de WrapperVisibilityCheckerInterface para WordPress
*
* Responsabilidad: Consultar BD y evaluar visibilidad de wrappers de componentes
*
* - Consulta tabla wp_roi_theme_component_settings para is_enabled, show_on_mobile, show_on_desktop
* - Delega evaluación de exclusiones a PageVisibilityHelper (DRY)
*
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
final class WordPressComponentVisibilityRepository implements WrapperVisibilityCheckerInterface
{
private string $tableName;
public function __construct(
private \wpdb $wpdb
) {
$this->tableName = $this->wpdb->prefix . 'roi_theme_component_settings';
}
/**
* {@inheritDoc}
*/
public function isEnabled(string $componentName): bool
{
$value = $this->getVisibilityAttribute($componentName, 'is_enabled');
// Si no existe el registro, asumir habilitado por defecto
if ($value === null) {
return true;
}
return $this->toBool($value);
}
/**
* {@inheritDoc}
*/
public function isVisibleOnDevice(string $componentName, bool $isMobile): bool
{
$attribute = $isMobile ? 'show_on_mobile' : 'show_on_desktop';
$value = $this->getVisibilityAttribute($componentName, $attribute);
// Si no existe el registro, asumir visible por defecto
if ($value === null) {
return true;
}
return $this->toBool($value);
}
/**
* {@inheritDoc}
*
* Delega a PageVisibilityHelper que ya implementa:
* - Visibilidad por tipo de página (home, posts, pages, archives, search)
* - Exclusiones por categoría, post ID, URL pattern
*/
public function isNotExcluded(string $componentName): bool
{
return PageVisibilityHelper::shouldShow($componentName);
}
/**
* Obtiene un atributo del grupo visibility desde la BD
*
* @param string $componentName
* @param string $attributeName
* @return string|null
*/
private function getVisibilityAttribute(string $componentName, string $attributeName): ?string
{
$sql = $this->wpdb->prepare(
"SELECT attribute_value
FROM {$this->tableName}
WHERE component_name = %s
AND group_name = %s
AND attribute_name = %s
LIMIT 1",
$componentName,
'visibility',
$attributeName
);
$result = $this->wpdb->get_var($sql);
return $result !== null ? (string) $result : null;
}
/**
* Convierte string a boolean
*
* @param string $value
* @return bool
*/
private function toBool(string $value): bool
{
return $value === '1' || strtolower($value) === 'true';
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Application\UseCases\CheckWrapperVisibilityUseCase;
use ROITheme\Shared\Infrastructure\Di\DIContainer;
/**
* Servicio facade para verificar visibilidad de wrappers desde templates
*
* Responsabilidad: Proveer acceso simplificado (singleton/static) al
* CheckWrapperVisibilityUseCase para uso en templates PHP.
*
* USO EN TEMPLATES:
* ```php
* if (WrapperVisibilityService::shouldRenderWrapper('navbar')) {
* // Renderizar wrapper y componente
* }
* ```
*
* @package ROITheme\Shared\Infrastructure\Services
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
final class WrapperVisibilityService
{
private static ?CheckWrapperVisibilityUseCase $useCase = null;
/**
* Verifica si el wrapper de un componente debe renderizarse
*
* @param string $componentName Nombre del componente (kebab-case)
* @return bool True si el wrapper debe renderizarse
*/
public static function shouldRenderWrapper(string $componentName): bool
{
$useCase = self::getUseCase();
$isMobile = self::detectMobile();
return $useCase->execute($componentName, $isMobile);
}
/**
* Verifica visibilidad para múltiples componentes
*
* Útil para determinar si renderizar un contenedor que agrupa varios componentes
*
* @param array<string> $componentNames Lista de nombres de componentes
* @return bool True si AL MENOS UNO de los componentes debe mostrarse
*/
public static function shouldRenderAnyWrapper(array $componentNames): bool
{
foreach ($componentNames as $componentName) {
if (self::shouldRenderWrapper($componentName)) {
return true;
}
}
return false;
}
/**
* Obtiene o crea el UseCase
*
* @return CheckWrapperVisibilityUseCase
*/
private static function getUseCase(): CheckWrapperVisibilityUseCase
{
if (self::$useCase === null) {
$container = DIContainer::getInstance();
self::$useCase = $container->getCheckWrapperVisibilityUseCase();
}
return self::$useCase;
}
/**
* Detecta si el dispositivo actual es móvil
*
* Usa wp_is_mobile() de WordPress
*
* @return bool
*/
private static function detectMobile(): bool
{
if (function_exists('wp_is_mobile')) {
return wp_is_mobile();
}
return false;
}
/**
* Limpia la instancia del UseCase (útil para tests)
*/
public static function reset(): void
{
self::$useCase = null;
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Wordpress;
use ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService;
/**
* Registra hook body_class para agregar clases CSS de componentes ocultos
*
* RESPONSABILIDAD:
* - Registrar hook body_class
* - Agregar clases CSS cuando componentes están ocultos
*
* FLUJO:
* 1. body_class filter → addHiddenComponentClasses()
* - Verifica visibilidad de componentes clave (navbar, sidebar components)
* - Agrega clases: roi-hide-navbar, roi-hide-sidebar, etc.
*
* PROPÓSITO:
* Failsafe CSS: Si los templates no pueden ocultar wrappers completamente,
* estas clases permiten ocultarlos via CSS.
*
* PATRÓN:
* - SRP: Solo registra hooks, delega lógica a WrapperVisibilityService
*
* @package ROITheme\Shared\Infrastructure\Wordpress
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
final class BodyClassHooksRegistrar
{
/**
* Componentes que afectan el layout principal
*/
private const LAYOUT_COMPONENTS = [
'navbar' => 'roi-hide-navbar',
'table-of-contents' => 'roi-hide-toc',
'cta-box-sidebar' => 'roi-hide-cta-sidebar',
'sidebar' => 'roi-hide-sidebar',
];
/**
* Componentes de sidebar que determinan si mostrar columna lateral
*/
private const SIDEBAR_COMPONENTS = [
'table-of-contents',
'cta-box-sidebar',
];
/**
* Registrar hooks de WordPress
*/
public function register(): void
{
add_filter('body_class', [$this, 'addHiddenComponentClasses']);
}
/**
* Callback para body_class - agrega clases para componentes ocultos
*
* @param array<string> $classes Clases existentes
* @return array<string> Clases modificadas
*/
public function addHiddenComponentClasses(array $classes): array
{
// Agregar clase por cada componente oculto
foreach (self::LAYOUT_COMPONENTS as $componentName => $cssClass) {
if (!WrapperVisibilityService::shouldRenderWrapper($componentName)) {
$classes[] = $cssClass;
}
}
// Verificar si TODOS los componentes de sidebar están ocultos
if ($this->allSidebarComponentsHidden()) {
$classes[] = 'roi-sidebar-empty';
}
return $classes;
}
/**
* Verifica si todos los componentes de sidebar están ocultos
*
* @return bool True si ningún componente de sidebar debe mostrarse
*/
private function allSidebarComponentsHidden(): bool
{
foreach (self::SIDEBAR_COMPONENTS as $componentName) {
if (WrapperVisibilityService::shouldRenderWrapper($componentName)) {
return false;
}
}
return true;
}
}

View File

@@ -370,6 +370,11 @@ add_action('after_setup_theme', function() {
$criticalCSSService = roi_get_critical_css_service();
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService);
$hooksRegistrar->register();
// 3. Body Class Hooks (Plan 99.15) - CSS failsafe para componentes ocultos
$container = \ROITheme\Shared\Infrastructure\Di\DIContainer::getInstance();
$bodyClassHooksRegistrar = $container->getBodyClassHooksRegistrar();
$bodyClassHooksRegistrar->register();
});
// =============================================================================
@@ -378,6 +383,47 @@ add_action('after_setup_theme', function() {
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
// =============================================================================
// =============================================================================
// HELPER FUNCTION: roi_should_render_wrapper() - Plan 99.15
// =============================================================================
/**
* Verifica si el wrapper de un componente debe renderizarse
*
* Evalúa:
* - is_enabled
* - show_on_mobile / show_on_desktop
* - Exclusiones (categoría, post ID, URL pattern, page visibility)
*
* USO EN TEMPLATES:
* ```php
* if (roi_should_render_wrapper('navbar')) {
* echo '<nav class="navbar">';
* echo roi_render_component('navbar');
* echo '</nav>';
* }
* ```
*
* @param string $componentName Nombre del componente (kebab-case)
* @return bool True si el wrapper debe renderizarse
* @see Plan 99.15 - Fix Empty Layout Wrappers
*/
function roi_should_render_wrapper(string $componentName): bool {
return \ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService::shouldRenderWrapper($componentName);
}
/**
* Verifica si AL MENOS UN componente de una lista debe renderizarse
*
* Útil para determinar si mostrar columna sidebar
*
* @param array<string> $componentNames Lista de nombres de componentes
* @return bool True si al menos uno debe mostrarse
*/
function roi_should_render_any_wrapper(array $componentNames): bool {
return \ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService::shouldRenderAnyWrapper($componentNames);
}
// =============================================================================
// HELPER FUNCTION: roi_get_adsense_search_config()
// =============================================================================

View File

@@ -35,6 +35,10 @@ if (function_exists('roi_render_component')) {
?>
<!-- Navbar (Template líneas 264-320) -->
<?php
// Plan 99.15: Solo renderizar wrapper si navbar debe mostrarse
if (function_exists('roi_should_render_wrapper') && roi_should_render_wrapper('navbar')):
?>
<nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'roi-theme'); ?>">
<div class="container">
@@ -95,3 +99,4 @@ if (function_exists('roi_render_component')) {
</div><!-- .container -->
</nav><!-- .navbar -->
<?php endif; // roi_should_render_wrapper('navbar') ?>

View File

@@ -26,11 +26,19 @@ if (function_exists('roi_render_component')) {
?>
<!-- Main Content Grid -->
<?php
// Plan 99.15: Determinar si mostrar sidebar basándose en visibilidad de componentes
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
$show_sidebar = function_exists('roi_should_render_any_wrapper')
? roi_should_render_any_wrapper($sidebar_components)
: true;
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
?>
<div class="container">
<div class="row">
<!-- Main Content Column (col-lg-9) -->
<div class="col-lg-9">
<!-- Main Content Column -->
<div class="<?php echo esc_attr($main_col_class); ?>">
<!-- Featured Image - Componente dinámico -->
<?php
@@ -79,8 +87,9 @@ if (function_exists('roi_render_component')) {
}
?>
</div><!-- .col-lg-9 -->
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
<?php if ($show_sidebar): ?>
<!-- Sidebar Column (col-lg-3) -->
<div class="col-lg-3">
<div class="sidebar-sticky">
@@ -99,6 +108,7 @@ if (function_exists('roi_render_component')) {
?>
</div>
</div>
<?php endif; ?>
</div><!-- .row -->
</div><!-- .container -->

View File

@@ -24,11 +24,19 @@ if (function_exists('roi_render_component')) {
?>
<!-- Main Content Grid (Template líneas 169-1020) -->
<?php
// Plan 99.15: Determinar si mostrar sidebar basándose en visibilidad de componentes
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
$show_sidebar = function_exists('roi_should_render_any_wrapper')
? roi_should_render_any_wrapper($sidebar_components)
: true;
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
?>
<div class="container">
<div class="row">
<!-- Main Content Column (col-lg-9) -->
<div class="col-lg-9">
<!-- Main Content Column -->
<div class="<?php echo esc_attr($main_col_class); ?>">
<!-- Featured Image - Componente dinámico -->
<?php
@@ -77,8 +85,9 @@ if (function_exists('roi_render_component')) {
}
?>
</div><!-- .col-lg-9 -->
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
<?php if ($show_sidebar): ?>
<!-- Sidebar Column (col-lg-3) -->
<div class="col-lg-3">
<div class="sidebar-sticky">
@@ -97,6 +106,7 @@ if (function_exists('roi_render_component')) {
?>
</div>
</div>
<?php endif; ?>
</div><!-- .row -->
</div><!-- .container -->