'array',
'sanitize_callback' => array( 'I365_PO_Plugin', 'sanitize_settings' ),
'default' => I365_PO_Plugin::defaults(),
)
);
}
/**
* Enqueue admin assets.
*
* @param string $hook Current page hook.
*
* @return void
*/
public static function enqueue_assets( $hook ) {
if ( 'settings_page_i365-po-settings' !== $hook ) {
return;
}
wp_enqueue_style(
'i365-po-admin',
plugin_dir_url( I365_PO_PLUGIN_FILE ) . 'assets/admin.css',
array(),
I365_PO_VERSION
);
wp_enqueue_script(
'i365-po-admin',
plugin_dir_url( I365_PO_PLUGIN_FILE ) . 'assets/admin.js',
array(),
I365_PO_VERSION,
true
);
$hero_guess = '';
$hero_path = get_theme_file_path( 'assets/images/hero.webp' );
if ( $hero_path && file_exists( $hero_path ) ) {
$hero_guess = get_theme_file_uri( 'assets/images/hero.webp' );
}
$font_guess = self::guess_font_url();
wp_localize_script(
'i365-po-admin',
'I365PODetect',
array(
'home' => home_url(),
'stylesheet' => get_stylesheet_uri(),
'hero' => $hero_guess,
'font' => $font_guess,
'fontsCdn' => 'https://fonts.gstatic.com',
'nonce' => wp_create_nonce( 'i365-po-detect' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'messages' => array(
'running' => __( 'Detecting from homepage…', '365i-performance-optimizer' ),
'failed' => __( 'Detection failed. Check your homepage is reachable.', '365i-performance-optimizer' ),
'completed'=> __( 'Detection updated fields.', '365i-performance-optimizer' ),
),
)
);
}
/**
* Render settings page.
*
* @return void
*/
public static function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = I365_PO_Plugin::get_settings();
?>
__( 'Unauthorized', '365i-performance-optimizer' ) ) );
}
$response = wp_remote_get(
home_url(),
array(
'timeout' => 10,
)
);
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}
$code = (int) wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( $code >= 400 || empty( $body ) ) {
wp_send_json_error( array( 'message' => __( 'Homepage could not be fetched.', '365i-performance-optimizer' ) ) );
}
$result = array(
'preconnect' => array_filter( array( home_url(), 'https://fonts.gstatic.com' ) ),
'stylesheet' => '',
'font' => '',
'hero' => '',
);
libxml_use_internal_errors( true );
$dom = new \DOMDocument();
$loaded = $dom->loadHTML( $body );
libxml_clear_errors();
if ( $loaded ) {
$result['stylesheet'] = self::pick_best_stylesheet( $dom );
$font_pick = self::pick_best_font_v2( $dom );
$result['font'] = $font_pick['primary'];
$result['font_list'] = $font_pick['candidates'];
$result['hero'] = self::pick_best_image( $dom );
}
$result['stylesheet'] = self::strip_query( $result['stylesheet'] );
$result['font'] = self::strip_query( $result['font'] );
$result['hero'] = self::strip_query( $result['hero'] );
if ( ! empty( $result['stylesheet'] ) ) {
$response_code = wp_remote_retrieve_response_code( wp_remote_head( $result['stylesheet'], array( 'timeout' => 6, 'redirection' => 1 ) ) );
if ( $response_code >= 400 ) {
$result['stylesheet'] = '';
}
}
wp_send_json_success( $result );
}
/**
* Normalize relative URL to absolute.
*
* @param string $url URL.
*
* @return string
*/
private static function make_absolute_url( $url ) {
if ( empty( $url ) ) {
return '';
}
// Protocol-relative.
if ( 0 === strpos( $url, '//' ) ) {
return set_url_scheme( $url, is_ssl() ? 'https' : 'http' );
}
$parts = wp_parse_url( $url );
if ( ! empty( $parts['scheme'] ) ) {
return $url;
}
if ( ! empty( $parts['path'] ) && '/' === $parts['path'][0] ) {
return home_url( $parts['path'] );
}
return trailingslashit( home_url() ) . ltrim( $url, '/' );
}
/**
* Remove query string for preload targets.
*
* @param string $url URL.
* @return string
*/
private static function strip_query( $url ) {
if ( empty( $url ) ) {
return '';
}
$parts = wp_parse_url( $url );
if ( empty( $parts['query'] ) ) {
return $url;
}
$queryless = $parts['scheme'] . '://' . $parts['host'];
if ( ! empty( $parts['port'] ) ) {
$queryless .= ':' . $parts['port'];
}
if ( ! empty( $parts['path'] ) ) {
$queryless .= $parts['path'];
}
return $queryless;
}
/**
* Choose the most likely primary stylesheet.
*
* @param \DOMDocument $dom DOM.
* @return string
*/
private static function pick_best_stylesheet( \DOMDocument $dom ) {
$links = $dom->getElementsByTagName( 'link' );
$best = '';
$best_s = -INF;
$host = wp_parse_url( home_url(), PHP_URL_HOST );
foreach ( $links as $idx => $link ) {
$rel = strtolower( $link->getAttribute( 'rel' ) );
$href = $link->getAttribute( 'href' );
if ( false === strpos( $rel, 'stylesheet' ) ) {
continue;
}
if ( empty( $href ) ) {
continue;
}
$url = self::make_absolute_url( $href );
$score = 0 - $idx; // earlier is slightly preferred.
$target_host = wp_parse_url( $url, PHP_URL_HOST );
if ( $target_host && $host && $target_host === $host ) {
$score += 20;
}
if ( strpos( $url, 'dashicons' ) !== false || strpos( $url, 'emoji' ) !== false ) {
$score -= 50;
}
if ( strpos( $url, 'googleapis' ) !== false || strpos( $url, 'gstatic' ) !== false ) {
$score -= 10;
}
if ( $score > $best_s ) {
$best_s = $score;
$best = $url;
}
}
return $best;
}
/**
* Choose the most likely primary font preload or font file.
*
* @param \DOMDocument $dom DOM.
* @return string
*/
private static function pick_best_font( \DOMDocument $dom ) {
$links = $dom->getElementsByTagName( 'link' );
$best = '';
$best_s = -INF;
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$google_fonts = '';
$candidates = array();
foreach ( $links as $idx => $link ) {
$rel = strtolower( $link->getAttribute( 'rel' ) );
$href = $link->getAttribute( 'href' );
$as = strtolower( $link->getAttribute( 'as' ) );
if ( empty( $href ) ) {
continue;
}
$url = self::make_absolute_url( $href );
// Track Google Fonts stylesheet as fallback (remote or local Elementor cache).
if ( empty( $google_fonts ) && ( false !== strpos( $url, 'fonts.googleapis.com' ) || false !== strpos( $url, 'google-fonts/css' ) ) ) {
$google_fonts = $url;
}
if ( false === stripos( $rel, 'preload' ) && ! preg_match( '/\\.woff2?$/i', $url ) ) {
continue;
}
if ( stripos( $rel, 'preload' ) !== false && 'font' !== $as ) {
continue;
}
$score = 0 - $idx;
if ( preg_match( '/\\.woff2$/i', $url ) ) {
$score += 20;
} elseif ( preg_match( '/\\.woff$/i', $url ) ) {
$score += 10;
}
$target_host = wp_parse_url( $url, PHP_URL_HOST );
if ( $target_host ) {
// Only allow same-host or fonts.gstatic.com.
if ( $host && $target_host === $host ) {
$score += 10;
} elseif ( false !== strpos( $target_host, 'fonts.gstatic.com' ) ) {
$score += 15;
} else {
continue;
}
}
// Avoid icon fonts.
if ( strpos( $url, 'font-awesome' ) !== false || strpos( $url, 'icomoon' ) !== false || strpos( $url, 'icon' ) !== false ) {
$score -= 50;
}
if ( $score > $best_s ) {
$best_s = $score;
$best = $url;
}
$candidates[] = $url;
}
// Keep only same-host or gstatic candidates.
$candidates = array_values(
array_unique(
array_filter(
$candidates,
function ( $url ) use ( $host ) {
$th = wp_parse_url( $url, PHP_URL_HOST );
return $th && ( $th === $host || false !== strpos( $th, 'fonts.gstatic.com' ) );
}
)
)
);
// Try to fetch first WOFF2 from Google Fonts CSS (prefer variable).
if ( $google_fonts ) {
$gf_fonts = self::font_from_stylesheet( $google_fonts );
if ( ! empty( $gf_fonts ) ) {
$candidates = array_merge( $gf_fonts, $candidates );
}
}
$candidates = array_values( array_unique( $candidates ) );
return array(
'primary' => isset( $candidates[0] ) ? $candidates[0] : '',
'candidates' => $candidates,
);
}
/**
* Choose a hero-ish image: prefer hero/cover/banner keywords or larger dimensions.
*
* @param \DOMDocument $dom DOM.
* @return string
*/
private static function pick_best_image( \DOMDocument $dom ) {
$images = $dom->getElementsByTagName( 'img' );
$best = '';
$best_s = -INF;
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$keywords_positive = array( 'hero', 'cover', 'banner', 'slider', 'header', 'featured', 'main', 'masthead' );
$keywords_negative = array( 'logo', 'icon', 'avatar', 'thumb', 'thumbnail', 'emoji' );
foreach ( $images as $idx => $img ) {
$src = $img->getAttribute( 'src' );
if ( empty( $src ) ) {
continue;
}
$url = self::make_absolute_url( $src );
$score = 0 - $idx;
$cls_id = strtolower( $img->getAttribute( 'class' ) . ' ' . $img->getAttribute( 'id' ) . ' ' . $url );
foreach ( $keywords_positive as $word ) {
if ( false !== strpos( $cls_id, $word ) ) {
$score += 25;
}
}
foreach ( $keywords_negative as $word ) {
if ( false !== strpos( $cls_id, $word ) ) {
$score -= 30;
}
}
$w = (int) $img->getAttribute( 'width' );
$h = (int) $img->getAttribute( 'height' );
if ( $w >= 1200 || $h >= 800 ) {
$score += 30;
} elseif ( $w >= 800 || $h >= 600 ) {
$score += 20;
} elseif ( $w >= 400 || $h >= 300 ) {
$score += 10;
} else {
$score -= 5;
}
$target_host = wp_parse_url( $url, PHP_URL_HOST );
if ( $target_host && $host && $target_host === $host ) {
$score += 5;
}
if ( $score > $best_s ) {
$best_s = $score;
$best = $url;
}
}
return $best;
}
/**
* Safe, single-font detector following conservative rules.
*
* @param \DOMDocument $dom DOM.
* @return array {primary, candidates}
*/
private static function pick_best_font_v2( \DOMDocument $dom ) {
$links = $dom->getElementsByTagName( 'link' );
$styles = $dom->getElementsByTagName( 'style' );
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$google_fonts = array();
$primary_family = '';
$priority_selectors = array( 'body', 'html', 'h1', '.entry-title', '.site-title', '.elementor-heading-title' );
foreach ( $styles as $style ) {
$css = $style->textContent;
foreach ( $priority_selectors as $selector ) {
if ( preg_match( '/' . preg_quote( $selector, '/' ) . '\\s*\\{[^}]*font-family\\s*:\\s*([^;}]+)[;}]/i', $css, $m ) ) {
$families = explode( ',', $m[1] );
if ( ! empty( $families[0] ) ) {
$family = trim( $families[0], "\"' \t\r\n" );
if ( ! in_array( strtolower( $family ), array( 'sans-serif', 'serif', 'monospace', 'inherit' ), true ) ) {
$primary_family = $family;
break 2;
}
}
}
}
}
foreach ( $links as $link ) {
$rel = strtolower( $link->getAttribute( 'rel' ) );
$href = $link->getAttribute( 'href' );
if ( false === strpos( $rel, 'stylesheet' ) || empty( $href ) ) {
continue;
}
$url = self::make_absolute_url( $href );
if ( false !== strpos( $url, 'fonts.googleapis.com' ) || false !== strpos( $url, 'google-fonts/css' ) ) {
$google_fonts[] = $url;
if ( empty( $primary_family ) && preg_match( '/family=([^:&]+)/', $url, $m ) ) {
$family = urldecode( str_replace( '+', ' ', $m[1] ) );
$primary_family = $family;
}
}
}
if ( empty( $primary_family ) ) {
return array(
'primary' => '',
'candidates' => array(),
);
}
$candidates = array();
foreach ( $google_fonts as $css_url ) {
$fonts = self::font_from_stylesheet( $css_url, $primary_family );
if ( ! empty( $fonts ) ) {
foreach ( $fonts as $f ) {
$th = wp_parse_url( $f, PHP_URL_HOST );
if ( $th && ( $th === $host || false !== strpos( $th, 'fonts.gstatic.com' ) ) ) {
$candidates[] = $f;
}
}
}
}
$candidates = array_values( array_unique( $candidates ) );
if ( count( $candidates ) > 1 ) {
foreach ( $candidates as $cand ) {
if ( false !== stripos( $cand, 'var' ) ) {
$candidates = array( $cand );
break;
}
}
}
$primary = ( count( $candidates ) === 1 ) ? $candidates[0] : '';
return array(
'primary' => $primary,
'candidates' => $candidates,
);
}
/**
* Fetch a stylesheet and return the first usable woff2 from fonts.gstatic.com (prefer variable).
*
* @param string $url Stylesheet URL.
* @param string $family Target family (unused currently, kept for clarity).
* @return string
*/
private static function font_from_stylesheet( $url, $family = '' ) {
$response = wp_remote_get(
$url,
array(
'timeout' => 6,
'redirection' => 2,
)
);
if ( is_wp_error( $response ) ) {
return '';
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code >= 400 ) {
return '';
}
$body = wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
return '';
}
$urls = array();
preg_match_all( '/url\\(([^)]+)\\)/i', $body, $matches );
if ( ! empty( $matches[1] ) ) {
foreach ( $matches[1] as $candidate ) {
$candidate = trim( $candidate, "\"' \t\n\r\0\x0B" );
if ( preg_match( '/\\.woff2(\\?|$)/i', $candidate ) ) {
$target_host = wp_parse_url( $candidate, PHP_URL_HOST );
if ( $target_host && false !== strpos( $target_host, 'fonts.gstatic.com' ) ) {
$urls[] = $candidate;
}
}
}
}
if ( empty( $urls ) ) {
return array();
}
// Prefer variable font if present.
foreach ( $urls as $candidate ) {
if ( false !== stripos( $candidate, 'var' ) ) {
return array( $candidate );
}
}
return array( $urls[0] );
}
/**
* Add contextual help tab.
*
* @return void
*/
public static function add_help_tab() {
$screen = get_current_screen();
if ( ! $screen || 'settings_page_i365-po-settings' !== $screen->id ) {
return;
}
$screen->add_help_tab(
array(
'id' => 'i365-po-help',
'title' => __( 'Performance Optimizer Help', '365i-performance-optimizer' ),
'content' => '' . esc_html__( 'Toggle the options to match your site: enable speculation for faster navigation, set preconnect/preload URLs to your theme assets, defer non-core scripts, and refine image loading.', '365i-performance-optimizer' ) . '
' .
'' . esc_html__( 'Use Auto-detect to fill fields from your active theme. Exclude checkout/cart paths from speculation. Save to apply changes; Restore defaults to undo.', '365i-performance-optimizer' ) . '
',
)
);
}
}