feat(critical-css): implementar TIPO 4 y TIPO 5 - CSS Below-the-fold y Lazy Loading
## TIPO 4: CSS Below-the-fold (Critical Variables + Responsive) - Inyecta variables CSS críticas inline en wp_head P:-1 - Inyecta media queries críticas inline en wp_head P:2 (corregido de P:1) - Auto-regeneración cuando archivos fuente cambian (filemtime check) - Cache en Assets/CriticalCSS/ para evitar lecturas repetidas - Comando WP-CLI: wp roi-theme generate-critical-css Archivos TIPO 4: - Public/CriticalCSS/Domain/Contracts/ - Interfaces (DIP) - Public/CriticalCSS/Application/UseCases/GetCriticalCSSUseCase.php - Public/CriticalCSS/Infrastructure/Cache/CriticalCSSFileCache.php - Public/CriticalCSS/Infrastructure/Services/CriticalCSSExtractor.php - Public/CriticalCSS/Infrastructure/Services/CriticalCSSInjector.php - bin/generate-critical-css.php ## TIPO 5: CSS No Crítico (Lazy Loading) - Animaciones CSS: carga 2s después de page load via requestIdleCallback - Print CSS: carga solo al imprimir via beforeprint event - Fallback <noscript> para usuarios sin JavaScript - Safari fallback: setTimeout cuando requestIdleCallback no disponible Archivos TIPO 5: - Assets/Js/lazy-css-loader.js - Public/LazyCSSLoader/Infrastructure/Contracts/LazyCSSRegistrarInterface.php - Public/LazyCSSLoader/Infrastructure/Services/LazyCSSRegistrar.php ## Fix: Colisión de prioridades wp_head Antes: TIPO 1 (P:1), TIPO 4 responsive (P:1), TIPO 3 (P:2) - CONFLICTO Después: TIPO 1 (P:1), TIPO 4 responsive (P:2), TIPO 3 (P:3) - OK Nuevo orden de prioridades: P:-1 roi-critical-variables (TIPO 4) P:0 roi-critical-bootstrap (TIPO 2) P:1 roi-critical-css (TIPO 1) P:2 roi-critical-responsive (TIPO 4) P:3 roi-custom-critical-css (TIPO 3) P:5 roi-theme-layout-css (ThemeSettings) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1
Assets/CriticalCSS/responsive.critical.css
Normal file
1
Assets/CriticalCSS/responsive.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
@media (max-width:575.98px){:root{--bs-gutter-x:1rem}body{font-size:14px}h1{font-size:24px}h2{font-size:20px}h3{font-size:18px}.container-fluid{padding:0 10px}.navbar{padding:0.5rem 0}.navbar-brand{font-size:18px}main{padding:0.5rem}.sidebar{margin-top:2rem}table{font-size:12px;margin-bottom:1rem;overflow-x:auto}.table-responsive{margin-bottom:1rem}.btn{padding:0.375rem 0.75rem;font-size:14px}.btn-lg{padding:0.5rem 1rem;font-size:16px}.card{margin-bottom:1rem}.form-group{margin-bottom:1rem}.form-control{padding:0.375rem 0.75rem;font-size:16px}.modal-dialog{margin:0.5rem}.modal-content{border-radius:4px}img{max-width:100%;height:auto}ul,ol{padding-left:1.5rem}.mt-1,.my-1{margin-top:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.p-1{padding:0.25rem !important}}@media (min-width:576px){body{font-size:14px}h1{font-size:28px}h2{font-size:22px}h3{font-size:18px}}@media (min-width:768px){body{font-size:15px}h1{font-size:32px}h2{font-size:26px}h3{font-size:20px}.row-md-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.navbar{padding:1rem 0}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.no-sidebar{grid-template-columns:1fr}}@media (min-width:992px){body{font-size:16px}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}.row-lg-3{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.with-left-sidebar{grid-template-columns:250px 1fr 300px}.content-wrapper{max-width:1200px;margin:0 auto}}
|
||||
1
Assets/CriticalCSS/variables.critical.css
Normal file
1
Assets/CriticalCSS/variables.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--color-navy-dark:#0E2337;--color-navy-primary:#1e3a5f;--color-navy-light:#2c5282;--color-blue-primary:#1e3a5f;--color-blue-secondary:#2c5282;--color-blue-light:#1a73e8;--color-cyan-primary:#61c7cd;--color-cyan-dark:#4db8c4;--color-cyan-darker:#4fb3b9;--color-orange-primary:#FF8600;--color-orange-secondary:#FFB800;--color-orange-light:#FFB800;--color-orange-button:#FF6B35;--color-orange-button-end:#FF8C42;--color-orange-hover:#FF6B35;--color-neutral-50:#f8f9fa;--color-neutral-100:#e9ecef;--color-neutral-600:#495057;--color-neutral-700:#6c757d;--color-slate-gray:#4C5C6B;--color-gray-50:#f8f9fa;--color-gray-100:#f7fafc;--color-gray-200:#e9ecef;--color-gray-300:#dee2e6;--color-gray-400:#cbd5e0;--color-gray-500:#a0aec0;--color-gray-600:#6c757d;--color-gray-700:#495057;--color-gray-800:#333;--color-gray-900:#212529;--color-gray-dark:#1a1a1a;--color-white:#ffffff;--color-black:#000000;--font-family-base:'Poppins',sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-size-base:1rem;--font-size-sm:0.875rem;--font-size-lg:1.125rem;--font-size-xl:1.25rem;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--line-height-base:1.5;--line-height-tight:1.25;--line-height-loose:1.8;--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--spacing-2xl:3rem;--spacing-3xl:4rem;--border-width:1px;--border-width-thick:2px;--border-width-thicker:3px;--border-width-lateral:4px;--border-radius-sm:4px;--border-radius-md:8px;--border-radius-lg:12px;--border-radius-xl:16px;--border-color-light:var(--color-gray-200);--border-color-default:var(--color-gray-300);--shadow-xs:0 1px 2px rgba(0,0,0,0.05);--shadow-sm:0 2px 4px rgba(0,0,0,0.1);--shadow-md:0 4px 12px rgba(0,0,0,0.15);--shadow-lg:0 8px 24px rgba(0,0,0,0.2);--shadow-xl:0 12px 32px rgba(0,0,0,0.25);--shadow-2xl:0 20px 60px rgba(0,0,0,0.3);--shadow-navbar:0 2px 4px rgba(0,0,0,0.15);--shadow-navbar-scrolled:0 4px 12px rgba(0,0,0,0.25);--shadow-dropdown:0 8px 24px rgba(0,0,0,0.12);--shadow-cta:0 8px 24px rgba(255,133,0,0.3);--shadow-cta-hover:0 12px 32px rgba(255,133,0,0.4);--shadow-button:0 4px 12px rgba(255,107,53,0.3);--shadow-related-posts:0 12px 32px rgba(26,115,232,0.15);--shadow-pagination:0 4px 12px rgba(26,115,232,0.3);--transition-fast:0.15s ease;--transition-base:0.3s ease;--transition-slow:0.5s ease;--transition-cubic:cubic-bezier(0.4,0,0.2,1);--z-dropdown:1000;--z-sticky:1020;--z-navbar:1030;--z-modal-backdrop:1040;--z-modal:1050;--z-popover:1060;--z-tooltip:1070;--gradient-hero:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-cta:linear-gradient(135deg,var(--color-orange-primary) 0%,var(--color-orange-secondary) 100%);--gradient-button-lets-talk:linear-gradient(135deg,var(--color-orange-button) 0%,var(--color-orange-button-end) 100%);--gradient-pagination:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-underline:linear-gradient(90deg,var(--color-cyan-primary) 0%,var(--color-cyan-dark) 100%);--gradient-border-related:linear-gradient(180deg,var(--color-blue-primary) 0%,var(--color-blue-light) 100%);--opacity-disabled:0.5;--opacity-hover:0.8;--opacity-backdrop:0.5;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--breakpoint-xxl:1400px}
|
||||
135
Assets/Js/lazy-css-loader.js
Normal file
135
Assets/Js/lazy-css-loader.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* TIPO 5: Lazy CSS Loader
|
||||
*
|
||||
* Carga CSS no critico despues del evento load usando:
|
||||
* - requestIdleCallback para CSS de baja prioridad
|
||||
* - Event listeners para CSS condicional
|
||||
*
|
||||
* @package ROITheme
|
||||
* @since 1.0.20
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuracion de CSS lazy (inyectada desde PHP)
|
||||
var config = window.roiLazyCSSConfig || {
|
||||
baseUrl: '',
|
||||
version: '1.0.0',
|
||||
idleTimeout: 2000,
|
||||
cssFiles: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Carga un archivo CSS de forma asincrona
|
||||
*
|
||||
* @param {string} href URL del archivo CSS
|
||||
* @param {string} id ID del elemento link
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function loadCSS(href, id) {
|
||||
// Evitar duplicados
|
||||
if (document.getElementById(id)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var link = document.createElement('link');
|
||||
link.id = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.onload = resolve;
|
||||
link.onerror = reject;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS cuando el navegador esta idle
|
||||
*
|
||||
* @param {Array} files Lista de archivos a cargar
|
||||
*/
|
||||
function loadOnIdle(files) {
|
||||
var load = function() {
|
||||
files.forEach(function(file) {
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-' + file.id
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(load, { timeout: config.idleTimeout });
|
||||
} else {
|
||||
// Fallback para Safari
|
||||
setTimeout(load, config.idleTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS de print solo cuando se va a imprimir
|
||||
*
|
||||
* @param {Object} file Archivo de print CSS
|
||||
*/
|
||||
function setupPrintCSS(file) {
|
||||
var loaded = false;
|
||||
|
||||
var load = function() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-print'
|
||||
);
|
||||
};
|
||||
|
||||
// Evento antes de imprimir
|
||||
window.addEventListener('beforeprint', load);
|
||||
|
||||
// Fallback: detectar Ctrl+P / Cmd+P
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializacion
|
||||
*/
|
||||
function init() {
|
||||
var idleFiles = [];
|
||||
var printFile = null;
|
||||
|
||||
config.cssFiles.forEach(function(file) {
|
||||
switch (file.trigger) {
|
||||
case 'idle':
|
||||
idleFiles.push(file);
|
||||
break;
|
||||
case 'print':
|
||||
printFile = file;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar CSS idle despues de que la pagina este lista
|
||||
if (idleFiles.length > 0) {
|
||||
if (document.readyState === 'complete') {
|
||||
loadOnIdle(idleFiles);
|
||||
} else {
|
||||
window.addEventListener('load', function() {
|
||||
loadOnIdle(idleFiles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configurar CSS de print
|
||||
if (printFile) {
|
||||
setupPrintCSS(printFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar
|
||||
init();
|
||||
|
||||
})();
|
||||
@@ -512,14 +512,16 @@ function roi_enqueue_theme_styles() {
|
||||
// );
|
||||
|
||||
// Theme Animations
|
||||
// DIFERIDO: Fase 4.2 PageSpeed - mejoras visuales no críticas
|
||||
wp_enqueue_style(
|
||||
'roi-animations',
|
||||
get_template_directory_uri() . '/Assets/Css/css-global-animations.css',
|
||||
array('roi-bootstrap'),
|
||||
'1.0.0',
|
||||
'print' // Diferido para no bloquear renderizado
|
||||
);
|
||||
// MIGRADO A TIPO 5: Lazy loading via LazyCSSRegistrar (requestIdleCallback)
|
||||
// Fecha: 2025-12-01
|
||||
// @see Public/LazyCSSLoader/Infrastructure/Services/LazyCSSRegistrar.php
|
||||
// wp_enqueue_style(
|
||||
// 'roi-animations',
|
||||
// get_template_directory_uri() . '/Assets/Css/css-global-animations.css',
|
||||
// array('roi-bootstrap'),
|
||||
// '1.0.0',
|
||||
// 'print'
|
||||
// );
|
||||
|
||||
// Theme Responsive Styles
|
||||
// DIFERIDO: Fase 4.3 - media queries no críticas para primer paint
|
||||
@@ -542,13 +544,16 @@ function roi_enqueue_theme_styles() {
|
||||
);
|
||||
|
||||
// Print Styles
|
||||
wp_enqueue_style(
|
||||
'roi-print',
|
||||
get_template_directory_uri() . '/Assets/Css/css-global-print.css',
|
||||
array(),
|
||||
'1.0.0',
|
||||
'print'
|
||||
);
|
||||
// MIGRADO A TIPO 5: Lazy loading via LazyCSSRegistrar (beforeprint event)
|
||||
// Fecha: 2025-12-01
|
||||
// @see Public/LazyCSSLoader/Infrastructure/Services/LazyCSSRegistrar.php
|
||||
// wp_enqueue_style(
|
||||
// 'roi-print',
|
||||
// get_template_directory_uri() . '/Assets/Css/css-global-print.css',
|
||||
// array(),
|
||||
// '1.0.0',
|
||||
// 'print'
|
||||
// );
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_theme_styles', 13);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Application\UseCases;
|
||||
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSExtractorInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso para obtener/generar CSS critico
|
||||
*
|
||||
* Cumple DIP: depende de interfaces, no de clases concretas
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Application\UseCases
|
||||
*/
|
||||
final class GetCriticalCSSUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CriticalCSSCacheInterface $cache,
|
||||
private readonly CriticalCSSExtractorInterface $extractor
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Obtiene CSS critico, regenerando si es necesario
|
||||
*
|
||||
* @param string $type Tipo de CSS critico ('variables', 'responsive')
|
||||
* @return string CSS critico o vacio si no existe
|
||||
*/
|
||||
public function execute(string $type): string
|
||||
{
|
||||
// Si no existe en cache, generar
|
||||
if (!$this->cache->has($type)) {
|
||||
$this->extractor->generateAll();
|
||||
}
|
||||
|
||||
return $this->cache->get($type) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuerza regeneracion de todo el CSS critico
|
||||
*
|
||||
* @return array<string, int> Mapa de tipo => bytes generados
|
||||
*/
|
||||
public function regenerate(): array
|
||||
{
|
||||
$this->cache->clear();
|
||||
return $this->extractor->generateAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para cache de CSS critico
|
||||
*
|
||||
* Define el contrato para almacenamiento y recuperacion
|
||||
* de CSS critico generado (TIPO 4).
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Domain\Contracts
|
||||
*/
|
||||
interface CriticalCSSCacheInterface
|
||||
{
|
||||
/**
|
||||
* Obtiene CSS del cache
|
||||
*
|
||||
* @param string $key Clave del cache (ej: 'variables', 'responsive')
|
||||
* @return string|null CSS cacheado o null si no existe
|
||||
*/
|
||||
public function get(string $key): ?string;
|
||||
|
||||
/**
|
||||
* Guarda CSS en cache
|
||||
*
|
||||
* @param string $key Clave del cache
|
||||
* @param string $css Contenido CSS a guardar
|
||||
* @return bool True si se guardo correctamente
|
||||
*/
|
||||
public function set(string $key, string $css): bool;
|
||||
|
||||
/**
|
||||
* Verifica si existe en cache
|
||||
*
|
||||
* @param string $key Clave del cache
|
||||
* @return bool True si existe
|
||||
*/
|
||||
public function has(string $key): bool;
|
||||
|
||||
/**
|
||||
* Limpia todo el cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear(): void;
|
||||
|
||||
/**
|
||||
* Verifica si el cache esta desactualizado respecto al archivo fuente
|
||||
*
|
||||
* Compara filemtime del archivo fuente vs archivo cache.
|
||||
* Si el fuente es mas nuevo, el cache esta desactualizado.
|
||||
*
|
||||
* @param string $key Clave del cache
|
||||
* @param string $sourceFile Ruta absoluta al archivo fuente
|
||||
* @return bool True si el cache es mas viejo que el fuente
|
||||
*/
|
||||
public function isStale(string $key, string $sourceFile): bool;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para extraccion de CSS critico
|
||||
*
|
||||
* Define el contrato para extraer CSS critico de archivos fuente
|
||||
* y generar versiones optimizadas para inline (TIPO 4).
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Domain\Contracts
|
||||
*/
|
||||
interface CriticalCSSExtractorInterface
|
||||
{
|
||||
/**
|
||||
* Extrae CSS de variables
|
||||
*
|
||||
* El archivo css-global-variables.css completo es critico
|
||||
* porque define las variables CSS usadas en todo el sitio.
|
||||
*
|
||||
* @return string CSS de variables minificado
|
||||
*/
|
||||
public function extractVariables(): string;
|
||||
|
||||
/**
|
||||
* Extrae media queries criticas
|
||||
*
|
||||
* Solo extrae breakpoints mobile-first criticos de
|
||||
* css-global-responsive.css para evitar layout shift.
|
||||
*
|
||||
* @return string CSS responsive critico minificado
|
||||
*/
|
||||
public function extractResponsive(): string;
|
||||
|
||||
/**
|
||||
* Genera y cachea todo el CSS critico
|
||||
*
|
||||
* Extrae variables y responsive, los minifica y guarda en cache.
|
||||
*
|
||||
* @return array<string, int> Mapa de tipo => bytes generados
|
||||
*/
|
||||
public function generateAll(): array;
|
||||
|
||||
/**
|
||||
* Verifica si algun archivo fuente ha cambiado
|
||||
* respecto al cache generado
|
||||
*
|
||||
* Usado para auto-regeneracion en el bootstrap.
|
||||
*
|
||||
* @return bool True si se necesita regenerar
|
||||
*/
|
||||
public function needsRegeneration(): bool;
|
||||
}
|
||||
117
Public/CriticalCSS/Infrastructure/Cache/CriticalCSSFileCache.php
Normal file
117
Public/CriticalCSS/Infrastructure/Cache/CriticalCSSFileCache.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Infrastructure\Cache;
|
||||
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
||||
|
||||
/**
|
||||
* Cache de CSS critico en filesystem con auto-invalidacion
|
||||
*
|
||||
* Almacena CSS critico generado en archivos .critical.css
|
||||
* dentro de Assets/CriticalCSS/
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Infrastructure\Cache
|
||||
*/
|
||||
final class CriticalCSSFileCache implements CriticalCSSCacheInterface
|
||||
{
|
||||
private readonly string $cacheDir;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cacheDir = get_template_directory() . '/Assets/CriticalCSS';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key): ?string
|
||||
{
|
||||
$file = $this->getFilePath($key);
|
||||
|
||||
if (!file_exists($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file);
|
||||
|
||||
return $content !== false ? $content : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function set(string $key, string $css): bool
|
||||
{
|
||||
$this->ensureDirectoryExists();
|
||||
$file = $this->getFilePath($key);
|
||||
|
||||
return file_put_contents($file, $css) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return file_exists($this->getFilePath($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$files = glob($this->cacheDir . '/*.critical.css');
|
||||
|
||||
if ($files === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Compara timestamps para detectar cache desactualizado.
|
||||
* Costo: ~0.1ms (2 llamadas a filemtime)
|
||||
*/
|
||||
public function isStale(string $key, string $sourceFile): bool
|
||||
{
|
||||
$cacheFile = $this->getFilePath($key);
|
||||
|
||||
// Si no existe cache, esta desactualizado
|
||||
if (!file_exists($cacheFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no existe fuente, no esta desactualizado (nada que regenerar)
|
||||
if (!file_exists($sourceFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comparar timestamps
|
||||
return filemtime($sourceFile) > filemtime($cacheFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera ruta del archivo cache
|
||||
*/
|
||||
private function getFilePath(string $key): string
|
||||
{
|
||||
return $this->cacheDir . '/' . $key . '.critical.css';
|
||||
}
|
||||
|
||||
/**
|
||||
* Asegura que el directorio de cache existe
|
||||
*/
|
||||
private function ensureDirectoryExists(): void
|
||||
{
|
||||
if (!is_dir($this->cacheDir)) {
|
||||
mkdir($this->cacheDir, 0755, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSExtractorInterface;
|
||||
|
||||
/**
|
||||
* Extrae CSS critico de archivos fuente
|
||||
*
|
||||
* Estrategia simple:
|
||||
* - Variables: Todo el archivo (siempre critico)
|
||||
* - Responsive: Solo breakpoints mobile-first criticos
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
|
||||
*/
|
||||
final class CriticalCSSExtractor implements CriticalCSSExtractorInterface
|
||||
{
|
||||
// Archivos fuente
|
||||
private const SOURCE_VARIABLES = '/Assets/Css/css-global-variables.css';
|
||||
private const SOURCE_RESPONSIVE = '/Assets/Css/css-global-responsive.css';
|
||||
|
||||
// Breakpoints criticos (mobile-first)
|
||||
// NOTA: El regex maneja 1 nivel de anidamiento.
|
||||
// Si el CSS tiene @supports dentro de @media, no se capturara correctamente.
|
||||
// Esto es aceptable para css-global-responsive.css que es simple.
|
||||
private const CRITICAL_BREAKPOINTS = [
|
||||
'@media (max-width: 575.98px)',
|
||||
'@media (min-width: 576px)',
|
||||
'@media (max-width: 767.98px)',
|
||||
'@media (min-width: 768px)',
|
||||
'@media (min-width: 992px)',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly CriticalCSSCacheInterface $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function extractVariables(): string
|
||||
{
|
||||
$sourceFile = get_template_directory() . self::SOURCE_VARIABLES;
|
||||
|
||||
if (!file_exists($sourceFile)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = file_get_contents($sourceFile);
|
||||
|
||||
if ($css === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Para variables, todo el contenido es critico
|
||||
return $this->minify($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function extractResponsive(): string
|
||||
{
|
||||
$sourceFile = get_template_directory() . self::SOURCE_RESPONSIVE;
|
||||
|
||||
if (!file_exists($sourceFile)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = file_get_contents($sourceFile);
|
||||
|
||||
if ($css === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$criticalCSS = '';
|
||||
|
||||
// Extraer solo media queries criticas
|
||||
// Regex: captura @media (...) { contenido con 1 nivel de {} }
|
||||
foreach (self::CRITICAL_BREAKPOINTS as $breakpoint) {
|
||||
$pattern = '/' . preg_quote($breakpoint, '/') . '\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/s';
|
||||
|
||||
if (preg_match_all($pattern, $css, $matches)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
$criticalCSS .= $match . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->minify($criticalCSS);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function generateAll(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Variables CSS
|
||||
$variablesCSS = $this->extractVariables();
|
||||
if (!empty($variablesCSS)) {
|
||||
$this->cache->set('variables', $variablesCSS);
|
||||
$results['variables'] = strlen($variablesCSS);
|
||||
}
|
||||
|
||||
// Responsive critico
|
||||
$responsiveCSS = $this->extractResponsive();
|
||||
if (!empty($responsiveCSS)) {
|
||||
$this->cache->set('responsive', $responsiveCSS);
|
||||
$results['responsive'] = strlen($responsiveCSS);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function needsRegeneration(): bool
|
||||
{
|
||||
$templateDir = get_template_directory();
|
||||
|
||||
return $this->cache->isStale('variables', $templateDir . self::SOURCE_VARIABLES)
|
||||
|| $this->cache->isStale('responsive', $templateDir . self::SOURCE_RESPONSIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minifica CSS eliminando espacios y comentarios innecesarios
|
||||
*/
|
||||
private function minify(string $css): string
|
||||
{
|
||||
// Eliminar comentarios
|
||||
$css = preg_replace('/\/\*[\s\S]*?\*\//', '', $css) ?? $css;
|
||||
|
||||
// Eliminar espacios innecesarios
|
||||
$css = preg_replace('/\s+/', ' ', $css) ?? $css;
|
||||
$css = preg_replace('/\s*([{};:,>+~])\s*/', '$1', $css) ?? $css;
|
||||
|
||||
// Eliminar ultimo punto y coma antes de }
|
||||
$css = preg_replace('/;}/', '}', $css) ?? $css;
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CriticalCSS\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
||||
|
||||
/**
|
||||
* Inyecta CSS critico en wp_head
|
||||
*
|
||||
* Prioridades:
|
||||
* - P:-1 Variables CSS (antes de todo)
|
||||
* - P:1 Responsive critico (despues de Bootstrap critico)
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
|
||||
*/
|
||||
final class CriticalCSSInjector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CriticalCSSCacheInterface $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Variables CSS: P:-1 (antes de CriticalBootstrapService P:0)
|
||||
add_action('wp_head', [$this, 'injectVariables'], -1);
|
||||
|
||||
// Responsive critico: P:2 (despues de CriticalCSSService P:1)
|
||||
// NOTA: Cambiado de P:1 a P:2 para evitar colision con roi-critical-css
|
||||
add_action('wp_head', [$this, 'injectResponsive'], 2);
|
||||
|
||||
// Deshabilitar enqueue de archivos que ahora son inline
|
||||
add_action('wp_enqueue_scripts', [$this, 'dequeueInlinedCSS'], 999);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta variables CSS criticas
|
||||
*/
|
||||
public function injectVariables(): void
|
||||
{
|
||||
$css = $this->cache->get('variables');
|
||||
|
||||
if (empty($css)) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<!-- TIPO 4: Variables CSS criticas -->' . "\n" .
|
||||
'<style id="roi-critical-variables">%s</style>' . "\n",
|
||||
$css
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta media queries criticas
|
||||
*/
|
||||
public function injectResponsive(): void
|
||||
{
|
||||
$css = $this->cache->get('responsive');
|
||||
|
||||
if (empty($css)) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<!-- TIPO 4: Responsive critico -->' . "\n" .
|
||||
'<style id="roi-critical-responsive">%s</style>' . "\n",
|
||||
$css
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deshabilita enqueue de archivos que ahora estan inline
|
||||
*/
|
||||
public function dequeueInlinedCSS(): void
|
||||
{
|
||||
// Variables ya inline - no cargar archivo externo
|
||||
if ($this->cache->has('variables')) {
|
||||
wp_dequeue_style('roi-variables');
|
||||
wp_deregister_style('roi-variables');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ final class CustomCSSInjector
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// CSS crítico: priority 2 (después de componentes, antes de theme-settings)
|
||||
add_action('wp_head', [$this, 'injectCriticalCSS'], 2);
|
||||
// CSS crítico: priority 3 (después de TIPO 4 responsive P:2, antes de theme-settings P:5)
|
||||
// NOTA: Cambiado de P:2 a P:3 para evitar colision con roi-critical-responsive
|
||||
add_action('wp_head', [$this, 'injectCriticalCSS'], 3);
|
||||
|
||||
// CSS diferido: priority alta en footer
|
||||
add_action('wp_footer', [$this, 'injectDeferredCSS'], 10);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\LazyCSSLoader\Infrastructure\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para registrar CSS lazy (TIPO 5)
|
||||
*
|
||||
* Cumple DIP: permite inyeccion de dependencias y testing
|
||||
*
|
||||
* @package ROITheme\Public\LazyCSSLoader\Infrastructure\Contracts
|
||||
*/
|
||||
interface LazyCSSRegistrarInterface
|
||||
{
|
||||
/**
|
||||
* Registra hooks de WordPress para lazy loading
|
||||
*/
|
||||
public function register(): void;
|
||||
|
||||
/**
|
||||
* Remueve CSS que ahora es TIPO 5 del enqueue normal
|
||||
*/
|
||||
public function dequeueType5CSS(): void;
|
||||
|
||||
/**
|
||||
* Encola el loader JavaScript con configuracion
|
||||
*/
|
||||
public function enqueueLoader(): void;
|
||||
|
||||
/**
|
||||
* Agrega fallback noscript para accesibilidad
|
||||
*/
|
||||
public function addNoscriptFallback(): void;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\LazyCSSLoader\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Public\LazyCSSLoader\Infrastructure\Contracts\LazyCSSRegistrarInterface;
|
||||
|
||||
/**
|
||||
* Registra CSS para carga lazy (TIPO 5)
|
||||
*
|
||||
* Separa CSS de TIPO 2 (diferido inmediato) vs TIPO 5 (lazy loading)
|
||||
*
|
||||
* CSS incluidos en TIPO 5:
|
||||
* - Animaciones: requestIdleCallback (2s timeout)
|
||||
* - Print: beforeprint event
|
||||
*
|
||||
* @package ROITheme\Public\LazyCSSLoader\Infrastructure\Services
|
||||
*/
|
||||
final class LazyCSSRegistrar implements LazyCSSRegistrarInterface
|
||||
{
|
||||
/**
|
||||
* CSS que se mueve de TIPO 2 a TIPO 5
|
||||
*
|
||||
* @var array<int, array{id: string, path: string, trigger: string, handle: string}>
|
||||
*/
|
||||
private const LAZY_CSS_FILES = [
|
||||
[
|
||||
'id' => 'animations',
|
||||
'path' => '/Assets/Css/css-global-animations.css',
|
||||
'trigger' => 'idle',
|
||||
'handle' => 'roi-animations',
|
||||
],
|
||||
[
|
||||
'id' => 'print',
|
||||
'path' => '/Assets/Css/css-global-print.css',
|
||||
'trigger' => 'print',
|
||||
'handle' => 'roi-print',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Timeout en ms para requestIdleCallback
|
||||
*/
|
||||
private const IDLE_TIMEOUT = 2000;
|
||||
|
||||
/**
|
||||
* Registra hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Dequeue CSS que ahora es TIPO 5
|
||||
add_action('wp_enqueue_scripts', [$this, 'dequeueType5CSS'], 999);
|
||||
|
||||
// Agregar script loader y configuracion
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueueLoader'], 20);
|
||||
|
||||
// Agregar fallback noscript
|
||||
add_action('wp_head', [$this, 'addNoscriptFallback'], 99);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remueve CSS que ahora es TIPO 5 del enqueue normal
|
||||
*/
|
||||
public function dequeueType5CSS(): void
|
||||
{
|
||||
foreach (self::LAZY_CSS_FILES as $file) {
|
||||
wp_dequeue_style($file['handle']);
|
||||
wp_deregister_style($file['handle']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encola el loader JavaScript con configuracion
|
||||
*/
|
||||
public function enqueueLoader(): void
|
||||
{
|
||||
$loaderPath = get_template_directory() . '/Assets/Js/lazy-css-loader.js';
|
||||
|
||||
// Solo encolar si el archivo existe
|
||||
if (!file_exists($loaderPath)) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme TIPO 5: lazy-css-loader.js not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'roi-lazy-css-loader',
|
||||
get_template_directory_uri() . '/Assets/Js/lazy-css-loader.js',
|
||||
[],
|
||||
ROI_VERSION,
|
||||
true // En footer
|
||||
);
|
||||
|
||||
// Pasar configuracion al JS
|
||||
wp_localize_script('roi-lazy-css-loader', 'roiLazyCSSConfig', [
|
||||
'baseUrl' => get_template_directory_uri(),
|
||||
'version' => ROI_VERSION,
|
||||
'idleTimeout' => self::IDLE_TIMEOUT,
|
||||
'cssFiles' => self::LAZY_CSS_FILES,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega fallback noscript para accesibilidad
|
||||
*/
|
||||
public function addNoscriptFallback(): void
|
||||
{
|
||||
echo '<noscript>' . "\n";
|
||||
|
||||
foreach (self::LAZY_CSS_FILES as $file) {
|
||||
$href = esc_url(get_template_directory_uri() . $file['path'] . '?ver=' . ROI_VERSION);
|
||||
$media = $file['trigger'] === 'print' ? 'print' : 'all';
|
||||
|
||||
printf(
|
||||
'<link rel="stylesheet" href="%s" media="%s">' . "\n",
|
||||
$href,
|
||||
$media
|
||||
);
|
||||
}
|
||||
|
||||
echo '</noscript>' . "\n";
|
||||
}
|
||||
}
|
||||
73
bin/generate-critical-css.php
Normal file
73
bin/generate-critical-css.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* WP-CLI Command para generar CSS critico TIPO 4
|
||||
*
|
||||
* Uso:
|
||||
* wp roi-theme generate-critical-css
|
||||
* wp roi-theme generate-critical-css --clear
|
||||
*
|
||||
* @package ROITheme
|
||||
*/
|
||||
|
||||
if (!defined('WP_CLI')) {
|
||||
return;
|
||||
}
|
||||
|
||||
use ROITheme\Public\CriticalCSS\Infrastructure\Cache\CriticalCSSFileCache;
|
||||
use ROITheme\Public\CriticalCSS\Infrastructure\Services\CriticalCSSExtractor;
|
||||
use ROITheme\Public\CriticalCSS\Application\UseCases\GetCriticalCSSUseCase;
|
||||
|
||||
/**
|
||||
* Comandos para gestionar CSS critico TIPO 4
|
||||
*/
|
||||
class ROI_CriticalCSS_Command
|
||||
{
|
||||
/**
|
||||
* Genera CSS critico para todas las paginas
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--clear]
|
||||
* : Limpia cache antes de regenerar
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp roi-theme generate-critical-css
|
||||
* wp roi-theme generate-critical-css --clear
|
||||
*
|
||||
* @when after_wp_load
|
||||
*/
|
||||
public function __invoke($args, $assoc_args)
|
||||
{
|
||||
$cache = new CriticalCSSFileCache();
|
||||
$extractor = new CriticalCSSExtractor($cache);
|
||||
$useCase = new GetCriticalCSSUseCase($cache, $extractor);
|
||||
|
||||
// Limpiar cache si se solicita
|
||||
if (isset($assoc_args['clear'])) {
|
||||
$cache->clear();
|
||||
WP_CLI::log('Cache limpiado.');
|
||||
}
|
||||
|
||||
WP_CLI::log('Generando CSS critico TIPO 4...');
|
||||
|
||||
$results = $useCase->regenerate();
|
||||
|
||||
if (empty($results)) {
|
||||
WP_CLI::warning('No se genero CSS critico. Verifica que existan los archivos fuente.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($results as $type => $size) {
|
||||
WP_CLI::success(sprintf(
|
||||
'%s.critical.css: %s bytes',
|
||||
$type,
|
||||
number_format($size)
|
||||
));
|
||||
}
|
||||
|
||||
WP_CLI::success('CSS critico TIPO 4 generado exitosamente.');
|
||||
}
|
||||
}
|
||||
|
||||
WP_CLI::add_command('roi-theme generate-critical-css', 'ROI_CriticalCSS_Command');
|
||||
@@ -338,6 +338,64 @@ if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme: Bootstrap completed successfully');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 5.4. CRITICAL CSS TIPO 4 (CSS Below-the-fold)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Bootstrap CriticalCSS TIPO 4
|
||||
*
|
||||
* Inyecta CSS critico (variables, responsive) inline en wp_head
|
||||
* antes de cualquier otro CSS para evitar FOUC.
|
||||
*
|
||||
* Incluye auto-regeneracion: si los archivos fuente cambian,
|
||||
* el cache se regenera automaticamente sin intervencion manual.
|
||||
* Costo: ~0.1ms (2 llamadas a filemtime())
|
||||
*/
|
||||
if (!is_admin()) {
|
||||
$criticalCSSCache = new \ROITheme\Public\CriticalCSS\Infrastructure\Cache\CriticalCSSFileCache();
|
||||
$criticalCSSExtractor = new \ROITheme\Public\CriticalCSS\Infrastructure\Services\CriticalCSSExtractor(
|
||||
$criticalCSSCache
|
||||
);
|
||||
|
||||
// Auto-regenerar si archivos fuente cambiaron
|
||||
if ($criticalCSSExtractor->needsRegeneration()) {
|
||||
$criticalCSSExtractor->generateAll();
|
||||
}
|
||||
|
||||
$criticalCSSInjector = new \ROITheme\Public\CriticalCSS\Infrastructure\Services\CriticalCSSInjector(
|
||||
$criticalCSSCache
|
||||
);
|
||||
$criticalCSSInjector->register();
|
||||
}
|
||||
|
||||
// Registrar comando WP-CLI (util para forzar regeneracion o limpiar cache)
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
require_once get_template_directory() . '/bin/generate-critical-css.php';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 5.5. LAZY CSS LOADER TIPO 5 (CSS No Critico)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Bootstrap LazyCSSLoader TIPO 5
|
||||
*
|
||||
* Carga CSS no critico de forma lazy:
|
||||
* - Animaciones: requestIdleCallback o timeout 2s
|
||||
* - Print: solo cuando usuario va a imprimir (beforeprint event)
|
||||
*
|
||||
* @since 1.0.20
|
||||
*/
|
||||
if (!is_admin()) {
|
||||
$lazyCSSRegistrar = new \ROITheme\Public\LazyCSSLoader\Infrastructure\Services\LazyCSSRegistrar();
|
||||
$lazyCSSRegistrar->register();
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme: TIPO 5 LazyCSSLoader registered');
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 6. INSTALACIÓN DE TABLAS DEL TEMA
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user