PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 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 All 33 releases
← All changes | includes/modules/Fonts/FontsModule.php +58 -10 1.0.4 → 1.3.7 View file →
@@ -19,8 +19,10 @@
19 19 declare(strict_types=1);
20 20
21 21 namespace XSpeed\Modules\Fonts;
22 22
23 +defined( 'ABSPATH' ) || exit;
24 +
23 25 use XSpeed\Module;
24 26
25 27 final class FontsModule extends Module {
26 28
@@ -29,11 +31,12 @@
29 31 public const VERSION = '1.0.0';
30 32
31 33 public function ui_metadata(): array {
32 34 return array(
33 - 'label' => 'Fonts',
35 + 'label' => __( 'Fonts', 'xspeed' ),
34 36 'icon' => 'Type',
35 - 'description' => 'Stop web fonts from blocking text. Adds display=swap to Google Fonts and preloads the fonts you mark critical.',
37 + 'description' => __( 'Shows text right away while web fonts load, and preloads key fonts.', 'xspeed' ),
38 + 'group' => 'performance',
36 39 );
37 40 }
38 41
39 42 public function settings_schema(): array {
@@ -40,22 +43,41 @@
40 43 return array(
41 44 'font_display_swap' => array(
42 45 'type' => 'bool',
43 46 'default' => true,
44 - 'label' => 'Add font-display: swap',
45 - 'description' => 'Append display=swap to Google Fonts URLs so text renders immediately in a fallback face while the web font loads. No effect on URLs that already declare a display value.',
47 + 'label' => __( 'Show text while fonts load', 'xspeed' ),
48 + 'description' => __( 'Google Fonts text shows at once in a standard font, then switches when the web font arrives.', 'xspeed' ),
46 49 ),
47 50 'preload_fonts' => array(
48 51 'type' => 'list',
49 52 'default' => array(),
50 53 'item_type' => 'url',
51 - 'label' => 'Preload Font URLs',
52 - 'description' => 'One absolute font URL per line (woff2/woff/ttf/otf). Each becomes a <link rel="preload" as="font" crossorigin> in the head so the browser starts downloading before the CSS parses. Use only for fonts that render above the fold.',
54 + 'label' => __( 'Fonts to preload', 'xspeed' ),
55 + 'description' => __( 'One full font file URL per line (woff2, woff, ttf or otf). The browser fetches these first, so list only fonts used at the top of the page.', 'xspeed' ),
53 56 ),
54 57 );
55 58 }
56 59
57 60 public function boot(): void {
61 + /*
62 + * Deferred to `init`. This module reads its own settings to decide
63 + * what to hook, and reading settings builds settings_schema(), whose
64 + * labels are declared through __(). boot() runs on `plugins_loaded`,
65 + * before `after_setup_theme` — the point WordPress 6.7+ treats as the
66 + * earliest safe moment to translate — so doing that here fires
67 + * _load_textdomain_just_in_time on every request AND resolves the
68 + * labels against a domain that is not loaded yet.
69 + *
70 + * Everything below hooks actions that fire after `init`, so running
71 + * one hook later is equivalent.
72 + */
73 + add_action( 'init', array( $this, 'boot_on_init' ) );
74 + }
75 +
76 + /**
77 + * The real boot body — see boot() for why it runs on `init`.
78 + */
79 + public function boot_on_init(): void {
58 80 // Frontend-only rewriting. Admin / cron / AJAX / REST never
59 81 // render <link rel="stylesheet"> tags we should touch.
60 82 if ( is_admin()
61 83 || ( defined( 'DOING_AJAX' ) && DOING_AJAX )
@@ -60,8 +82,12 @@
60 82 if ( is_admin()
61 83 || ( defined( 'DOING_AJAX' ) && DOING_AJAX )
62 84 || ( defined( 'DOING_CRON' ) && DOING_CRON )
63 85 || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
86 + // A builder editing screen is a front-end URL none of the above
87 + // catch; swapping font-display under it changes what the editor
88 + // measures. (#281)
89 + || \XSpeed\Builder_Editor::is_active()
64 90 ) {
65 91 return;
66 92 }
67 93
@@ -103,15 +129,36 @@
103 129 }
104 130
105 131 $href = $m[2];
106 132
107 - // Already has a display param — leave it alone (respect the
108 - // theme / plugin that set it).
109 - if ( preg_match( '/[?&]display=/i', $href ) ) {
133 + // A display param the theme set is respected ONLY when it is one of
134 + // the non-blocking choices (swap / fallback / optional) — someone
135 + // picked those deliberately and each is a defensible trade. `auto`
136 + // and `block` are the values this setting exists to remove: `auto`
137 + // IS block behavior in every engine, and it is almost never a
138 + // choice — it is the default a theme's enqueue happened to emit.
139 + // "Respecting" it turned the switch into a no-op on exactly the
140 + // sites that need it: a live text-LCP measured a 5.5s render delay
141 + // behind flatsome's `display=auto` Poppins URL while this option
142 + // was on and its label promised the opposite. WP Rocket and
143 + // LiteSpeed rewrite these too. The href here has been through
144 + // esc_url(), which encodes "&" as "&#038;" — decode before
145 + // matching, rewrite on the ORIGINAL encoded href so str_replace
146 + // finds it in the tag verbatim. (FBS-82161)
147 + $href_decoded = html_entity_decode( $href, ENT_QUOTES | ENT_HTML5 );
148 + if ( preg_match( '/([?&])display=(auto|block)(&|$)/i', $href_decoded ) ) {
149 + $new_href = preg_replace( '/((?:[?&]|&#0*38;|&#[xX]0*26;|&amp;)display=)(?:auto|block)(?=&|$)/i', '$1swap', $href );
150 +
151 + return str_replace( $href, $new_href, $tag );
152 + }
153 + if ( preg_match( '/[?&]display=/i', $href_decoded ) ) {
110 154 return $tag;
111 155 }
112 156
113 - $separator = ( false === strpos( $href, '?' ) ) ? '?' : '&';
157 + // Pick the separator from the DECODED url (so a "?" hidden behind an
158 + // entity is still recognised), but append to the ORIGINAL (encoded)
159 + // href so the str_replace below matches the tag verbatim.
160 + $separator = ( false === strpos( $href_decoded, '?' ) ) ? '?' : '&';
114 161 $new_href = $href . $separator . 'display=swap';
115 162
116 163 return str_replace( $href, $new_href, $tag );
117 164 }
@@ -177,8 +224,9 @@
177 224 array(
178 225 'name' => 'xspeed fonts',
179 226 'callback' => array( $this, 'cli_handler' ),
180 227 'shortdesc' => 'Show font-optimization settings.',
228 + 'ai_hint' => 'How are web fonts being optimized (font-display swap, preloading, local hosting)? Use for questions about invisible text while loading (FOIT/FOUT) or render-blocking fonts.',
181 229 'synopsis' => array(),
182 230 ),
183 231 );
184 232 }