match.
$url = self::strategy_direct_link( $html, $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'direct_link' );
}
// Strategy 2: @import url(fonts.googleapis.com...) match.
$url = self::strategy_import_match( $html, $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'import' );
}
// Strategy 3: Broad URL match anywhere in HTML.
$url = self::strategy_broad_url( $html, $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'broad_url' );
}
// Strategy 4: CSS font-family detection + URL construction.
$url = self::strategy_css_family( $html, $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'css_family' );
}
}
// Strategy 5: theme.json scanning (no homepage HTML needed).
$url = self::strategy_theme_json( $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'theme_json' );
}
// Strategy 6: Theme file scanning.
$url = self::strategy_theme_files( $log );
if ( $url ) {
return array( 'url' => $url, 'log' => $log, 'strategy' => 'theme_files' );
}
$log[] = 'All 6 strategies exhausted. No Google Fonts detected.';
return array( 'url' => false, 'log' => $log, 'strategy' => '' );
}
/**
* Fetch homepage HTML for font detection.
*
* @param array $log Detection log (passed by reference).
* @return string|false HTML body or false on failure.
*/
private static function fetch_homepage_html( &$log ) {
$log[] = 'Fetching homepage: ' . home_url();
$response = wp_remote_get(
home_url(),
array( 'timeout' => 15 )
);
if ( is_wp_error( $response ) ) {
$log[] = 'ERROR: Homepage fetch failed: ' . $response->get_error_message();
return false;
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code >= 400 ) {
$log[] = 'ERROR: Homepage returned HTTP ' . $code;
return false;
}
$body = wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
$log[] = 'ERROR: Homepage returned empty body.';
return false;
}
$log[] = 'Homepage fetched OK (' . size_format( strlen( $body ) ) . ').';
return $body;
}
/**
* Strategy 1: Match directly.
*
* @param string $html Homepage HTML.
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_direct_link( $html, &$log ) {
$log[] = '[Strategy 1: Direct Link] Scanning for with Google Fonts href...';
if ( preg_match( '/href=["\']([^"\']*fonts\.googleapis\.com\/css2?\?[^"\']+)["\']/i', $html, $matches ) ) {
$url = html_entity_decode( $matches[1] );
$log[] = '[Strategy 1] FOUND: ' . $url;
return $url;
}
$log[] = '[Strategy 1] No tag with Google Fonts found.';
return false;
}
/**
* Strategy 2: Match @import url(fonts.googleapis.com/css...) in HTML.
*
* @param string $html Homepage HTML.
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_import_match( $html, &$log ) {
$log[] = '[Strategy 2: @import] Scanning for @import with Google Fonts...';
if ( preg_match( '/@import\s+(?:url\s*\(\s*)?["\']?(https?:\/\/fonts\.googleapis\.com\/css2?\?[^"\')\s]+)["\']?\s*\)?/i', $html, $matches ) ) {
$url = html_entity_decode( $matches[1] );
$log[] = '[Strategy 2] FOUND: ' . $url;
return $url;
}
// Protocol-relative.
if ( preg_match( '/@import\s+(?:url\s*\(\s*)?["\']?(\/\/fonts\.googleapis\.com\/css2?\?[^"\')\s]+)["\']?\s*\)?/i', $html, $matches ) ) {
$url = 'https:' . html_entity_decode( $matches[1] );
$log[] = '[Strategy 2] FOUND (protocol-relative): ' . $url;
return $url;
}
$log[] = '[Strategy 2] No @import rule found.';
return false;
}
/**
* Strategy 3: Find any fonts.googleapis.com/css URL anywhere in HTML.
*
* Catches URLs in comments, data attributes, JavaScript variables, etc.
*
* @param string $html Homepage HTML.
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_broad_url( $html, &$log ) {
$log[] = '[Strategy 3: Broad URL] Scanning entire HTML for any Google Fonts CSS URL...';
if ( preg_match_all( '/(https?:\/\/fonts\.googleapis\.com\/css2?\?[^\s"\'<>)\]]+)/i', $html, $matches ) ) {
$urls = array_unique( $matches[1] );
foreach ( $urls as $url ) {
$url = html_entity_decode( $url );
if ( false !== strpos( $url, 'family=' ) ) {
$log[] = '[Strategy 3] FOUND: ' . $url;
return $url;
}
}
}
$log[] = '[Strategy 3] No Google Fonts URL found anywhere in HTML.';
return false;
}
/**
* Strategy 4: Parse CSS font-family declarations, construct Google Fonts URL, validate.
*
* This is the key strategy for sites where other plugins have removed/replaced
* Google Fonts URLs. The CSS still declares font-family names, which we can use
* to construct a fresh Google Fonts URL.
*
* @param string $html Homepage HTML.
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_css_family( $html, &$log ) {
$log[] = '[Strategy 4: CSS Family] Parsing page CSS for font-family declarations...';
libxml_use_internal_errors( true );
$dom = new \DOMDocument();
$dom->loadHTML( $html );
libxml_clear_errors();
$families = self::extract_font_families_from_dom( $dom, $log );
if ( empty( $families ) ) {
$log[] = '[Strategy 4] No candidate font families found in CSS.';
return false;
}
$log[] = '[Strategy 4] Candidates: ' . implode( ', ', $families );
$url = self::construct_google_fonts_url( $families, $log );
if ( $url ) {
$log[] = '[Strategy 4] FOUND: ' . $url;
return $url;
}
$log[] = '[Strategy 4] No candidates validated as Google Fonts.';
return false;
}
/**
* Strategy 5: Scan theme.json for font families that might be Google Fonts.
*
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_theme_json( &$log ) {
$log[] = '[Strategy 5: theme.json] Checking active theme...';
$paths = array_unique( array(
get_stylesheet_directory() . '/theme.json',
get_template_directory() . '/theme.json',
) );
$generic = self::get_generic_families();
$families = array();
foreach ( $paths as $path ) {
if ( ! file_exists( $path ) ) {
continue;
}
$log[] = '[Strategy 5] Reading: ' . wp_basename( dirname( $path ) ) . '/theme.json';
$data = wp_json_file_decode( $path, array( 'associative' => true ) );
if ( empty( $data ) ) {
$log[] = '[Strategy 5] Invalid JSON.';
continue;
}
// Collect font entries from settings.typography.fontFamilies.
$font_entries = array();
if ( ! empty( $data['settings']['typography']['fontFamilies'] ) ) {
$ff = $data['settings']['typography']['fontFamilies'];
if ( isset( $ff[0] ) ) {
$font_entries = $ff;
} else {
foreach ( $ff as $entries ) {
if ( is_array( $entries ) ) {
$font_entries = array_merge( $font_entries, $entries );
}
}
}
}
foreach ( $font_entries as $entry ) {
// Check for direct Google Fonts src references.
if ( ! empty( $entry['fontFace'] ) ) {
foreach ( $entry['fontFace'] as $face ) {
if ( ! empty( $face['src'] ) ) {
$src = is_array( $face['src'] ) ? $face['src'][0] : $face['src'];
if ( false !== strpos( $src, 'fonts.googleapis.com' ) ) {
$log[] = '[Strategy 5] FOUND direct Google Fonts URL in theme.json';
return $src;
}
}
}
}
// Extract the family name.
$font_family = isset( $entry['fontFamily'] ) ? $entry['fontFamily'] : ( isset( $entry['name'] ) ? $entry['name'] : '' );
if ( empty( $font_family ) ) {
continue;
}
$parts = explode( ',', $font_family );
$family = trim( $parts[0], "\"' \t\r\n" );
if ( empty( $family ) || in_array( strtolower( $family ), $generic, true ) ) {
continue;
}
if ( ! in_array( $family, $families, true ) ) {
$families[] = $family;
$log[] = '[Strategy 5] Candidate: "' . $family . '"';
}
}
}
if ( empty( $families ) ) {
$log[] = '[Strategy 5] No font families found in theme.json.';
return false;
}
$url = self::construct_google_fonts_url( $families, $log );
if ( $url ) {
$log[] = '[Strategy 5] FOUND: ' . $url;
return $url;
}
$log[] = '[Strategy 5] No families validated as Google Fonts.';
return false;
}
/**
* Strategy 6: Search theme style.css and functions.php for Google Fonts URLs.
*
* @param array $log Detection log (by reference).
* @return string|false Google Fonts URL or false.
*/
private static function strategy_theme_files( &$log ) {
$log[] = '[Strategy 6: Theme Files] Scanning theme source files...';
$files = array_unique( array(
get_stylesheet_directory() . '/style.css',
get_stylesheet_directory() . '/functions.php',
get_template_directory() . '/style.css',
get_template_directory() . '/functions.php',
) );
foreach ( $files as $file ) {
if ( ! file_exists( $file ) ) {
continue;
}
$fp = fopen( $file, 'r' );
if ( ! $fp ) {
continue;
}
$contents = fread( $fp, filesize( $file ) );
fclose( $fp );
if ( empty( $contents ) ) {
continue;
}
if ( preg_match( '/(https?:\/\/fonts\.googleapis\.com\/css2?\?[^\s"\'<>)\]]+)/i', $contents, $matches ) ) {
$url = $matches[1];
if ( false !== strpos( $url, 'family=' ) ) {
$log[] = '[Strategy 6] FOUND in ' . wp_basename( $file ) . ': ' . $url;
return $url;
}
}
}
$log[] = '[Strategy 6] No Google Fonts URLs found in theme files.';
return false;
}
/**
* Get list of generic CSS font families to exclude from detection.
*
* @return array Lowercase family names.
*/
private static function get_generic_families() {
return array(
'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
'math', 'emoji', 'fangsong',
'-apple-system', 'blinkmacsystemfont',
'inherit', 'initial', 'unset', 'revert', 'revert-layer',
);
}
/**
* Extract font family names from DOM style blocks and linked stylesheets.
*
* @param \DOMDocument $dom DOM document.
* @param array $log Detection log (by reference).
* @return array Candidate family names.
*/
private static function extract_font_families_from_dom( \DOMDocument $dom, &$log ) {
$styles = $dom->getElementsByTagName( 'style' );
$links = $dom->getElementsByTagName( 'link' );
$generic = self::get_generic_families();
$found = array();
$priority_selectors = array(
'body', 'html', ':root', 'h1', 'h2', 'h3', 'p',
'.entry-title', '.site-title', '.elementor-heading-title',
'.wp-block-heading', '.has-global-padding',
);
// Scan inline