get_template_directory() . '/Shared/', 'ROITheme\\Public\\' => get_template_directory() . '/Public/', 'ROITheme\\Admin\\' => get_template_directory() . '/Admin/', ]; foreach ($prefixes as $prefix => $base_dir) { $len = strlen($prefix); if (strncmp($prefix, $class, $len) === 0) { $relative_class = substr($class, $len); $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; if (file_exists($file)) { require $file; return; } } } }); // ============================================================================= // FONT PRELOAD - Previene CLS por font swap // ============================================================================= /** * Precarga fuentes críticas para prevenir CLS por font swap * * Las fuentes Poppins se precargan con alta prioridad para que estén * disponibles cuando el CSS las necesite, evitando el "flash" de * fuente de respaldo que causa layout shift. */ add_action('wp_head', function() { $theme_url = get_template_directory_uri(); // Fuentes críticas: regular y 600 (usadas en títulos y body) $critical_fonts = [ 'poppins-v24-latin-regular.woff2', 'poppins-v24-latin-600.woff2', ]; foreach ($critical_fonts as $font) { printf( '%s', esc_url($theme_url), esc_attr($font), "\n" ); } }, 1); // Priority 1 = muy temprano en wp_head // ============================================================================= // HELPER FUNCTION: roi_get_component_setting() - GENÉRICA // ============================================================================= /** * Obtiene un valor de configuración de cualquier componente desde la BD * * Reemplaza a roi_get_option() legacy - lee de wp_roi_theme_component_settings * * @param string $component Nombre del componente (ej: 'featured-image', 'navbar') * @param string $group Nombre del grupo (ej: 'visibility', 'content') * @param string $attribute Nombre del atributo (ej: 'is_enabled', 'show_on_pages') * @param mixed $default Valor por defecto si no existe * @return mixed Valor del atributo */ function roi_get_component_setting(string $component, string $group, string $attribute, $default = null) { global $wpdb; $table = $wpdb->prefix . 'roi_theme_component_settings'; $value = $wpdb->get_var($wpdb->prepare( "SELECT attribute_value FROM {$table} WHERE component_name = %s AND group_name = %s AND attribute_name = %s", $component, $group, $attribute )); if ($value === null) { return $default; } // Convertir booleanos if ($value === '1') return true; if ($value === '0') return false; // Intentar decodificar JSON $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return $decoded; } return $value; } // ============================================================================= // HELPER FUNCTION: roi_get_navbar_setting() // ============================================================================= /** * Obtiene un valor de configuración del navbar desde la BD * * @param string $group Nombre del grupo (ej: 'media', 'visibility') * @param string $attribute Nombre del atributo (ej: 'show_brand', 'logo_url') * @param mixed $default Valor por defecto si no existe * @return mixed Valor del atributo */ function roi_get_navbar_setting(string $group, string $attribute, $default = null) { global $wpdb; $table = $wpdb->prefix . 'roi_theme_component_settings'; $value = $wpdb->get_var($wpdb->prepare( "SELECT attribute_value FROM {$table} WHERE component_name = 'navbar' AND group_name = %s AND attribute_name = %s", $group, $attribute )); if ($value === null) { return $default; } // Convertir booleanos if ($value === '1') return true; if ($value === '0') return false; // Intentar decodificar JSON $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return $decoded; } return $value; } // ============================================================================= // CRITICAL CSS SERVICE SINGLETON // ============================================================================= /** * Obtiene la instancia singleton del CriticalCSSService * * Este servicio consulta la BD para componentes con is_critical=true * y genera su CSS en wp_head ANTES de que los componentes rendericen. * * @return \ROITheme\Shared\Infrastructure\Services\CriticalCSSService */ function roi_get_critical_css_service(): \ROITheme\Shared\Infrastructure\Services\CriticalCSSService { global $wpdb; static $cssGenerator = null; if ($cssGenerator === null) { $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); } return \ROITheme\Shared\Infrastructure\Services\CriticalCSSService::getInstance($wpdb, $cssGenerator); } // ============================================================================= // HELPER FUNCTION: roi_render_component() // ============================================================================= /** * Renderiza un componente por su nombre * * @param string $componentName Nombre del componente * @return string HTML del componente renderizado */ function roi_render_component(string $componentName): string { global $wpdb; // DEBUG: Trace component rendering error_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}"); try { // Obtener datos del componente desde BD normalizada $table = $wpdb->prefix . 'roi_theme_component_settings'; $rows = $wpdb->get_results($wpdb->prepare( "SELECT group_name, attribute_name, attribute_value FROM {$table} WHERE component_name = %s ORDER BY group_name, attribute_name", $componentName )); if (empty($rows)) { return ''; } // Reconstruir estructura de datos agrupada $data = []; foreach ($rows as $row) { if (!isset($data[$row->group_name])) { $data[$row->group_name] = []; } // Decodificar valor $value = $row->attribute_value; // Convertir booleanos almacenados como '1' o '0' if ($value === '1' || $value === '0') { $value = ($value === '1'); } else { // Intentar decodificar JSON $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $value = $decoded; } } $data[$row->group_name][$row->attribute_name] = $value; } // Crear Value Objects requeridos $name = new \ROITheme\Shared\Domain\ValueObjects\ComponentName($componentName); $configuration = new \ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration($data); // ===================================================================== // VALIDACIÓN DE VISIBILIDAD POR DISPOSITIVO (Previene CLS) // ===================================================================== // Si el componente no debe mostrarse en el dispositivo actual, // NO renderizar nada. Esto evita CLS causado por elementos que // se renderizan y luego se ocultan con CSS. // ===================================================================== // Leer configuración de visibilidad desde BD $isEnabled = ($data['visibility']['is_enabled'] ?? true) === true; $showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true; $showOnMobile = ($data['visibility']['show_on_mobile'] ?? true) === true; // Si no está habilitado, no renderizar if (!$isEnabled) { return ''; } // Detectar dispositivo actual y validar visibilidad // wp_is_mobile() detecta móviles y tablets via User-Agent $isMobileDevice = wp_is_mobile(); if ($isMobileDevice && !$showOnMobile) { // Dispositivo móvil pero show_on_mobile = false → NO renderizar return ''; } if (!$isMobileDevice && !$showOnDesktop) { // Dispositivo desktop pero show_on_desktop = false → NO renderizar return ''; } // Crear ComponentVisibility con datos reales de BD $visibility = new \ROITheme\Shared\Domain\ValueObjects\ComponentVisibility( enabled: $isEnabled, visibleDesktop: $showOnDesktop, visibleTablet: $showOnMobile, // tablet tratado como mobile visibleMobile: $showOnMobile ); // Crear instancia del componente $component = new \ROITheme\Shared\Domain\Entities\Component( $name, $configuration, $visibility ); // Obtener renderer específico para el componente $renderer = null; // Crear instancia del CSSGeneratorService (reutilizable para todos los renderers) $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); switch ($componentName) { // Componentes con soporte de CSS Crítico (above-the-fold) // Nota: Si is_critical=true, el CSS ya fue inyectado en por CriticalCSSService case 'top-notification-bar': $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator); break; case 'navbar': $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator); break; case 'hero': error_log("ROI Theme DEBUG: Creating HeroRenderer"); $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator); error_log("ROI Theme DEBUG: HeroRenderer created successfully"); break; // Componentes sin soporte de CSS Crítico (below-the-fold) case 'cta-lets-talk': $renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator); break; case 'featured-image': $renderer = new \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer($cssGenerator); break; case 'table-of-contents': $renderer = new \ROITheme\Public\TableOfContents\Infrastructure\Ui\TableOfContentsRenderer($cssGenerator); break; case 'cta-box-sidebar': $renderer = new \ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui\CtaBoxSidebarRenderer($cssGenerator); break; case 'social-share': $renderer = new \ROITheme\Public\SocialShare\Infrastructure\Ui\SocialShareRenderer($cssGenerator); break; case 'cta-post': $renderer = new \ROITheme\Public\CtaPost\Infrastructure\Ui\CtaPostRenderer($cssGenerator); break; case 'related-post': $renderer = new \ROITheme\Public\RelatedPost\Infrastructure\Ui\RelatedPostRenderer($cssGenerator); break; case 'contact-form': $renderer = new \ROITheme\Public\ContactForm\Infrastructure\Ui\ContactFormRenderer($cssGenerator); break; case 'footer': $renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator); break; } if (!$renderer) { error_log("ROI Theme DEBUG: No renderer for {$componentName}"); return ''; } error_log("ROI Theme DEBUG: Calling render() for {$componentName}"); $output = $renderer->render($component); error_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}"); return $output; } catch (\Exception $e) { // Always log errors for debugging error_log('ROI Theme ERROR: Exception rendering component ' . $componentName . ': ' . $e->getMessage()); error_log('ROI Theme ERROR: Stack trace: ' . $e->getTraceAsString()); return ''; } } // ============================================================================= // REGISTRO DE CRITICAL CSS HOOKS // ============================================================================= /** * Registra hooks para inyectar CSS crítico en * * FLUJO: * 1. wp_head (priority 0) → CriticalBootstrapService::render() * - Inyecta critical-bootstrap.css inline (grid, navbar, utilities) * - Permite diferir bootstrap-subset.min.css * * 2. wp_head (priority 1) → CriticalCSSService::render() * - Consulta BD por componentes con is_critical=true * - Genera CSS usando los métodos públicos generateCSS() de los Renderers * - Output: * * IMPORTANTE: CriticalBootstrapService HABILITADO para mejorar LCP. * critical-bootstrap.css incluye grid system para evitar CLS. */ add_action('after_setup_theme', function() { // 1. Critical Bootstrap CSS (priority 0) - inline bootstrap crítico $criticalBootstrapService = \ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService::getInstance(); add_action('wp_head', [$criticalBootstrapService, 'render'], 0); // 2. Critical Component CSS (priority 1) - CSS de componentes críticos $criticalCSSService = roi_get_critical_css_service(); $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService); $hooksRegistrar->register(); }); // ============================================================================= // NOTA: Los estilos de TOC y CTA Box Sidebar se generan dinámicamente // desde la base de datos a través de sus respectivos Renderers. // NO hardcodear CSS aquí - viola la arquitectura Clean Architecture. // ============================================================================= // ============================================================================= // HELPER FUNCTION: roi_get_adsense_search_config() // ============================================================================= /** * Obtiene la configuracion de AdSense para resultados de busqueda * * Esta funcion es la API publica que el plugin roi-apu-search consume. * El plugin NO debe acceder directamente a la tabla del tema. * * OPTIMIZACION: Una sola query carga todos los settings del componente. * * @return array Configuracion para JavaScript */ function roi_get_adsense_search_config(): array { global $wpdb; // ========================================================================= // CARGAR TODOS LOS SETTINGS EN UNA SOLA QUERY // ========================================================================= $table = $wpdb->prefix . 'roi_theme_component_settings'; $rows = $wpdb->get_results($wpdb->prepare( "SELECT group_name, attribute_name, attribute_value FROM {$table} WHERE component_name = %s", 'adsense-placement' )); if (empty($rows)) { return ['enabled' => false]; } // Organizar en array asociativo por grupo/atributo $settings = []; foreach ($rows as $row) { if (!isset($settings[$row->group_name])) { $settings[$row->group_name] = []; } // Decodificar valor $value = $row->attribute_value; if ($value === '1') $value = true; elseif ($value === '0') $value = false; else { $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $value = $decoded; } } $settings[$row->group_name][$row->attribute_name] = $value; } // Helper para obtener valor con default $get = function(string $group, string $attr, $default = null) use ($settings) { return $settings[$group][$attr] ?? $default; }; // ========================================================================= // VALIDAR CONDICIONES GLOBALES // ========================================================================= // AdSense global deshabilitado if ($get('visibility', 'is_enabled', false) !== true) { return ['enabled' => false]; } // Ads en busqueda deshabilitados if ($get('search_results', 'search_ads_enabled', false) !== true) { return ['enabled' => false]; } // Publisher ID vacio $publisherId = $get('content', 'publisher_id', ''); if (empty($publisherId)) { return ['enabled' => false]; } // ========================================================================= // VALIDAR EXCLUSIONES (igual que el resto del sistema) // ========================================================================= // Ocultar para usuarios logueados if ($get('visibility', 'hide_for_logged_in', false) === true && is_user_logged_in()) { return ['enabled' => false]; } // Visibilidad por dispositivo $isMobile = wp_is_mobile(); if ($isMobile && $get('visibility', 'show_on_mobile', true) !== true) { return ['enabled' => false]; } if (!$isMobile && $get('visibility', 'show_on_desktop', true) !== true) { return ['enabled' => false]; } // ========================================================================= // CONSTRUIR CONFIGURACION // ========================================================================= return [ 'enabled' => true, 'publisherId' => $publisherId, 'slots' => [ 'auto' => $get('content', 'slot_auto', ''), 'inArticle' => $get('content', 'slot_inarticle', ''), 'autorelaxed' => $get('content', 'slot_autorelaxed', ''), 'display' => $get('content', 'slot_display', ''), ], 'topAd' => [ 'enabled' => $get('search_results', 'search_top_ad_enabled', true) === true, 'format' => $get('search_results', 'search_top_ad_format', 'auto'), ], 'betweenAds' => [ 'enabled' => $get('search_results', 'search_between_enabled', true) === true, 'max' => min(3, max(1, (int) $get('search_results', 'search_between_max', '1'))), 'format' => $get('search_results', 'search_between_format', 'in-article'), 'position' => $get('search_results', 'search_between_position', 'random'), 'every' => (int) $get('search_results', 'search_between_every', '5'), ], 'delay' => [ 'enabled' => $get('forms', 'delay_enabled', true) === true, 'timeout' => (int) $get('forms', 'delay_timeout', '5000'), ], ]; }