PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 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 All 29 releases
← All changes | includes/modules/Minify/MinifyModule.php +123 -24 1.1.01.3.3 View file →
@@ -30,12 +30,12 @@
30 30 public const VERSION = '1.2.0';
31 31
32 32 public function ui_metadata(): array {
33 33 return array(
34 - 'label' => 'CSS & JavaScript',
35 - 'tab_label' => 'Minify', // its own tab on the CSS & JavaScript page
34 + 'label' => __( 'CSS & JavaScript', 'xspeed' ),
35 + 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
36 36 'icon' => 'Wand2',
37 - 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
37 + 'description' => __( 'Strip whitespace and rewrite enqueued CSS / JS.', 'xspeed' ),
38 38 // Host page: Minify (this module) / Critical CSS (Pro) / Unused
39 39 // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
40 40 // page instead of three sidebar rows (FBS-83633).
41 41 'custom_panel' => 'CssJsPanel',
@@ -46,53 +46,57 @@
46 46 return array(
47 47 'minify_html' => array(
48 48 'type' => 'bool',
49 49 'default' => false,
50 - 'label' => 'Minify HTML',
51 - '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' ),
52 56 ),
53 57 'minify_css' => array(
54 58 'type' => 'bool',
55 59 'default' => false,
56 - 'label' => 'Minify CSS',
57 - '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' ),
58 62 ),
59 63 'minify_js' => array(
60 64 'type' => 'bool',
61 65 'default' => false,
62 - 'label' => 'Minify JavaScript',
63 - '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' ),
64 68 ),
65 69 'defer_js' => array(
66 70 'type' => 'bool',
67 71 'default' => false,
68 - 'label' => 'Defer JavaScript',
69 - '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' ),
70 74 ),
71 75 'delay_js' => array(
72 76 'type' => 'bool',
73 77 'default' => false,
74 - 'label' => 'Delay JavaScript Until Interaction',
75 - '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' ),
76 80 ),
77 81 'async_css' => array(
78 82 'type' => 'bool',
79 83 'default' => false,
80 - 'label' => 'Load CSS Asynchronously',
81 - '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' ),
82 86 ),
83 87 'remove_query_strings' => array(
84 88 'type' => 'bool',
85 89 'default' => false,
86 - 'label' => 'Remove Asset Query Strings',
87 - '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' ),
88 92 ),
89 93 'defer_js_excluded' => array(
90 94 'type' => 'list',
91 95 'default' => array( 'jquery-core', 'jquery-migrate' ),
92 96 'item_type' => 'string',
93 - 'label' => 'Defer / Delay Exclusions',
94 - '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' ),
95 99 // Only relevant once defer OR delay is on — the exclusion list
96 100 // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
97 101 'dependsOn' => array(
98 102 'any' => array(
@@ -100,19 +104,37 @@
100 104 array( 'field' => 'delay_js' ),
101 105 ),
102 106 ),
103 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 + ),
104 126 'combine_css' => array(
105 127 'type' => 'bool',
106 128 'default' => false,
107 - 'label' => 'Combine CSS Files',
108 - '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' ),
109 131 ),
110 132 'combine_js' => array(
111 133 'type' => 'bool',
112 134 'default' => false,
113 - 'label' => 'Combine JavaScript Files',
114 - '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' ),
115 137 ),
116 138 );
117 139 }
118 140
@@ -172,8 +194,9 @@
172 194 array(
173 195 'name' => 'xspeed minify',
174 196 'callback' => array( $this, 'cli_handler' ),
175 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.',
176 199 'synopsis' => array(
177 200 array(
178 201 'type' => 'positional',
179 202 'name' => 'action',
@@ -193,8 +216,26 @@
193 216 * 2. Instantiate the v1 Minifier engine; it now reads from
194 217 * Settings_Manager::get('minify') via its updated read path.
195 218 */
196 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 {
197 238 $this->seed_from_legacy_if_needed();
198 239 new LegacyMinifier();
199 240 }
200 241
@@ -203,8 +244,45 @@
203 244 // run more than once.
204 245 $this->seed_from_legacy_if_needed();
205 246 }
206 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 +
207 285 private function seed_from_legacy_if_needed(): void {
208 286 $existing = get_option( 'xspeed_module_minify', null );
209 287 if ( null !== $existing ) {
210 288 return;
@@ -232,9 +310,21 @@
232 310 $action = $args[0] ?? 'status';
233 311
234 312 if ( 'status' === $action ) {
235 313 $opts = Settings_Manager::get( self::SLUG );
236 - \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 ) );
237 327 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
238 328 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
239 329 return;
240 330 }
@@ -245,6 +335,15 @@
245 335 return;
246 336 }
247 337
248 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();
249 348 }
250 349 }