| @@ -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 | } |