PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.2
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.2
2.0.4 2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / src / Frontend / SmartPreload.php

SmartPreload.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.2, at src/Frontend/SmartPreload.php

176 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Smart preload.
4 *
5 * Emits <link rel="preload"> for the font faces that render ABOVE THE FOLD on
6 * the current page, so the browser fetches them as early as possible. Preload
7 * is a scarce resource — preloading a font that isn't critical (or isn't even
8 * on the page) competes with the hero content — so the set is built narrowly:
9 *
10 * 1. PER-ROUTE above-the-fold families — measured by the beacon for THIS
11 * route. A font above the fold only on one page is preloaded only there.
12 * 2. GLOBAL above-the-fold families — families that are above the fold on
13 * every route where they render (e.g. the body/heading font). Their answer
14 * is the same everywhere, so they're preloaded wherever present without any
15 * per-route lookup (the cheap path the site-wide fonts take).
16 * 3. USER-FORCED families — variants the user toggled to "always preload".
17 *
18 * Every candidate is intersected with the faces actually emitted on this page
19 * (Consolidator::touched), and resolved to the file that is genuinely hosted —
20 * so a font is never preloaded on a page that doesn't use it, and a face the
21 * beacon reported at a weight we don't host still resolves to the right file.
22 *
23 * @package EasyFonts
24 */
25
26 namespace EasyFonts\Frontend;
27
28 use EasyFonts\Detect\Consolidator;
29 use EasyFonts\Fonts\Registry;
30 use EasyFonts\Fonts\Storage;
31 use EasyFonts\Fonts\UsageTracker;
32 use EasyFonts\Settings;
33
34 defined( 'ABSPATH' ) || exit;
35
36 /**
37 * Builds deduped, page-scoped preload link markup.
38 */
39 class SmartPreload {
40
41 /**
42 * @var Registry
43 */
44 private Registry $registry;
45
46 /**
47 * @var Storage
48 */
49 private Storage $storage;
50
51 /**
52 * Constructor.
53 */
54 public function __construct() {
55 $this->registry = new Registry();
56 $this->storage = new Storage();
57 }
58
59 /**
60 * Build preload markup for the current request.
61 *
62 * @param string $route Canonical route (path) of the current page.
63 * @param string $device 'mobile'|'desktop'|'any' (reserved; matching is family-level).
64 * @param array<string,bool> $present_families Lowercased families emitted on this page (from Consolidator::touched).
65 * @param array<string,bool> $above_fold Lowercased families measured above the fold on THIS route.
66 * @return string
67 */
68 public function build( string $route = '', string $device = 'any', array $present_families = array(), array $above_fold = array() ): string {
69 if ( ! Settings::get( 'smart_preload', 1 ) ) {
70 return '';
71 }
72
73 // Families that should preload here: this route's above-the-fold set,
74 // plus the uniformly-above-the-fold (global) set. The global set is the
75 // "skip per-route checking" path — its members preload wherever present.
76 $want = $above_fold;
77
78 foreach ( ( new UsageTracker() )->global_preload_families() as $fam => $_ ) {
79 $want[ $fam ] = true;
80 }
81
82 $cap = max( 1, (int) apply_filters( 'easyfonts_preload_cap', 6 ) );
83
84 $links = '';
85 $seen = array();
86 $count = 0;
87
88 // 1. Above-the-fold (per-route + global): preload the faces this page
89 // actually emits for those families — using the hosted variant, so a
90 // faux-bold/weight-mismatch report still resolves to a real file.
91 foreach ( Consolidator::touched() as $t ) {
92 if ( $count >= $cap ) {
93 break;
94 }
95
96 $fam = strtolower( (string) $t['family'] );
97
98 if ( empty( $want[ $fam ] ) ) {
99 continue;
100 }
101
102 $file = $this->registry->resolve_preload_file( (string) $t['family'], (string) $t['weight'], (string) $t['style'] );
103
104 if ( $this->emit_link( $file, $seen, $links ) ) {
105 $count++;
106 }
107 }
108
109 // 2. User-forced "always preload" variants, intersected with the page.
110 $gate = ! empty( $present_families );
111
112 foreach ( $this->registry->preload_targets() as $u ) {
113 if ( $count >= $cap ) {
114 break;
115 }
116
117 if ( $gate && empty( $present_families[ strtolower( (string) $u['family'] ) ] ) ) {
118 continue; // forced font isn't on this page — don't preload here.
119 }
120
121 if ( $this->emit_link( $u['file'], $seen, $links ) ) {
122 $count++;
123 }
124 }
125
126 return $links;
127 }
128
129 /**
130 * Append a preload <link> for a hosted file, deduping by file and skipping
131 * anything missing on disk. Returns true when a link was added.
132 *
133 * @param string|null $file Font filename.
134 * @param array<string,bool> $seen Dedupe set (by reference).
135 * @param string $links Accumulated markup (by reference).
136 * @return bool
137 */
138 private function emit_link( ?string $file, array &$seen, string &$links ): bool {
139 $file = (string) $file;
140
141 if ( '' === $file || isset( $seen[ $file ] ) || ! $this->storage->exists( $file ) ) {
142 return false;
143 }
144
145 $seen[ $file ] = true;
146
147 $url = add_query_arg( 'ver', Settings::buster(), $this->storage->url( $file ) );
148 $links .= sprintf(
149 '<link rel="preload" href="%s" as="font" type="%s" crossorigin>' . "\n",
150 esc_url( $url ),
151 esc_attr( $this->mime_for( $file ) )
152 );
153
154 return true;
155 }
156
157 /**
158 * MIME for a font file.
159 *
160 * @param string $file Filename.
161 * @return string
162 */
163 private function mime_for( string $file ): string {
164 $ext = strtolower( (string) pathinfo( $file, PATHINFO_EXTENSION ) );
165
166 $map = array(
167 'woff2' => 'font/woff2',
168 'woff' => 'font/woff',
169 'ttf' => 'font/ttf',
170 'otf' => 'font/otf',
171 );
172
173 return $map[ $ext ] ?? 'font/woff2';
174 }
175 }
176