Files
roi-theme/Inc/critical-css.php
FrankZamora 0b34317cc6 fix(critical-css): Increase badge min-height 32px→36px and add APU tables critical CSS
- Fix hero badge min-height from 32px to 36px (actual height is 35px)
- Add vertical-align: middle to badge for better alignment
- Add critical CSS for .analisis and .desglose tables:
  - table-layout: fixed to prevent reflow
  - overflow-x: auto for mobile horizontal scroll
- These changes target CLS 0.117 caused by hero badges

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 12:58:38 -06:00

438 lines
12 KiB
PHP

<?php
/**
* Critical CSS Generator and Inline Loader
*
* This file provides functionality to inline critical CSS for above-the-fold content,
* improving First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
*
* IMPORTANT: This feature is DISABLED by default. Enable it via Customizer.
*
* How it works:
* 1. When enabled, critical CSS is inlined in the <head>
* 2. Main stylesheet is loaded asynchronously after page load
* 3. Improves Core Web Vitals by reducing render-blocking CSS
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Check if Critical CSS is enabled
*
* @since 1.0.0
* @return bool
*/
function roi_is_critical_css_enabled() {
return get_theme_mod( 'roi_enable_critical_css', false );
}
/**
* Get Critical CSS Content
*
* Returns the critical CSS for the current page type.
* You can customize this based on page types (home, single, archive, etc.)
*
* @since 1.0.0
* @return string Critical CSS content
*/
function roi_get_critical_css() {
// Define critical CSS based on page type
$critical_css = '';
// Get transient to cache critical CSS
$transient_key = 'roi_critical_css_' . roi_get_page_type();
$cached_css = get_transient( $transient_key );
if ( false !== $cached_css ) {
return $cached_css;
}
// Generate critical CSS based on page type
if ( is_front_page() || is_home() ) {
$critical_css = roi_get_home_critical_css();
} elseif ( is_single() ) {
$critical_css = roi_get_single_critical_css();
} elseif ( is_archive() || is_category() || is_tag() ) {
$critical_css = roi_get_archive_critical_css();
} else {
$critical_css = roi_get_default_critical_css();
}
// Cache for 24 hours
set_transient( $transient_key, $critical_css, DAY_IN_SECONDS );
return $critical_css;
}
/**
* Get current page type for caching
*
* @since 1.0.0
* @return string Page type identifier
*/
function roi_get_page_type() {
if ( is_front_page() ) {
return 'home';
} elseif ( is_single() ) {
return 'single';
} elseif ( is_archive() ) {
return 'archive';
} elseif ( is_search() ) {
return 'search';
} elseif ( is_404() ) {
return '404';
} else {
return 'page';
}
}
/**
* Critical CSS for Homepage
*
* @since 1.0.0
* @return string
*/
function roi_get_home_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
.site-branding{display:flex;align-items:center}
.site-title{margin:0;font-size:1.5rem;font-weight:700}
/* Hero Section */
.hero-section{padding:3rem 1rem;text-align:center;background:#f8f9fa}
.hero-title{font-size:2.5rem;margin:0 0 1rem;font-weight:700;line-height:1.2}
.hero-description{font-size:1.125rem;color:#666;max-width:600px;margin:0 auto}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Featured Posts Grid */
.featured-posts{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2rem;margin:2rem 0}
.post-card{background:#fff;border:1px solid #e5e5e5;border-radius:8px;overflow:hidden;transition:transform .2s}
.post-card:hover{transform:translateY(-4px)}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Single Post/Page
*
* @since 1.0.0
* @return string
*/
function roi_get_single_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
h1,h2,h3,h4,h5,h6{margin:1.5rem 0 1rem;font-weight:700;line-height:1.3}
h1{font-size:2.5rem}
h2{font-size:2rem}
h3{font-size:1.5rem}
p{margin:0 0 1.5rem}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Article */
.entry-header{margin:2rem 0}
.entry-title{font-size:2.5rem;margin:0 0 1rem;line-height:1.2}
.entry-meta{color:#666;font-size:.875rem}
.entry-content{max-width:800px;margin:0 auto;font-size:1.125rem;line-height:1.8}
.featured-image{margin:2rem 0}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Archive Pages
*
* @since 1.0.0
* @return string
*/
function roi_get_archive_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Archive Header */
.archive-header{padding:2rem 1rem;background:#f8f9fa;border-bottom:1px solid #e5e5e5}
.archive-title{margin:0;font-size:2rem;font-weight:700}
.archive-description{margin:1rem 0 0;color:#666;font-size:1.125rem}
/* Posts Grid */
.posts-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2rem;margin:2rem 0}
.post-card{background:#fff;border:1px solid #e5e5e5;border-radius:8px;overflow:hidden}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Default Pages
*
* @since 1.0.0
* @return string
*/
function roi_get_default_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Content */
.site-content{min-height:50vh;padding:2rem 0}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Output Critical CSS inline in head
*
* @since 1.0.0
*/
function roi_output_critical_css() {
if ( ! roi_is_critical_css_enabled() ) {
return;
}
$critical_css = roi_get_critical_css();
if ( empty( $critical_css ) ) {
return;
}
// Minify CSS (remove extra whitespace and newlines)
$critical_css = preg_replace( '/\s+/', ' ', $critical_css );
$critical_css = trim( $critical_css );
// Output inline critical CSS
echo '<style id="roi-critical-css">' . $critical_css . '</style>' . "\n";
}
add_action( 'wp_head', 'roi_output_critical_css', 1 );
/**
* Load main stylesheet asynchronously when critical CSS is enabled
*
* @since 1.0.0
*/
function roi_async_main_stylesheet() {
if ( ! roi_is_critical_css_enabled() ) {
return;
}
// Dequeue main stylesheet to prevent render-blocking
wp_dequeue_style( 'roi-theme-style' );
// Enqueue with media="print" and onload to load asynchronously
wp_enqueue_style(
'roi-theme-style-async',
get_stylesheet_uri(),
array(),
ROI_VERSION,
'print'
);
// Add onload attribute to switch media to "all"
add_filter( 'style_loader_tag', 'roi_add_async_attribute', 10, 2 );
}
add_action( 'wp_enqueue_scripts', 'roi_async_main_stylesheet', 999 );
/**
* Add async loading attributes to stylesheet
*
* @since 1.0.0
* @param string $html The link tag for the enqueued style.
* @param string $handle The style's registered handle.
* @return string Modified link tag
*/
function roi_add_async_attribute( $html, $handle ) {
if ( 'roi-theme-style-async' !== $handle ) {
return $html;
}
// Add onload attribute to switch media to "all"
$html = str_replace(
"media='print'",
"media='print' onload=\"this.media='all'\"",
$html
);
// Add noscript fallback
$html .= '<noscript><link rel="stylesheet" href="' . get_stylesheet_uri() . '"></noscript>';
return $html;
}
/**
* Add Customizer setting for Critical CSS
*
* @since 1.0.0
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/
function roi_critical_css_customizer( $wp_customize ) {
// Add Performance section
$wp_customize->add_section(
'roi_performance',
array(
'title' => __( 'Performance Optimization', 'roi-theme' ),
'priority' => 130,
)
);
// Critical CSS Enable/Disable
$wp_customize->add_setting(
'roi_enable_critical_css',
array(
'default' => false,
'sanitize_callback' => 'roi_sanitize_checkbox',
'transport' => 'refresh',
)
);
$wp_customize->add_control(
'roi_enable_critical_css',
array(
'label' => __( 'Enable Critical CSS', 'roi-theme' ),
'description' => __( 'Inline critical CSS and load main stylesheet asynchronously. This can improve Core Web Vitals but may cause a flash of unstyled content (FOUC). Test thoroughly before enabling in production.', 'roi-theme' ),
'section' => 'roi_performance',
'type' => 'checkbox',
)
);
}
add_action( 'customize_register', 'roi_critical_css_customizer' );
/**
* Clear critical CSS cache when theme is updated
*
* @since 1.0.0
*/
function roi_clear_critical_css_cache() {
$page_types = array( 'home', 'single', 'archive', 'search', '404', 'page' );
foreach ( $page_types as $type ) {
delete_transient( 'roi_critical_css_' . $type );
}
}
add_action( 'after_switch_theme', 'roi_clear_critical_css_cache' );
add_action( 'customize_save_after', 'roi_clear_critical_css_cache' );
/**
* Output Hero Section Critical CSS
*
* This CSS is ALWAYS injected in the <head> to prevent CLS (Cumulative Layout Shift)
* by reserving space for the hero section BEFORE it renders.
*
* The min-height values match the actual rendered height (~260px on desktop)
* to prevent any layout shift when the dynamic CSS loads.
*
* @since 1.0.21
* @see Public/Hero/Infrastructure/Ui/HeroRenderer.php
*/
function roi_output_hero_critical_css() {
// Only output on single posts where hero is displayed
if ( ! is_single() ) {
return;
}
// Check if hero is enabled (read from database)
$is_enabled = roi_get_component_setting( 'hero', 'visibility', 'is_enabled', true );
if ( ! $is_enabled ) {
return;
}
$critical_css = '
/* Hero Critical CSS - Prevent CLS */
.hero-section {
min-height: 260px;
background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
padding: 3rem 0;
margin-bottom: 1.5rem;
box-sizing: border-box;
}
.hero-section__title {
min-height: 3.5rem;
margin-bottom: 0;
}
.hero-section__badge {
min-height: 36px;
display: inline-block;
vertical-align: middle;
}
@media (max-width: 767.98px) {
.hero-section {
min-height: 200px;
}
.hero-section__title {
min-height: 2.5rem;
}
}
/* APU Tables Critical CSS - Prevent table reflow */
.analisis table, .desglose table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.analisis, .desglose {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
';
// Minify CSS
$critical_css = preg_replace( '/\s+/', ' ', $critical_css );
$critical_css = trim( $critical_css );
echo '<style id="roi-hero-critical-css">' . $critical_css . '</style>' . "\n";
}
add_action( 'wp_head', 'roi_output_hero_critical_css', 2 );