PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.1.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / integrations / class-google-pagespeed-client.php

class-google-pagespeed-client.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.1.0, at includes/integrations/class-google-pagespeed-client.php

361 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google PageSpeed Insights Client Class
4 *
5 * Handles communication with Google PageSpeed Insights API for Core Web Vitals
6 * and performance data retrieval. Extends the base Google API client with
7 * PageSpeed-specific functionality and rate limiting.
8 *
9 * @package ThinkRank\Integrations
10 * @since 1.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\Integrations;
16
17 // Prevent direct access
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * Google PageSpeed Insights Client Class
24 *
25 * Single Responsibility: Handle PageSpeed Insights API communication
26 * Following ThinkRank HTTP client patterns from Claude_Client and OpenAI_Client
27 *
28 * @since 1.0.0
29 */
30 class Google_PageSpeed_Client extends Google_API_Base_Client {
31
32 /**
33 * PageSpeed Insights API base URL
34 */
35 private const API_BASE_URL = 'https://www.googleapis.com/pagespeedonline/v5';
36
37 /**
38 * Rate limit transient key prefix
39 * Following ThinkRank option naming patterns
40 */
41 private const RATE_LIMIT_KEY = 'thinkrank_pagespeed_rate_limit';
42
43 /**
44 * Maximum requests per day (Google free tier limit)
45 */
46 private const MAX_REQUESTS_PER_DAY = 25000;
47
48 /**
49 * Run PageSpeed test for a URL
50 *
51 * @param string $url URL to test
52 * @param string $strategy Device strategy ('mobile' or 'desktop')
53 * @param array $categories Categories to test (default: ['performance'])
54 * @return array PageSpeed test results
55 * @throws \Exception If API request fails
56 */
57 public function run_pagespeed_test(string $url, string $strategy = 'mobile', array $categories = ['performance']): array {
58 $endpoint = '/runPagespeed';
59 $params = [
60 'url' => $url,
61 'strategy' => $strategy,
62 'category' => $categories,
63 'key' => $this->api_key
64 ];
65
66 $full_url = self::API_BASE_URL . $endpoint;
67 return $this->make_request($full_url, $params, 'GET');
68 }
69
70 /**
71 * Test API connection
72 * Following ThinkRank test_connection patterns from AI clients
73 *
74 * @return array Connection test results
75 */
76 public function test_connection(): array {
77 try {
78 $test_url = home_url();
79 $result = $this->run_pagespeed_test($test_url, 'mobile', ['performance']);
80
81 $performance_score = 0;
82 if (isset($result['lighthouseResult']['categories']['performance']['score'])) {
83 $performance_score = $result['lighthouseResult']['categories']['performance']['score'] * 100;
84 }
85
86 return [
87 'success' => true,
88 'message' => 'PageSpeed Insights API connection successful',
89 'test_url' => $test_url,
90 'performance_score' => $performance_score
91 ];
92 } catch (\Exception $e) {
93 return [
94 'success' => false,
95 'error' => $e->getMessage()
96 ];
97 }
98 }
99
100 /**
101 * Get Core Web Vitals data for a URL
102 *
103 * @param string $url URL to analyze
104 * @param string $strategy Device strategy ('mobile' or 'desktop')
105 * @return array Core Web Vitals data
106 * @throws \Exception If API request fails
107 */
108 public function get_core_web_vitals(string $url, string $strategy = 'mobile'): array {
109 $result = $this->run_pagespeed_test($url, $strategy, ['performance']);
110 return $this->parse_core_web_vitals($result);
111 }
112
113 /**
114 * Get performance opportunities for a URL
115 *
116 * @param string $url URL to test
117 * @param string $strategy Testing strategy (mobile/desktop)
118 * @return array Performance opportunities
119 * @throws \Exception If API request fails
120 */
121 public function get_opportunities(string $url, string $strategy = 'mobile'): array {
122 $result = $this->run_pagespeed_test($url, $strategy, ['performance']);
123 return $this->parse_opportunities($result);
124 }
125
126 /**
127 * Get diagnostic information for a URL
128 *
129 * @param string $url URL to test
130 * @param string $strategy Testing strategy (mobile/desktop)
131 * @return array Diagnostic information
132 * @throws \Exception If API request fails
133 */
134 public function get_diagnostics(string $url, string $strategy = 'mobile'): array {
135 $result = $this->run_pagespeed_test($url, $strategy, ['performance']);
136 return $this->parse_diagnostics($result);
137 }
138
139 /**
140 * Parse Core Web Vitals from PageSpeed response
141 *
142 * @param array $pagespeed_data Raw PageSpeed API response
143 * @return array Parsed Core Web Vitals data
144 */
145 private function parse_core_web_vitals(array $pagespeed_data): array {
146 $audits = $pagespeed_data['lighthouseResult']['audits'] ?? [];
147
148 return [
149 'lcp' => [
150 'name' => 'Largest Contentful Paint',
151 'value' => round((($audits['largest-contentful-paint']['numericValue'] ?? 0) / 1000), 4),
152 'score' => ($audits['largest-contentful-paint']['score'] ?? 0) * 100,
153 'unit' => 's',
154 'good_threshold' => 2.5,
155 'needs_improvement_threshold' => 4.0,
156 'description' => 'Time until the largest content element is rendered'
157 ],
158 'fid' => [
159 'name' => 'First Input Delay',
160 'value' => round(($audits['max-potential-fid']['numericValue'] ?? 0), 4),
161 'score' => ($audits['max-potential-fid']['score'] ?? 0) * 100,
162 'unit' => 'ms',
163 'good_threshold' => 100,
164 'needs_improvement_threshold' => 300,
165 'description' => 'Time from first user interaction to browser response'
166 ],
167 'cls' => [
168 'name' => 'Cumulative Layout Shift',
169 'value' => round(($audits['cumulative-layout-shift']['numericValue'] ?? 0), 4),
170 'score' => ($audits['cumulative-layout-shift']['score'] ?? 0) * 100,
171 'unit' => '',
172 'good_threshold' => 0.1,
173 'needs_improvement_threshold' => 0.25,
174 'description' => 'Measure of visual stability during page load'
175 ],
176 'fcp' => [
177 'name' => 'First Contentful Paint',
178 'value' => round((($audits['first-contentful-paint']['numericValue'] ?? 0) / 1000), 4),
179 'score' => ($audits['first-contentful-paint']['score'] ?? 0) * 100,
180 'unit' => 's',
181 'good_threshold' => 1.8,
182 'needs_improvement_threshold' => 3.0,
183 'description' => 'Time until the first content is painted on screen'
184 ]
185 ];
186 }
187
188 /**
189 * Parse performance opportunities from PageSpeed data
190 *
191 * @param array $pagespeed_data Raw PageSpeed API response
192 * @return array Parsed opportunities data
193 */
194 private function parse_opportunities(array $pagespeed_data): array {
195 $audits = $pagespeed_data['lighthouseResult']['audits'] ?? [];
196 $opportunities = [];
197
198 // Define opportunity audits that provide savings estimates
199 $opportunity_audits = [
200 'render-blocking-resources' => 'Eliminate render-blocking resources',
201 'unused-css-rules' => 'Remove unused CSS',
202 'unused-javascript' => 'Remove unused JavaScript',
203 'modern-image-formats' => 'Serve images in next-gen formats',
204 'offscreen-images' => 'Defer offscreen images',
205 'unminified-css' => 'Minify CSS',
206 'unminified-javascript' => 'Minify JavaScript',
207 'efficient-animated-content' => 'Use video formats for animated content',
208 'duplicated-javascript' => 'Remove duplicate modules in JavaScript bundles',
209 'legacy-javascript' => 'Avoid serving legacy JavaScript to modern browsers'
210 ];
211
212 foreach ($opportunity_audits as $audit_id => $title) {
213 if (isset($audits[$audit_id]) && isset($audits[$audit_id]['details'])) {
214 $audit = $audits[$audit_id];
215 $savings = $audit['details']['overallSavingsMs'] ?? 0;
216
217 if ($savings > 0) {
218 $opportunities[] = [
219 'id' => $audit_id,
220 'title' => $title,
221 'description' => $audit['description'] ?? '',
222 'estimated_savings' => $savings,
223 'score' => ($audit['score'] ?? 0) * 100,
224 'details' => $audit['details'] ?? [],
225 'difficulty' => $this->get_difficulty_level($audit_id)
226 ];
227 }
228 }
229 }
230
231 // Sort by estimated savings (highest first)
232 usort($opportunities, function($a, $b) {
233 return $b['estimated_savings'] - $a['estimated_savings'];
234 });
235
236 return $opportunities;
237 }
238
239 /**
240 * Parse diagnostic information from PageSpeed data
241 *
242 * @param array $pagespeed_data Raw PageSpeed API response
243 * @return array Parsed diagnostics data
244 */
245 private function parse_diagnostics(array $pagespeed_data): array {
246 $audits = $pagespeed_data['lighthouseResult']['audits'] ?? [];
247 $diagnostics = [];
248
249 // Define diagnostic audits
250 $diagnostic_audits = [
251 'first-contentful-paint' => ['title' => 'First Contentful Paint', 'impact' => 'Performance'],
252 'largest-contentful-paint' => ['title' => 'Largest Contentful Paint', 'impact' => 'LCP'],
253 'first-meaningful-paint' => ['title' => 'First Meaningful Paint', 'impact' => 'Performance'],
254 'speed-index' => ['title' => 'Speed Index', 'impact' => 'Performance'],
255 'total-blocking-time' => ['title' => 'Total Blocking Time', 'impact' => 'Performance'],
256 'max-potential-fid' => ['title' => 'Max Potential First Input Delay', 'impact' => 'FID'],
257 'cumulative-layout-shift' => ['title' => 'Cumulative Layout Shift', 'impact' => 'CLS'],
258 'server-response-time' => ['title' => 'Initial server response time was short', 'impact' => 'Performance'],
259 'interactive' => ['title' => 'Time to Interactive', 'impact' => 'Performance'],
260 'user-timings' => ['title' => 'User Timing marks and measures', 'impact' => 'Performance'],
261 'critical-request-chains' => ['title' => 'Avoid chaining critical requests', 'impact' => 'Performance'],
262 'redirects' => ['title' => 'Avoid multiple page redirects', 'impact' => 'Performance'],
263 'installable-manifest' => ['title' => 'Web app manifest meets the installability requirements', 'impact' => 'PWA'],
264 'apple-touch-icon' => ['title' => 'Provides a valid apple-touch-icon', 'impact' => 'PWA'],
265 'splash-screen' => ['title' => 'Configured for a custom splash screen', 'impact' => 'PWA'],
266 'themed-omnibox' => ['title' => 'Sets a theme color for the address bar', 'impact' => 'PWA'],
267 'content-width' => ['title' => 'Content is sized correctly for the viewport', 'impact' => 'Mobile'],
268 'image-aspect-ratio' => ['title' => 'Displays images with correct aspect ratio', 'impact' => 'Layout'],
269 'image-size-responsive' => ['title' => 'Serves images with appropriate resolution', 'impact' => 'Performance'],
270 'preload-fonts' => ['title' => 'Fonts with font-display: optional are preloaded', 'impact' => 'Performance'],
271 'font-display' => ['title' => 'All text remains visible during webfont loads', 'impact' => 'Performance']
272 ];
273
274 foreach ($diagnostic_audits as $audit_id => $config) {
275 if (isset($audits[$audit_id])) {
276 $audit = $audits[$audit_id];
277 $score = $audit['score'] ?? null;
278
279 $status = 'info';
280 if ($score !== null) {
281 if ($score >= 0.9) {
282 $status = 'passed';
283 } elseif ($score >= 0.5) {
284 $status = 'warning';
285 } else {
286 $status = 'failed';
287 }
288 }
289
290 $diagnostics[] = [
291 'id' => $audit_id,
292 'title' => $config['title'],
293 'description' => $audit['description'] ?? '',
294 'status' => $status,
295 'score' => $score ? ($score * 100) : null,
296 'impact' => $config['impact'],
297 'details' => $audit['details'] ?? [],
298 'display_value' => $audit['displayValue'] ?? null
299 ];
300 }
301 }
302
303 return $diagnostics;
304 }
305
306 /**
307 * Get difficulty level for optimization opportunities
308 *
309 * @param string $audit_id Audit identifier
310 * @return string Difficulty level
311 */
312 private function get_difficulty_level(string $audit_id): string {
313 $difficulty_map = [
314 'unminified-css' => 'Easy',
315 'unminified-javascript' => 'Easy',
316 'modern-image-formats' => 'Easy',
317 'offscreen-images' => 'Medium',
318 'unused-css-rules' => 'Hard',
319 'unused-javascript' => 'Hard',
320 'render-blocking-resources' => 'Medium',
321 'efficient-animated-content' => 'Medium',
322 'duplicated-javascript' => 'Hard',
323 'legacy-javascript' => 'Medium'
324 ];
325
326 return $difficulty_map[$audit_id] ?? 'Medium';
327 }
328
329 /**
330 * Get rate limit configuration
331 * Following ThinkRank rate limiting patterns
332 *
333 * @return array Rate limit configuration
334 */
335 protected function get_rate_limits(): array {
336 return [
337 'max_requests_per_day' => self::MAX_REQUESTS_PER_DAY,
338 'reset_time' => get_transient(self::RATE_LIMIT_KEY . '_reset') ?: strtotime('tomorrow')
339 ];
340 }
341
342 /**
343 * Get rate limit transient key
344 * Following ThinkRank option naming patterns
345 *
346 * @return string Rate limit key
347 */
348 protected function get_rate_limit_key(): string {
349 return self::RATE_LIMIT_KEY;
350 }
351
352 /**
353 * Get rate limit error message
354 *
355 * @return string Error message
356 */
357 protected function get_rate_limit_error_message(): string {
358 return 'PageSpeed Insights API rate limit exceeded. Try again tomorrow.';
359 }
360 }
361