| 1 |
<?php |
| 2 |
namespace ABlocks\Classes; |
| 3 |
|
| 4 |
if ( ! defined( 'ABSPATH' ) ) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
/** |
| 9 |
* Phase 2: hand font emission to WordPress core. |
| 10 |
* |
| 11 |
* Locally-hosted fonts the current page needs are injected into theme.json |
| 12 |
* (settings.typography.fontFamilies). Core's wp_print_font_faces() then prints |
| 13 |
* the @font-face rules automatically — no custom enqueue/discovery. |
| 14 |
* |
| 15 |
* Any font not yet self-hosted falls back to a Google Fonts <link>. |
| 16 |
* |
| 17 |
* Requires WP 6.5+ (the plugin requires 6.8). See docs/FONT-MANAGEMENT-PLAN.md. |
| 18 |
*/ |
| 19 |
class CoreFontRegistry { |
| 20 |
|
| 21 |
public static function init() { |
| 22 |
$self = new self(); |
| 23 |
// Inject the page's local fonts so core prints their @font-face. Register |
| 24 |
// on a single origin to avoid emitting duplicate @font-face rules. |
| 25 |
add_filter( 'wp_theme_json_data_theme', [ $self, 'register_page_fonts' ] ); |
| 26 |
// Remote fallback for fonts not yet downloaded locally. |
| 27 |
add_action( 'wp_enqueue_scripts', [ $self, 'enqueue_remote_fallback' ], 999 ); |
| 28 |
// Metric-adjusted local faces, so the pre-swap render is the right size. |
| 29 |
add_action( 'wp_enqueue_scripts', [ $self, 'enqueue_fallback_faces' ], 5 ); |
| 30 |
} |
| 31 |
|
| 32 |
/** |
| 33 |
* Emit the metric-adjusted fallback @font-face rules for this page's fonts. |
| 34 |
* |
| 35 |
* These reference locally installed system fonts via local() — nothing is |
| 36 |
* downloaded. Their only job is to make the fallback render occupy exactly the |
| 37 |
* space the real font will, so the swap doesn't shift the layout. |
| 38 |
*/ |
| 39 |
public function enqueue_fallback_faces() { |
| 40 |
$fonts = $this->page_fonts(); |
| 41 |
if ( empty( $fonts ) ) { |
| 42 |
return; |
| 43 |
} |
| 44 |
|
| 45 |
$css = FontStack::get_fallback_face_css( $fonts ); |
| 46 |
if ( '' === $css ) { |
| 47 |
return; |
| 48 |
} |
| 49 |
|
| 50 |
wp_register_style( 'ablocks-font-fallbacks', false, [], ABLOCKS_VERSION ); |
| 51 |
wp_enqueue_style( 'ablocks-font-fallbacks' ); |
| 52 |
wp_add_inline_style( 'ablocks-font-fallbacks', $css ); |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Page fonts, resolved once per request. |
| 57 |
*/ |
| 58 |
protected function page_fonts() { |
| 59 |
return FontCollector::get_page_fonts(); |
| 60 |
} |
| 61 |
|
| 62 |
/** |
| 63 |
* Register the current page's locally-hosted fonts into theme.json data so |
| 64 |
* core emits their @font-face declarations. |
| 65 |
* |
| 66 |
* @param \WP_Theme_JSON_Data $theme_json |
| 67 |
* @return \WP_Theme_JSON_Data |
| 68 |
*/ |
| 69 |
public function register_page_fonts( $theme_json ) { |
| 70 |
if ( is_admin() || ! is_object( $theme_json ) || ! method_exists( $theme_json, 'get_data' ) ) { |
| 71 |
return $theme_json; |
| 72 |
} |
| 73 |
|
| 74 |
$fonts = $this->page_fonts(); |
| 75 |
if ( empty( $fonts ) ) { |
| 76 |
return $theme_json; |
| 77 |
} |
| 78 |
|
| 79 |
$loader = new FontLoadLocally(); |
| 80 |
$families = []; |
| 81 |
|
| 82 |
foreach ( $fonts as $family => $weights ) { |
| 83 |
$local_faces = $loader->get_local_font_faces( $family, (array) $weights ); |
| 84 |
if ( empty( $local_faces ) ) { |
| 85 |
continue; // not self-hosted yet → handled by the remote fallback |
| 86 |
} |
| 87 |
|
| 88 |
$font_faces = []; |
| 89 |
foreach ( $local_faces as $face ) { |
| 90 |
$font_faces[] = [ |
| 91 |
'fontFamily' => $family, |
| 92 |
'fontStyle' => 'normal', |
| 93 |
'fontWeight' => $face['weight'], |
| 94 |
'fontDisplay' => 'swap', |
| 95 |
'src' => [ $face['src'] ], |
| 96 |
]; |
| 97 |
} |
| 98 |
|
| 99 |
$families[] = [ |
| 100 |
'fontFamily' => $family, |
| 101 |
'name' => $family, |
| 102 |
'slug' => sanitize_title( $family ), |
| 103 |
'fontFace' => $font_faces, |
| 104 |
]; |
| 105 |
} |
| 106 |
|
| 107 |
if ( empty( $families ) ) { |
| 108 |
return $theme_json; |
| 109 |
} |
| 110 |
|
| 111 |
$data = $theme_json->get_data(); |
| 112 |
|
| 113 |
// WP 6.6+ keys typography.fontFamilies by origin ( [ 'theme' => [ …presets… ] ] ); |
| 114 |
// older versions used a flat preset list. Append our families to the 'theme' |
| 115 |
// origin bucket. Merging a flat list onto the origin-keyed shape (the previous |
| 116 |
// behaviour) produced a malformed preset with no 'slug', tripping core's |
| 117 |
// get_settings_values_by_slug()/get_settings_slugs() (undefined array key "slug"). |
| 118 |
$existing = $data['settings']['typography']['fontFamilies'] ?? []; |
| 119 |
if ( ! is_array( $existing ) ) { |
| 120 |
$existing = []; |
| 121 |
} |
| 122 |
if ( isset( $existing[0] ) ) { // flat list → normalise to the theme origin |
| 123 |
$existing = [ 'theme' => $existing ]; |
| 124 |
} |
| 125 |
$existing['theme'] = array_merge( |
| 126 |
( isset( $existing['theme'] ) && is_array( $existing['theme'] ) ) ? $existing['theme'] : [], |
| 127 |
$families |
| 128 |
); |
| 129 |
$data['settings']['typography']['fontFamilies'] = $existing; |
| 130 |
|
| 131 |
// update_with() replaces the theme origin's preset list wholesale, so $existing |
| 132 |
// must already contain the theme's own fonts alongside ours. |
| 133 |
return $theme_json->update_with( $data ); |
| 134 |
} |
| 135 |
|
| 136 |
/** |
| 137 |
* Enqueue a Google Fonts <link> for any page font not available locally. |
| 138 |
*/ |
| 139 |
public function enqueue_remote_fallback() { |
| 140 |
$fonts = $this->page_fonts(); |
| 141 |
if ( empty( $fonts ) ) { |
| 142 |
return; |
| 143 |
} |
| 144 |
|
| 145 |
$missing = ( new FontLoadLocally() )->get_missing( $fonts ); |
| 146 |
if ( empty( $missing ) ) { |
| 147 |
return; |
| 148 |
} |
| 149 |
|
| 150 |
$family_strings = []; |
| 151 |
foreach ( $missing as $family => $weights ) { |
| 152 |
$weights = array_unique( array_map( 'strval', (array) $weights ) ); |
| 153 |
sort( $weights ); |
| 154 |
$encoded = str_replace( ' ', '+', $family ); |
| 155 |
$family_strings[] = ! empty( $weights ) ? $encoded . ':wght@' . implode( ';', $weights ) : $encoded; |
| 156 |
} |
| 157 |
|
| 158 |
$url = 'https://fonts.googleapis.com/css2?family=' . implode( '&family=', $family_strings ) . '&display=swap'; |
| 159 |
wp_enqueue_style( 'ablocks-google-fonts', esc_url( $url ), [], null ); |
| 160 |
|
| 161 |
// Warm up the font connections early so the render-blocking stylesheet + |
| 162 |
// its font files resolve faster (helps FCP/LCP). Only added when a remote |
| 163 |
// font is actually loaded. |
| 164 |
add_filter( 'wp_resource_hints', [ $this, 'preconnect_google_fonts' ], 10, 2 ); |
| 165 |
} |
| 166 |
|
| 167 |
/** |
| 168 |
* Add preconnect hints for the Google Fonts origins. |
| 169 |
* |
| 170 |
* @param array $hints Resource hint URLs/attributes for this relation. |
| 171 |
* @param string $relation Current relation type (preconnect, dns-prefetch, …). |
| 172 |
* @return array |
| 173 |
*/ |
| 174 |
public function preconnect_google_fonts( $hints, $relation ) { |
| 175 |
if ( 'preconnect' !== $relation ) { |
| 176 |
return $hints; |
| 177 |
} |
| 178 |
$hints[] = [ 'href' => 'https://fonts.googleapis.com' ]; |
| 179 |
$hints[] = [ |
| 180 |
'href' => 'https://fonts.gstatic.com', |
| 181 |
'crossorigin' => 'anonymous', |
| 182 |
]; |
| 183 |
return $hints; |
| 184 |
} |
| 185 |
} |
| 186 |
|