PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.2
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
← All changes | includes/modules/Minify/MinifyModule.php +128 -24 1.0.71.3.2 View file →
@@ -30,11 +30,16 @@
30 30 public const VERSION = '1.2.0';
31 31
32 32 public function ui_metadata(): array {
33 33 return array(
34 - 'label' => 'Minify',
35 - 'icon' => 'Wand2',
36 - 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
34 + 'label' => __( 'CSS & JavaScript', 'xspeed' ),
35 + 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
36 + 'icon' => 'Wand2',
37 + 'description' => __( 'Strip whitespace and rewrite enqueued CSS / JS.', 'xspeed' ),
38 + // Host page: Minify (this module) / Critical CSS (Pro) / Unused
39 + // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
40 + // page instead of three sidebar rows (FBS-83633).
41 + 'custom_panel' => 'CssJsPanel',
37 42 );
38 43 }
39 44
40 45 public function settings_schema(): array {
@@ -41,53 +46,57 @@
41 46 return array(
42 47 'minify_html' => array(
43 48 'type' => 'bool',
44 49 'default' => false,
45 - 'label' => 'Minify HTML',
46 - 'description' => 'Strip whitespace and comments from HTML output. Safe on most themes.',
50 + 'label' => __( 'Minify HTML', 'xspeed' ),
51 + // Names the logged-out caveat up front: minification runs on the
52 + // request that WRITES a cache entry, and should_cache() refuses
53 + // logged-in requests — so "view source while logged in" shows
54 + // un-minified HTML and reads as the feature being broken. (#2)
55 + 'description' => __( 'Strip whitespace and comments from HTML output, including inline <style> and <script> blocks. Safe on most themes. Applies to cached (logged-out) responses — view the page in a private window to see the result.', 'xspeed' ),
47 56 ),
48 57 'minify_css' => array(
49 58 'type' => 'bool',
50 59 'default' => false,
51 - 'label' => 'Minify CSS',
52 - 'description' => 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.',
60 + 'label' => __( 'Minify CSS', 'xspeed' ),
61 + 'description' => __( 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.', 'xspeed' ),
53 62 ),
54 63 'minify_js' => array(
55 64 'type' => 'bool',
56 65 'default' => false,
57 - 'label' => 'Minify JavaScript',
58 - 'description' => 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.',
66 + 'label' => __( 'Minify JavaScript', 'xspeed' ),
67 + 'description' => __( 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.', 'xspeed' ),
59 68 ),
60 69 'defer_js' => array(
61 70 'type' => 'bool',
62 71 'default' => false,
63 - 'label' => 'Defer JavaScript',
64 - 'description' => 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.',
72 + 'label' => __( 'Defer JavaScript', 'xspeed' ),
73 + 'description' => __( 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.', 'xspeed' ),
65 74 ),
66 75 'delay_js' => array(
67 76 'type' => 'bool',
68 77 'default' => false,
69 - 'label' => 'Delay JavaScript Until Interaction',
70 - 'description' => 'Postpone script loading until the visitor scrolls, moves the mouse, taps, or presses a key. Drastically improves first paint on script-heavy pages; can break above-the-fold scripted UI — test before leaving on.',
78 + 'label' => __( 'Delay JavaScript Until Interaction', 'xspeed' ),
79 + 'description' => __( 'Postpone script loading until the visitor scrolls, moves the mouse, taps, or presses a key. Drastically improves first paint on script-heavy pages; can break above-the-fold scripted UI — test before leaving on.', 'xspeed' ),
71 80 ),
72 81 'async_css' => array(
73 82 'type' => 'bool',
74 83 'default' => false,
75 - 'label' => 'Load CSS Asynchronously',
76 - 'description' => 'Rewrite stylesheet link tags to load non-blocking via the print-then-all pattern. Pairs well with critical-CSS workflows; can cause a flash of unstyled content if the theme has no critical CSS.',
84 + 'label' => __( 'Load CSS Asynchronously', 'xspeed' ),
85 + 'description' => __( 'Rewrite stylesheet link tags to load non-blocking via the print-then-all pattern. Pairs well with critical-CSS workflows; can cause a flash of unstyled content if the theme has no critical CSS.', 'xspeed' ),
77 86 ),
78 87 'remove_query_strings' => array(
79 88 'type' => 'bool',
80 89 'default' => false,
81 - 'label' => 'Remove Asset Query Strings',
82 - 'description' => 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent.',
90 + 'label' => __( 'Remove Asset Query Strings', 'xspeed' ),
91 + 'description' => __( 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent. Files under wp-content/uploads keep their version — page builders and consent plugins rewrite generated CSS there in place, and ?ver is what tells browsers to refetch it. Plugin, theme and core assets are still stripped: with Browser Cache on, an update reaches returning visitors only when their browser cache expires.', 'xspeed' ),
83 92 ),
84 93 'defer_js_excluded' => array(
85 94 'type' => 'list',
86 95 'default' => array( 'jquery-core', 'jquery-migrate' ),
87 96 'item_type' => 'string',
88 - 'label' => 'Defer / Delay Exclusions',
89 - 'description' => 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.',
97 + 'label' => __( 'Defer / Delay Exclusions', 'xspeed' ),
98 + 'description' => __( 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.', 'xspeed' ),
90 99 // Only relevant once defer OR delay is on — the exclusion list
91 100 // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
92 101 'dependsOn' => array(
93 102 'any' => array(
@@ -95,19 +104,37 @@
95 104 array( 'field' => 'delay_js' ),
96 105 ),
97 106 ),
98 107 ),
108 + 'delay_js_targets' => array(
109 + 'type' => 'list',
110 + 'default' => array(),
111 + 'item_type' => 'string',
112 + 'label' => __( 'Delay Only These Scripts', 'xspeed' ),
113 + 'description' => __( 'Script handles OR URL substrings. When non-empty, matching scripts are delayed, plus the known third-party tags xSpeed recognises on its own (analytics, tag managers, chat widgets, review embeds, error trackers) — so a heavy vendor script is postponed even when it is not listed here. Leave empty to delay all scripts (minus the exclusions above). Handles are the more reliable selector — a URL substring has to match the script\'s original URL, and minification rewrites that to a hashed cache path. One per line.', 'xspeed' ),
114 + 'dependsOn' => array( 'field' => 'delay_js' ),
115 + ),
116 + 'delay_js_timeout' => array(
117 + 'type' => 'int',
118 + 'default' => 8000,
119 + 'min' => 0,
120 + 'max' => 60000,
121 + 'label' => __( 'Delay Failsafe Timeout (ms)', 'xspeed' ),
122 + 'unit' => 'ms',
123 + 'description' => __( 'Load delayed scripts automatically after this many milliseconds when the visitor never interacts. Set to 0 for interaction-only, with no timer: a timer that fires inside a lab tool\'s measurement window loads the "delayed" scripts anyway and inflates the reported TTI. Keep a non-zero value if a delayed script must eventually run for visitors who never scroll, tap, or type.', 'xspeed' ),
124 + 'dependsOn' => array( 'field' => 'delay_js' ),
125 + ),
99 126 'combine_css' => array(
100 127 'type' => 'bool',
101 128 'default' => false,
102 - 'label' => 'Combine CSS Files',
103 - 'description' => 'Concatenate enqueued local stylesheets into a single file (with @import and url(…) paths resolved). External CSS is left alone. Pairs poorly with HTTP/2 push — only enable on HTTP/1.1 hosts.',
129 + 'label' => __( 'Combine CSS Files', 'xspeed' ),
130 + 'description' => __( 'Concatenate enqueued local stylesheets into a single file (with @import and url(…) paths resolved). External CSS is left alone. Pairs poorly with HTTP/2 push — only enable on HTTP/1.1 hosts.', 'xspeed' ),
104 131 ),
105 132 'combine_js' => array(
106 133 'type' => 'bool',
107 134 'default' => false,
108 - 'label' => 'Combine JavaScript Files',
109 - 'description' => 'Concatenate enqueued local scripts into a single file. External scripts + scripts marked async / deferred are left alone. Disable if you hit dependency-order issues; the combiner respects WordPress enqueue order but inline scripts attached via wp_add_inline_script can shift behavior.',
135 + 'label' => __( 'Combine JavaScript Files', 'xspeed' ),
136 + 'description' => __( 'Concatenate enqueued local scripts into a single file. External scripts + scripts marked async / deferred are left alone. Disable if you hit dependency-order issues; the combiner respects WordPress enqueue order but inline scripts attached via wp_add_inline_script can shift behavior.', 'xspeed' ),
110 137 ),
111 138 );
112 139 }
113 140
@@ -167,8 +194,9 @@
167 194 array(
168 195 'name' => 'xspeed minify',
169 196 'callback' => array( $this, 'cli_handler' ),
170 197 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
198 + 'ai_hint' => 'Which CSS/JS optimizations are active (minify, combine, defer, delay, async)? Use for questions about render-blocking resources, unminified assets in PageSpeed, or when JavaScript broke after enabling optimizations.',
171 199 'synopsis' => array(
172 200 array(
173 201 'type' => 'positional',
174 202 'name' => 'action',
@@ -188,8 +216,26 @@
188 216 * 2. Instantiate the v1 Minifier engine; it now reads from
189 217 * Settings_Manager::get('minify') via its updated read path.
190 218 */
191 219 public function boot(): void {
220 + /*
221 + * Deferred to `init`: both calls below read this module's settings,
222 + * which builds settings_schema(), whose labels go through __().
223 + * boot() runs on `plugins_loaded`, before `after_setup_theme` — the
224 + * earliest point WordPress 6.7+ considers safe to translate — so doing
225 + * it here fires _load_textdomain_just_in_time on every request and
226 + * resolves those labels against an unloaded domain.
227 + *
228 + * Every filter LegacyMinifier registers fires after `init`, so running
229 + * one hook later is equivalent.
230 + */
231 + add_action( 'init', array( $this, 'boot_on_init' ) );
232 + }
233 +
234 + /**
235 + * The real boot body — see boot() for why it runs on `init`.
236 + */
237 + public function boot_on_init(): void {
192 238 $this->seed_from_legacy_if_needed();
193 239 new LegacyMinifier();
194 240 }
195 241
@@ -198,8 +244,45 @@
198 244 // run more than once.
199 245 $this->seed_from_legacy_if_needed();
200 246 }
201 247
248 + /**
249 + * Say so when HTML minification is switched on but suppressed.
250 + *
251 + * `Minifier::skip_reason()` was consulted only by `wp xspeed minify status`
252 + * — the dashboard read "on" while `minify_html()` returned its input
253 + * untouched, so the feature looked broken rather than paused. A field
254 + * report showed a live site with `minify_html: on` and 3,856 indented lines
255 + * delivered, and nothing anywhere explaining the contradiction. (#2)
256 + *
257 + * @return array<int,array<string,mixed>>
258 + */
259 + public function ui_notices(): array {
260 + $opts = $this->get_settings();
261 + if ( empty( $opts['minify_html'] ) ) {
262 + return array();
263 + }
264 +
265 + $reason = LegacyMinifier::skip_reason();
266 + if ( '' === $reason ) {
267 + return array();
268 + }
269 +
270 + // Two different causes, two different fixes — naming the wrong one
271 + // sends the user hunting in the wrong file.
272 + $body = 'wp_debug' === $reason
273 + ? __( 'HTML minification is paused because WP_DEBUG is enabled in wp-config.php. Readable HTML is usually what you want while debugging, so xSpeed leaves the markup alone. Cached pages are served un-minified until WP_DEBUG is turned off.', 'xspeed' )
274 + : __( 'HTML minification is paused because a plugin or theme is returning true from the xspeed_skip_minify filter. Cached pages are served un-minified until that filter stops suppressing it.', 'xspeed' );
275 +
276 + return array(
277 + array(
278 + 'tone' => 'info',
279 + 'title' => __( 'HTML minification is on but currently paused', 'xspeed' ),
280 + 'body' => $body,
281 + ),
282 + );
283 + }
284 +
202 285 private function seed_from_legacy_if_needed(): void {
203 286 $existing = get_option( 'xspeed_module_minify', null );
204 287 if ( null !== $existing ) {
205 288 return;
@@ -227,9 +310,21 @@
227 310 $action = $args[0] ?? 'status';
228 311
229 312 if ( 'status' === $action ) {
230 313 $opts = Settings_Manager::get( self::SLUG );
231 - \WP_CLI::log( sprintf( 'minify_html: %s', $opts['minify_html'] ? 'on' : 'off' ) );
314 + // A bare "on" is a lie when the skip guard is active: the
315 + // setting is stored, but Minifier::minify_html() returns its
316 + // input untouched and the delivered HTML is unchanged. Say so
317 + // on the same line, so the contradiction can never be read as
318 + // "minify is broken".
319 + $skip = \XSpeed\Minifier::skip_reason();
320 + $html_state = $opts['minify_html'] ? 'on' : 'off';
321 + if ( $opts['minify_html'] && '' !== $skip ) {
322 + $html_state .= ( 'wp_debug' === $skip )
323 + ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
324 + : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
325 + }
326 + \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
232 327 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
233 328 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
234 329 return;
235 330 }
@@ -240,6 +335,15 @@
240 335 return;
241 336 }
242 337
243 338 \WP_CLI::error( "Unknown action: $action" );
339 + }
340 +
341 + /**
342 + * Minify has no master switch -- it is on when any of minify_html /
343 + * minify_css / minify_js / defer_js / delay_js / async_css /
344 + * remove_query_strings is set. (#363)
345 + */
346 + public function is_active(): ?bool {
347 + return $this->any_bool_flag_on();
244 348 }
245 349 }