PluginProbe
ezCache / 2.5.2
ezCache v2.5.2
2.6.5 2.6.4 2.6.2 2.6.3 2.6.1 2.6.0 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5 2.2.1 2.2.2 trunk 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.3.1 1.3.10 1.3.11 All 49 releases
ezcache / includes / CriticalCSS.php

CriticalCSS.php in ezCache 2.5.2, at includes/CriticalCSS.php

202 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Critical CSS Generator
4 *
5 * Generates above-the-fold CSS and inlines it in the page <head>,
6 * deferring the full stylesheets to load asynchronously.
7 *
8 * @package Upress\EzCache
9 */
10 namespace Upress\EzCache;
11
12 class CriticalCSS {
13
14 /** Storage directory for generated critical CSS files */
15 const STORAGE_DIR = 'ezcache/critical-css/';
16
17 /** Diagnostic API endpoint for critical CSS extraction */
18 const DIAG_PATH = '/critical-css';
19
20 /** Viewport height used for above-the-fold extraction (px) */
21 const VIEWPORT_HEIGHT = 1200;
22
23 private static $instance;
24 private $settings;
25 private $storage_path;
26
27 private function __construct() {
28 $this->settings = Settings::get_settings();
29 $this->storage_path = WP_CONTENT_DIR . '/' . self::STORAGE_DIR;
30 }
31
32 public static function instance() {
33 if ( ! self::$instance ) {
34 self::$instance = new self();
35 }
36 return self::$instance;
37 }
38
39 /**
40 * Register hooks
41 */
42 public function register() {
43 if ( empty( $this->settings->enable_critical_css ) ) {
44 return;
45 }
46
47 if ( ! PremiumFeatures::is_premium() ) {
48 return;
49 }
50
51 add_action( 'wp_head', [ $this, 'inject_critical_css' ], 1 );
52 add_filter( 'style_loader_tag', [ $this, 'defer_stylesheet' ], 10, 4 );
53 }
54
55 /**
56 * Inject stored critical CSS for the current URL.
57 */
58 public function inject_critical_css() {
59 $css = $this->get_critical_css( $this->get_url_pattern() );
60 if ( ! $css ) { return; }
61 echo '<style id="ezcache-critical-css">' . wp_strip_all_tags( $css ) . '</style>' . "\n";
62 }
63
64 /**
65 * Convert stylesheet link tags to async load + noscript fallback.
66 */
67 public function defer_stylesheet( $tag, $handle, $href, $media ) {
68 // Don't defer admin or login stylesheets
69 if ( is_admin() ) { return $tag; }
70 $media_attr = $media ?: 'all';
71 $deferred = sprintf(
72 '<link rel="preload" href="%s" as="style" onload="this.onload=null;this.rel=\'stylesheet\';this.media=\'%s\'">' . "\n" .
73 '<noscript><link rel="stylesheet" href="%s" media="%s"></noscript>' . "\n",
74 esc_url( $href ), esc_attr( $media_attr ),
75 esc_url( $href ), esc_attr( $media_attr )
76 );
77 return $deferred;
78 }
79
80 /**
81 * Generate critical CSS for a URL using the diagnostic API.
82 *
83 * @param string $page_url Full URL of the page.
84 * @param string $pattern Storage pattern key (home, post, page, archive).
85 * @return array{success:bool, message:string, css:string}
86 */
87 public function generate( $page_url, $pattern = null ) {
88 if ( ! PremiumFeatures::is_premium() ) {
89 return [ 'success' => false, 'message' => 'Premium required' ];
90 }
91
92 $diag_url = $this->get_diag_api_url();
93 if ( ! $diag_url ) {
94 return [ 'success' => false, 'message' => 'Diagnostic API not configured' ];
95 }
96
97 $response = wp_remote_post( $diag_url . self::DIAG_PATH, [
98 'timeout' => 30,
99 'body' => wp_json_encode( [
100 'url' => $page_url,
101 'viewport_height' => self::VIEWPORT_HEIGHT,
102 ] ),
103 'headers' => [ 'Content-Type' => 'application/json' ],
104 'sslverify' => false,
105 ] );
106
107 if ( is_wp_error( $response ) ) {
108 return [ 'success' => false, 'message' => $response->get_error_message() ];
109 }
110
111 $body = json_decode( wp_remote_retrieve_body( $response ), true );
112 if ( empty( $body['css'] ) ) {
113 return [ 'success' => false, 'message' => 'No CSS returned from API' ];
114 }
115
116 $pattern = $pattern ?: $this->get_url_pattern();
117 $this->save_critical_css( $pattern, $body['css'] );
118
119 return [ 'success' => true, 'message' => 'Critical CSS generated', 'css' => $body['css'] ];
120 }
121
122 /**
123 * Get stored critical CSS for a URL pattern.
124 *
125 * @param string $pattern
126 * @return string|false
127 */
128 public function get_critical_css( $pattern ) {
129 $file = $this->storage_path . sanitize_file_name( $pattern ) . '.css';
130 if ( file_exists( $file ) ) {
131 return file_get_contents( $file );
132 }
133 return false;
134 }
135
136 /**
137 * Save critical CSS for a pattern.
138 *
139 * @param string $pattern
140 * @param string $css
141 */
142 public function save_critical_css( $pattern, $css ) {
143 wp_mkdir_p( $this->storage_path );
144 $file = $this->storage_path . sanitize_file_name( $pattern ) . '.css';
145 file_put_contents( $file, wp_strip_all_tags( $css ) );
146 }
147
148 /**
149 * Delete all stored critical CSS files.
150 */
151 public function clear_all() {
152 if ( ! is_dir( $this->storage_path ) ) { return; }
153 $files = glob( $this->storage_path . '*.css' );
154 if ( $files ) {
155 foreach ( $files as $f ) { @unlink( $f ); }
156 }
157 }
158
159 /**
160 * Get a list of patterns that have generated critical CSS.
161 *
162 * @return array
163 */
164 public function get_patterns() {
165 if ( ! is_dir( $this->storage_path ) ) { return []; }
166 $files = glob( $this->storage_path . '*.css' ) ?: [];
167 $patterns = [];
168 foreach ( $files as $f ) {
169 $patterns[] = basename( $f, '.css' );
170 }
171 return $patterns;
172 }
173
174 /**
175 * Determine a canonical pattern key for the current URL.
176 *
177 * @return string e.g. 'home', 'post', 'page', 'archive'
178 */
179 public function get_url_pattern() {
180 if ( is_front_page() || is_home() ) { return 'home'; }
181 if ( is_single() ) { return 'post'; }
182 if ( is_page() ) { return 'page'; }
183 if ( is_archive() ) { return 'archive'; }
184 return 'default';
185 }
186
187 /**
188 * Build the diagnostic API base URL from plugin settings.
189 *
190 * @return string|false
191 */
192 private function get_diag_api_url() {
193 // Try to find the local diagnostic API (Booting AI diagnostic server).
194 $diag_url = \get_option( 'ezcache_diag_api_url' );
195 if ( $diag_url ) {
196 return rtrim( $diag_url, '/' );
197 }
198 // Default: localhost diagnostic service
199 return 'http://localhost:8787';
200 }
201 }
202