,
* deferring the full stylesheets to load asynchronously.
*
* @package Upress\EzCache
*/
namespace Upress\EzCache;
class CriticalCSS {
/** Storage directory for generated critical CSS files */
const STORAGE_DIR = 'ezcache/critical-css/';
/** Diagnostic API endpoint for critical CSS extraction */
const DIAG_PATH = '/critical-css';
/** Viewport height used for above-the-fold extraction (px) */
const VIEWPORT_HEIGHT = 1200;
private static $instance;
private $settings;
private $storage_path;
private function __construct() {
$this->settings = Settings::get_settings();
$this->storage_path = WP_CONTENT_DIR . '/' . self::STORAGE_DIR;
}
public static function instance() {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Register hooks
*/
public function register() {
if ( empty( $this->settings->enable_critical_css ) ) {
return;
}
if ( ! PremiumFeatures::is_premium() ) {
return;
}
add_action( 'wp_head', [ $this, 'inject_critical_css' ], 1 );
add_filter( 'style_loader_tag', [ $this, 'defer_stylesheet' ], 10, 4 );
}
/**
* Inject stored critical CSS for the current URL.
*/
public function inject_critical_css() {
$css = $this->get_critical_css( $this->get_url_pattern() );
if ( ! $css ) { return; }
echo '' . "\n";
}
/**
* Convert stylesheet link tags to async load + noscript fallback.
*/
public function defer_stylesheet( $tag, $handle, $href, $media ) {
// Don't defer admin or login stylesheets
if ( is_admin() ) { return $tag; }
$media_attr = $media ?: 'all';
$deferred = sprintf(
'' . "\n" .
'' . "\n",
esc_url( $href ), esc_attr( $media_attr ),
esc_url( $href ), esc_attr( $media_attr )
);
return $deferred;
}
/**
* Generate critical CSS for a URL using the diagnostic API.
*
* @param string $page_url Full URL of the page.
* @param string $pattern Storage pattern key (home, post, page, archive).
* @return array{success:bool, message:string, css:string}
*/
public function generate( $page_url, $pattern = null ) {
if ( ! PremiumFeatures::is_premium() ) {
return [ 'success' => false, 'message' => 'Premium required' ];
}
$diag_url = $this->get_diag_api_url();
if ( ! $diag_url ) {
return [ 'success' => false, 'message' => 'Diagnostic API not configured' ];
}
$response = wp_remote_post( $diag_url . self::DIAG_PATH, [
'timeout' => 30,
'body' => wp_json_encode( [
'url' => $page_url,
'viewport_height' => self::VIEWPORT_HEIGHT,
] ),
'headers' => [ 'Content-Type' => 'application/json' ],
'sslverify' => false,
] );
if ( is_wp_error( $response ) ) {
return [ 'success' => false, 'message' => $response->get_error_message() ];
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['css'] ) ) {
return [ 'success' => false, 'message' => 'No CSS returned from API' ];
}
$pattern = $pattern ?: $this->get_url_pattern();
$this->save_critical_css( $pattern, $body['css'] );
return [ 'success' => true, 'message' => 'Critical CSS generated', 'css' => $body['css'] ];
}
/**
* Get stored critical CSS for a URL pattern.
*
* @param string $pattern
* @return string|false
*/
public function get_critical_css( $pattern ) {
$file = $this->storage_path . sanitize_file_name( $pattern ) . '.css';
if ( file_exists( $file ) ) {
return file_get_contents( $file );
}
return false;
}
/**
* Save critical CSS for a pattern.
*
* @param string $pattern
* @param string $css
*/
public function save_critical_css( $pattern, $css ) {
wp_mkdir_p( $this->storage_path );
$file = $this->storage_path . sanitize_file_name( $pattern ) . '.css';
file_put_contents( $file, wp_strip_all_tags( $css ) );
}
/**
* Delete all stored critical CSS files.
*/
public function clear_all() {
if ( ! is_dir( $this->storage_path ) ) { return; }
$files = glob( $this->storage_path . '*.css' );
if ( $files ) {
foreach ( $files as $f ) { @unlink( $f ); }
}
}
/**
* Get a list of patterns that have generated critical CSS.
*
* @return array
*/
public function get_patterns() {
if ( ! is_dir( $this->storage_path ) ) { return []; }
$files = glob( $this->storage_path . '*.css' ) ?: [];
$patterns = [];
foreach ( $files as $f ) {
$patterns[] = basename( $f, '.css' );
}
return $patterns;
}
/**
* Determine a canonical pattern key for the current URL.
*
* @return string e.g. 'home', 'post', 'page', 'archive'
*/
public function get_url_pattern() {
if ( is_front_page() || is_home() ) { return 'home'; }
if ( is_single() ) { return 'post'; }
if ( is_page() ) { return 'page'; }
if ( is_archive() ) { return 'archive'; }
return 'default';
}
/**
* Build the diagnostic API base URL from plugin settings.
*
* @return string|false
*/
private function get_diag_api_url() {
// Try to find the local diagnostic API (Booting AI diagnostic server).
$diag_url = \get_option( 'ezcache_diag_api_url' );
if ( $diag_url ) {
return rtrim( $diag_url, '/' );
}
// Default: localhost diagnostic service
return 'http://localhost:8787';
}
}