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