PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 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 All 35 releases
← All changes | includes/modules/Fonts/FontsModule.php +102 -15 1.2.4 → 1.4.1 View file →
@@ -31,11 +31,12 @@
31 31 public const VERSION = '1.0.0';
32 32
33 33 public function ui_metadata(): array {
34 34 return array(
35 - 'label' => 'Fonts',
35 + 'label' => __( 'Fonts', 'xspeed' ),
36 36 'icon' => 'Type',
37 - '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',
38 39 );
39 40 }
40 41
41 42 public function settings_schema(): array {
@@ -42,22 +43,41 @@
42 43 return array(
43 44 'font_display_swap' => array(
44 45 'type' => 'bool',
45 46 'default' => true,
46 - 'label' => 'Add font-display: swap',
47 - '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' => __( 'Text in Google Fonts, and in fonts added to WordPress itself, shows at once in a standard font, then switches when the web font arrives.', 'xspeed' ),
48 49 ),
49 50 'preload_fonts' => array(
50 51 'type' => 'list',
51 52 'default' => array(),
52 53 'item_type' => 'url',
53 - 'label' => 'Preload Font URLs',
54 - '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' ),
55 56 ),
56 57 );
57 58 }
58 59
59 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 {
60 80 // Frontend-only rewriting. Admin / cron / AJAX / REST never
61 81 // render <link rel="stylesheet"> tags we should touch.
62 82 if ( is_admin()
63 83 || ( defined( 'DOING_AJAX' ) && DOING_AJAX )
@@ -74,8 +94,27 @@
74 94 $opts = $this->get_settings();
75 95
76 96 if ( ! empty( $opts['font_display_swap'] ) ) {
77 97 add_filter( 'style_loader_tag', array( __CLASS__, 'inject_display_swap' ), 10, 2 );
98 +
99 + // Fonts added through the Font Library or theme.json are printed
100 + // by core with font-display: fallback, and core offers no filter
101 + // for it. Fallback hides the text for up to 100ms while the font
102 + // loads; when that text is the LCP, the hero paints after the
103 + // font instead of at first paint. Measured on a live text hero:
104 + // LCP landed ~85ms after FCP with fallback and on FCP with swap,
105 + // CLS unchanged, and PageSpeed mobile moved between 81 and 90
106 + // depending on which side of that window the font fell. So core's
107 + // own printer runs with swap as the default. Only an exact
108 + // priority-50 hook is replaced: anything that already moved or
109 + // removed it is left alone.
110 + if ( function_exists( 'wp_print_font_faces' )
111 + && class_exists( '\WP_Font_Face_Resolver' )
112 + && 50 === has_action( 'wp_head', 'wp_print_font_faces' )
113 + ) {
114 + remove_action( 'wp_head', 'wp_print_font_faces', 50 );
115 + add_action( 'wp_head', array( __CLASS__, 'print_font_faces_swap' ), 50 );
116 + }
78 117 }
79 118
80 119 if ( ! empty( $opts['preload_fonts'] ) ) {
81 120 add_action(
@@ -109,18 +148,28 @@
109 148 }
110 149
111 150 $href = $m[2];
112 151
113 - // Already has a display param — leave it alone (respect the theme /
114 - // plugin that set it). The href here has been through esc_url(),
115 - // which encodes "&" as the entity "&#038;", so a real URL like
116 - // ...?family=Roboto&display=optional arrives as
117 - // ...?family=Roboto&#038;display=optional — the char before
118 - // "display=" is then ";" (tail of the entity), not "&", and the old
119 - // [?&]display= guard missed it, double-appending a second display.
120 - // Decode entities before the check so it matches either form.
121 - // (FBS-82161)
152 + // A display param the theme set is respected ONLY when it is one of
153 + // the non-blocking choices (swap / fallback / optional) — someone
154 + // picked those deliberately and each is a defensible trade. `auto`
155 + // and `block` are the values this setting exists to remove: `auto`
156 + // IS block behavior in every engine, and it is almost never a
157 + // choice — it is the default a theme's enqueue happened to emit.
158 + // "Respecting" it turned the switch into a no-op on exactly the
159 + // sites that need it: a live text-LCP measured a 5.5s render delay
160 + // behind flatsome's `display=auto` Poppins URL while this option
161 + // was on and its label promised the opposite. WP Rocket and
162 + // LiteSpeed rewrite these too. The href here has been through
163 + // esc_url(), which encodes "&" as "&#038;" — decode before
164 + // matching, rewrite on the ORIGINAL encoded href so str_replace
165 + // finds it in the tag verbatim. (FBS-82161)
122 166 $href_decoded = html_entity_decode( $href, ENT_QUOTES | ENT_HTML5 );
167 + if ( preg_match( '/([?&])display=(auto|block)(&|$)/i', $href_decoded ) ) {
168 + $new_href = preg_replace( '/((?:[?&]|&#0*38;|&#[xX]0*26;|&amp;)display=)(?:auto|block)(?=&|$)/i', '$1swap', $href );
169 +
170 + return str_replace( $href, $new_href, $tag );
171 + }
123 172 if ( preg_match( '/[?&]display=/i', $href_decoded ) ) {
124 173 return $tag;
125 174 }
126 175
@@ -130,8 +179,46 @@
130 179 $separator = ( false === strpos( $href_decoded, '?' ) ) ? '?' : '&';
131 180 $new_href = $href . $separator . 'display=swap';
132 181
133 182 return str_replace( $href, $new_href, $tag );
183 + }
184 +
185 + /**
186 + * Print core's font faces as wp_print_font_faces() does, with swap as
187 + * the font-display default.
188 + */
189 + public static function print_font_faces_swap(): void {
190 + $fonts = \WP_Font_Face_Resolver::get_fonts_from_theme_json();
191 + if ( empty( $fonts ) ) {
192 + return;
193 + }
194 + // WordPress 6.4+. Only hooked when the function exists (see
195 + // boot_on_init()), called by name so a 6.0 floor stays compatible.
196 + call_user_func( 'wp_print_font_faces', self::default_display_swap( $fonts ) );
197 + }
198 +
199 + /**
200 + * Give every font face without its own font-display a swap one.
201 + *
202 + * The resolver only sets font-display when a theme.json fontFace
203 + * declares fontDisplay, so a face that has one was chosen on purpose
204 + * and keeps it. Public + static so the test suite can drive it.
205 + *
206 + * @param array<int|string,mixed> $fonts Font families, each a list of faces.
207 + * @return array<int|string,mixed>
208 + */
209 + public static function default_display_swap( array $fonts ): array {
210 + foreach ( $fonts as $family => $faces ) {
211 + if ( ! is_array( $faces ) ) {
212 + continue;
213 + }
214 + foreach ( $faces as $i => $face ) {
215 + if ( is_array( $face ) && ! isset( $face['font-display'] ) ) {
216 + $fonts[ $family ][ $i ]['font-display'] = 'swap';
217 + }
218 + }
219 + }
220 + return $fonts;
134 221 }
135 222
136 223 /**
137 224 * Render the preload <link> markup for a list of font URLs.