From 4f25297f14ffce9bb74c7b3488bb6fb17289d4d6 Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Sat, 29 Nov 2025 09:29:45 -0600 Subject: [PATCH] =?UTF-8?q?feat(pagespeed):=20implementar=20campo=20is=5Fc?= =?UTF-8?q?ritical=20para=20CSS=20cr=C3=ADtico=20din=C3=A1mico=20(Phase=20?= =?UTF-8?q?4.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementación completa del sistema de Critical CSS dinámico según plan 13.01: Domain Layer: - Crear CriticalCSSCollectorInterface para DIP compliance Infrastructure Layer: - Implementar CriticalCSSCollector (singleton via DIContainer) - Crear CriticalCSSHooksRegistrar para inyección en wp_head - Actualizar DIContainer con getCriticalCSSCollector() Schemas: - Agregar campo is_critical a navbar, top-notification-bar, hero - Sincronizar con BD (18+39+31 campos) Renderers (navbar, top-notification-bar, hero): - Inyectar CriticalCSSCollectorInterface via constructor - Lógica condicional: si is_critical=true → CSS a Admin (FormBuilders + FieldMappers): - Toggle "CSS Crítico" en sección visibility - Mapeo AJAX para persistencia Beneficios: - LCP optimizado: CSS crítico inline en - Above-the-fold rendering sin FOUC - Componentes configurables desde admin panel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../FieldMapping/HeroFieldMapper.php | 1 + .../Infrastructure/Ui/HeroFormBuilder.php | 16 ++++- .../FieldMapping/NavbarFieldMapper.php | 1 + .../Infrastructure/Ui/NavbarFormBuilder.php | 15 ++++- .../TopNotificationBarFieldMapper.php | 1 + .../Ui/TopNotificationBarFormBuilder.php | 16 ++++- .../Hero/Infrastructure/Ui/HeroRenderer.php | 19 +++++- .../Infrastructure/Ui/NavbarRenderer.php | 16 ++++- .../Ui/TopNotificationBarRenderer.php | 17 ++++- Schemas/hero.json | 7 ++ Schemas/navbar.json | 7 ++ Schemas/top-notification-bar.json | 7 ++ .../CriticalCSSCollectorInterface.php | 45 +++++++++++++ Shared/Infrastructure/Di/DIContainer.php | 19 ++++++ .../Services/CriticalCSSCollector.php | 67 +++++++++++++++++++ .../Wordpress/CriticalCSSHooksRegistrar.php | 46 +++++++++++++ functions-addon.php | 55 ++++++++++++--- 17 files changed, 340 insertions(+), 15 deletions(-) create mode 100644 Shared/Domain/Contracts/CriticalCSSCollectorInterface.php create mode 100644 Shared/Infrastructure/Services/CriticalCSSCollector.php create mode 100644 Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php diff --git a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php index 56b55922..0ad8eebd 100644 --- a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php +++ b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php @@ -27,6 +27,7 @@ final class HeroFieldMapper implements FieldMapperInterface 'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], 'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + 'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], // Content 'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'], diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php index 160dce70..a9e13ec0 100644 --- a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php +++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php @@ -103,7 +103,7 @@ final class HeroFormBuilder $html .= ' '; $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; + $html .= '
'; $html .= '
'; + // Switch: CSS Crítico + $isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + $html .= '
'; $html .= ''; diff --git a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php index 54fe0adb..2e25f22b 100644 --- a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php +++ b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php @@ -28,6 +28,7 @@ final class NavbarFieldMapper implements FieldMapperInterface 'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], 'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'], + 'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], // Layout 'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'], diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php index 1a568dc0..596ad0d7 100644 --- a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php +++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php @@ -119,7 +119,7 @@ final class NavbarFormBuilder // Switch: Sticky $sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true); - $html .= '
'; + $html .= '
'; $html .= '
'; $html .= ' '; @@ -129,6 +129,19 @@ final class NavbarFormBuilder $html .= '
'; $html .= '
'; + // Switch: CSS Crítico + $isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + $html .= '
'; $html .= ''; diff --git a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php index f5f8f163..5dd5997b 100644 --- a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php +++ b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php @@ -27,6 +27,7 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface 'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], 'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + 'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], // Content 'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'], diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php index b5087dc6..cd130552 100644 --- a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php +++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php @@ -107,7 +107,7 @@ final class TopNotificationBarFormBuilder // Select: Show on Pages $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); - $html .= '
'; + $html .= '
'; $html .= '
'; + // Switch: CSS Crítico + $isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + $html .= '
'; $html .= ''; diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php index 8f970168..d3a027ce 100644 --- a/Public/Hero/Infrastructure/Ui/HeroRenderer.php +++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php @@ -5,6 +5,7 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; +use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use ROITheme\Shared\Domain\Entities\Component; /** @@ -28,8 +29,13 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class HeroRenderer implements RendererInterface { + /** + * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS + * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico + */ public function __construct( - private CSSGeneratorInterface $cssGenerator + private CSSGeneratorInterface $cssGenerator, + private CriticalCSSCollectorInterface $criticalCollector ) {} public function render(Component $component): string @@ -47,6 +53,17 @@ final class HeroRenderer implements RendererInterface $css = $this->generateCSS($data); $html = $this->buildHTML($data); + // Verificar si el CSS debe ser crítico (inyectado en ) + $isCritical = isset($data['visibility']['is_critical']) && + $data['visibility']['is_critical'] === true; + + if ($isCritical) { + // CSS crítico: agregar al collector para inyección en + $this->criticalCollector->add('hero', $css); + return $html; // Solo HTML, CSS se inyecta en + } + + // CSS no crítico: incluir inline con el componente return sprintf("\n%s", $css, $html); } diff --git a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php index d31b9e55..3fe298c1 100644 --- a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php +++ b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui; use ROITheme\Shared\Domain\Entities\Component; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; +use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use Walker_Nav_Menu; /** @@ -30,9 +31,11 @@ final class NavbarRenderer implements RendererInterface { /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS + * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico */ public function __construct( - private CSSGeneratorInterface $cssGenerator + private CSSGeneratorInterface $cssGenerator, + private CriticalCSSCollectorInterface $criticalCollector ) {} public function render(Component $component): string @@ -46,6 +49,17 @@ final class NavbarRenderer implements RendererInterface $css = $this->generateCSS($data); $html = $this->buildMenu($data); + // Verificar si el CSS debe ser crítico (inyectado en ) + $isCritical = isset($data['visibility']['is_critical']) && + $data['visibility']['is_critical'] === true; + + if ($isCritical) { + // CSS crítico: agregar al collector para inyección en + $this->criticalCollector->add('navbar', $css); + return $html; // Solo HTML, CSS se inyecta en + } + + // CSS no crítico: incluir inline con el componente return sprintf( "\n%s", $css, diff --git a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php index 1c31544c..31b7915f 100644 --- a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php +++ b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php @@ -5,6 +5,7 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; +use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use ROITheme\Shared\Domain\Entities\Component; /** @@ -36,9 +37,11 @@ final class TopNotificationBarRenderer implements RendererInterface { /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS + * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico */ public function __construct( - private CSSGeneratorInterface $cssGenerator + private CSSGeneratorInterface $cssGenerator, + private CriticalCSSCollectorInterface $criticalCollector ) {} /** @@ -64,7 +67,17 @@ final class TopNotificationBarRenderer implements RendererInterface // Generar HTML $html = $this->buildHTML($data); - // Combinar todo + // Verificar si el CSS debe ser crítico (inyectado en ) + $isCritical = isset($data['visibility']['is_critical']) && + $data['visibility']['is_critical'] === true; + + if ($isCritical) { + // CSS crítico: agregar al collector para inyección en + $this->criticalCollector->add('top-notification-bar', $css); + return $html; // Solo HTML, CSS se inyecta en + } + + // CSS no crítico: incluir inline con el componente return sprintf( "\n%s", $css, diff --git a/Schemas/hero.json b/Schemas/hero.json index 7dd491b6..5a82caf9 100644 --- a/Schemas/hero.json +++ b/Schemas/hero.json @@ -44,6 +44,13 @@ "home": "Solo página de inicio" }, "description": "Define en qué tipo de contenido se mostrará el hero" + }, + "is_critical": { + "type": "boolean", + "label": "CSS Crítico", + "default": true, + "editable": true, + "description": "Inyectar CSS inline en para optimizar LCP (componente above-the-fold)" } } }, diff --git a/Schemas/navbar.json b/Schemas/navbar.json index 654f3931..d81a77bc 100644 --- a/Schemas/navbar.json +++ b/Schemas/navbar.json @@ -48,6 +48,13 @@ "default": true, "editable": true, "description": "Mantiene el navbar fijo en la parte superior al hacer scroll" + }, + "is_critical": { + "type": "boolean", + "label": "CSS Crítico", + "default": true, + "editable": true, + "description": "Inyectar CSS inline en para optimizar LCP (componente above-the-fold)" } } }, diff --git a/Schemas/top-notification-bar.json b/Schemas/top-notification-bar.json index 9fbd590b..87f0134f 100644 --- a/Schemas/top-notification-bar.json +++ b/Schemas/top-notification-bar.json @@ -44,6 +44,13 @@ "editable": true, "required": true, "description": "Muestra la barra en dispositivos móviles (pantallas pequeñas)" + }, + "is_critical": { + "type": "boolean", + "label": "CSS Crítico", + "default": true, + "editable": true, + "description": "Inyectar CSS inline en para optimizar LCP (componente above-the-fold)" } } }, diff --git a/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php b/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php new file mode 100644 index 00000000..afd4ec94 --- /dev/null +++ b/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php @@ -0,0 +1,45 @@ + [componentName => css] + */ + public function getAll(): array; + + /** + * Renderizar CSS crítico como tag ', + $css + ); + } + + /** + * {@inheritDoc} + */ + public function clear(): void + { + $this->criticalStyles = []; + } +} diff --git a/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php new file mode 100644 index 00000000..7547dd4b --- /dev/null +++ b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php @@ -0,0 +1,46 @@ +, antes de otros estilos + add_action('wp_head', [$this, 'renderCriticalCSS'], 1); + } + + /** + * Callback para wp_head + */ + public function renderCriticalCSS(): void + { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->collector->render(); + } +} diff --git a/functions-addon.php b/functions-addon.php index ee3a7708..070a3a0d 100644 --- a/functions-addon.php +++ b/functions-addon.php @@ -114,6 +114,28 @@ function roi_get_navbar_setting(string $group, string $attribute, $default = nul return $value; } +// ============================================================================= +// CRITICAL CSS COLLECTOR SINGLETON +// ============================================================================= + +/** + * Obtiene la instancia singleton del CriticalCSSCollector + * + * Patrón Singleton implementado via función para mantener una única instancia + * que será compartida por todos los Renderers y el HooksRegistrar + * + * @return \ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface + */ +function roi_get_critical_css_collector(): \ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface { + static $collector = null; + + if ($collector === null) { + $collector = new \ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector(); + } + + return $collector; +} + // ============================================================================= // HELPER FUNCTION: roi_render_component() // ============================================================================= @@ -186,22 +208,27 @@ function roi_render_component(string $componentName): string { // Crear instancia del CSSGeneratorService (reutilizable para todos los renderers que lo necesiten) $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); + // Obtener instancia singleton del CriticalCSSCollector + $criticalCollector = roi_get_critical_css_collector(); + switch ($componentName) { - // Componentes nuevos (namespace PascalCase correcto) + // Componentes con soporte de CSS Crítico (above-the-fold) case 'top-notification-bar': - $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator); + $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator, $criticalCollector); break; case 'navbar': - $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator); - break; - case 'cta-lets-talk': - $renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator); + $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator, $criticalCollector); break; case 'hero': error_log("ROI Theme DEBUG: Creating HeroRenderer"); - $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator); + $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator, $criticalCollector); 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 'hero-section': $renderer = new \ROITheme\Public\HeroSection\Infrastructure\Ui\HeroSectionRenderer(); break; @@ -250,9 +277,21 @@ function roi_render_component(string $componentName): string { } // ============================================================================= -// ESTILOS BASE PARA TOP NOTIFICATION BAR +// REGISTRO DE CRITICAL CSS HOOKS // ============================================================================= +/** + * Registra el hook para inyectar CSS crítico en + * + * IMPORTANTE: El HooksRegistrar usa la misma instancia singleton del collector + * que usan los Renderers, garantizando que el CSS recolectado se inyecte + * correctamente en wp_head con prioridad 1 (muy temprano). + */ +add_action('after_setup_theme', function() { + $criticalCollector = roi_get_critical_css_collector(); + $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCollector); + $hooksRegistrar->register(); +}); // ============================================================================= // NOTA: Los estilos de TOC y CTA Box Sidebar se generan dinámicamente