*/ private static function snapshot( ?array $totals_override = null ): array { $opts = static function ( string $slug ): array { return (array) get_option( 'xspeed_module_' . $slug, array() ); }; if ( null !== $totals_override ) { $totals = $totals_override; } elseif ( class_exists( '\\XSpeed\\Hit_Counter' ) ) { $totals = Hit_Counter::totals_24h(); } else { $totals = array( 'hits' => 0, 'misses' => 0, 'ratio' => 0.0 ); } return array( 'cache' => $opts( 'cache' ), 'minify' => $opts( 'minify' ), 'lazy' => $opts( 'lazy' ), 'gzip' => $opts( 'gzip' ), 'browser_cache' => $opts( 'browser-cache' ), 'cloudflare' => $opts( 'cloudflare' ), 'cdn' => $opts( 'cdn' ), 'database' => $opts( 'database' ), 'preloader' => $opts( 'preloader' ), 'heartbeat' => $opts( 'heartbeat' ), 'cache_enabled' => class_exists( '\\XSpeed\\Settings' ) ? ! empty( Settings::get()['cache_enabled'] ) : false, 'totals_24h' => array( 'hits' => (int) ( $totals['hits'] ?? 0 ), 'misses' => (int) ( $totals['misses'] ?? 0 ), 'total' => (int) ( $totals['hits'] ?? 0 ) + (int) ( $totals['misses'] ?? 0 ), 'ratio' => (float) ( $totals['ratio'] ?? 0.0 ), ), ); } /** * @param array|null $totals_override See snapshot(). Production * callers pass nothing. * * @return array */ public static function run( ?array $totals_override = null ): array { $s = self::snapshot( $totals_override ); $out = array(); // Rule 1 — Cloudflare connected but APO not in use. // High signal: user already pays the Cloudflare overhead, APO // is the highest-leverage Pro feature they can flip on next. if ( ! empty( $s['cloudflare']['enabled'] ) ) { $out[] = array( 'id' => 'cloudflare-apo', 'severity' => self::SEVERITY_HIGH, 'reason' => 'Cloudflare is already connected. Pro adds Automatic Platform Optimization, which edge-caches your HTML — typically cuts TTFB in half.', 'fact' => 'Cloudflare on', ); } // Rule 2 — Low cache hit ratio with meaningful traffic. // "Meaningful" = > 50 hits over 24h; below that the ratio is // statistical noise and we'd suggest based on bad data. if ( $s['totals_24h']['total'] >= 50 && $s['totals_24h']['ratio'] < 0.5 ) { $pct = (int) round( $s['totals_24h']['ratio'] * 100 ); $out[] = array( 'id' => 'recommendations', 'severity' => self::SEVERITY_HIGH, 'reason' => sprintf( 'Cache hit ratio is %d%% over the last 24h. Pro Recommendations identifies which URLs miss the cache and why, with one-click fixes.', $pct ), 'fact' => $pct . '% hit', ); } // Rule 3 — Lazy-load enabled but no auto WebP/AVIF. // User cares about images (lazy on) → next gain is format. if ( ! empty( $s['lazy']['lazy_images'] ) ) { $out[] = array( 'id' => 'webp-avif', 'severity' => self::SEVERITY_MED, 'reason' => 'Images are lazy-loaded. Pro auto-converts new JPEG/PNG uploads to WebP and AVIF — typically 25-35% smaller at the same visual quality.', ); } // Rule 4 — High traffic without RUM data. // Real-user metrics matter more than synthetic Lighthouse when // the site has actual visitors. if ( $s['totals_24h']['total'] >= 100 ) { $views = number_format( $s['totals_24h']['total'] ); $out[] = array( 'id' => 'rum', 'severity' => self::SEVERITY_MED, 'reason' => sprintf( 'You served %s requests in 24h. Pro RUM samples actual LCP, CLS and INP from those visitors — Lighthouse only simulates one device, one connection.', $views ), 'fact' => $views . ' / 24h', ); } // Rule 5 — HTML minify on but JS minify off (theme-safe stance). // Suggest Critical CSS as the next gain that doesn't touch JS. if ( ! empty( $s['minify']['minify_html'] ) && empty( $s['minify']['minify_js'] ) ) { $out[] = array( 'id' => 'critical-css', 'severity' => self::SEVERITY_MED, 'reason' => 'JS minify is off (good — high theme-conflict risk). Pro Critical CSS delivers similar first-paint gains without touching JavaScript.', ); } // Rule 6 — Database cleanup on manual schedule. // Only fire when the user has actually configured the Database // module (has saved options). Empty option = user hasn't // touched it; don't suggest scheduling something they might // never use. if ( ! empty( $s['database'] ) && 'manual' === ( $s['database']['schedule'] ?? 'manual' ) ) { $out[] = array( 'id' => 'recommendations', 'severity' => self::SEVERITY_LOW, 'reason' => 'Database cleanup is set to manual. Pro Recommendations engine auto-schedules cleanups based on smart triggers (after publish, before backup).', ); } // Rule 7 — Agency / professional usage signal. // >= 5 enabled modules suggests serious use → white-label is // what they'd actually want next. $enabled = 0; foreach ( array( 'minify', 'gzip', 'lazy', 'browser_cache', 'cloudflare', 'cdn', 'preloader' ) as $k ) { if ( ! empty( $s[ $k ]['enabled'] ) ) { $enabled++; } } if ( $s['cache_enabled'] ) { $enabled++; } if ( $enabled >= 5 ) { $out[] = array( 'id' => 'white-label', 'severity' => self::SEVERITY_LOW, 'reason' => sprintf( 'You\'ve configured %d modules — looks like agency work. Pro White-Label rebrands the dashboard chrome for client handoff.', $enabled ), 'fact' => $enabled . ' modules', ); } // Fallback — never return an empty audit. Analytics is the // safe always-relevant suggestion (every site has cache // activity to chart). if ( empty( $out ) ) { $out[] = array( 'id' => 'analytics', 'severity' => self::SEVERITY_LOW, 'reason' => 'See which pages benefit most from caching, where your slow URLs are, and your hit-ratio over time.', ); } // Dedupe by id, keeping the highest-severity rule per feature. // Rules independently suggest the same feature for different // reasons; pick the strongest reason to show. $by_id = array(); $order = array( self::SEVERITY_HIGH => 0, self::SEVERITY_MED => 1, self::SEVERITY_LOW => 2 ); foreach ( $out as $row ) { $id = $row['id']; if ( ! isset( $by_id[ $id ] ) ) { $by_id[ $id ] = $row; continue; } $existing_rank = $order[ $by_id[ $id ]['severity'] ] ?? 9; $new_rank = $order[ $row['severity'] ] ?? 9; if ( $new_rank < $existing_rank ) { $by_id[ $id ] = $row; } } $out = array_values( $by_id ); usort( $out, static function ( $a, $b ) use ( $order ) { return ( $order[ $a['severity'] ] ?? 9 ) <=> ( $order[ $b['severity'] ] ?? 9 ); } ); return $out; } }