PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.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-search-console-client.php

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

432 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google Search Console Client Class
4 *
5 * Handles communication with Google Search Console API for search performance
6 * data retrieval and site verification. Extends the base Google API client with
7 * Search Console-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 Search Console Client Class
24 *
25 * Single Responsibility: Handle Google Search Console API communication
26 * Following ThinkRank HTTP client patterns from Claude_Client and OpenAI_Client
27 *
28 * @since 1.0.0
29 */
30 class Google_Search_Console_Client extends Google_API_Base_Client {
31
32 /**
33 * Google Search Console API base URL
34 */
35 private const API_BASE_URL = 'https://www.googleapis.com/webmasters/v3';
36
37 /**
38 * Rate limit transient key prefix
39 * Following ThinkRank option naming patterns
40 */
41 private const RATE_LIMIT_KEY = 'thinkrank_gsc_rate_limit';
42
43 /**
44 * Maximum requests per day (Google standard quota)
45 */
46 private const MAX_REQUESTS_PER_DAY = 2000;
47
48 /**
49 * Test API connection
50 * Following ThinkRank test_connection patterns from AI clients
51 *
52 * @return array Connection test results
53 */
54 public function test_connection(): array {
55 try {
56 $result = $this->list_sites();
57
58 return [
59 'success' => true,
60 'message' => 'Google Search Console API connection successful',
61 'sites_count' => count($result['siteEntry'] ?? [])
62 ];
63 } catch (\Exception $e) {
64 return [
65 'success' => false,
66 'error' => $e->getMessage()
67 ];
68 }
69 }
70
71 /**
72 * List verified sites in Search Console
73 *
74 * @return array List of verified sites
75 * @throws \Exception If API request fails
76 */
77 public function list_sites(): array {
78 $endpoint = '/sites';
79 $params = ['key' => $this->api_key];
80
81 $full_url = self::API_BASE_URL . $endpoint;
82 return $this->make_request($full_url, $params, 'GET');
83 }
84
85 /**
86 * Verify site ownership in Search Console
87 *
88 * @param string $site_url Site URL to verify
89 * @param string $verification_method Verification method used
90 * @return array Verification results
91 */
92 public function verify_site(string $site_url, string $verification_method = 'meta'): array {
93 try {
94 // Check if the site is already verified by listing sites
95 $sites = $this->list_sites();
96 $site_verified = false;
97
98 foreach ($sites['siteEntry'] ?? [] as $site) {
99 if ($site['siteUrl'] === $site_url) {
100 $site_verified = true;
101 break;
102 }
103 }
104
105 return [
106 'success' => $site_verified,
107 'message' => $site_verified ? 'Site is verified in Search Console' : 'Site not found in Search Console',
108 'site_url' => $site_url,
109 'verification_method' => $verification_method
110 ];
111 } catch (\Exception $e) {
112 return [
113 'success' => false,
114 'error' => $e->getMessage()
115 ];
116 }
117 }
118
119 /**
120 * Get search performance data with flexible dimensions
121 *
122 * @param string $site_url Site URL to get data for
123 * @param string $date_range Date range ('7d', '30d', '90d')
124 * @param array $dimensions Dimensions to group by (query, page, country, device, searchAppearance)
125 * @param int $row_limit Maximum number of rows to return
126 * @return array Search performance data
127 * @throws \Exception If API request fails
128 */
129 public function get_search_performance(string $site_url, string $date_range = '30d', array $dimensions = ['query'], int $row_limit = 100): array {
130 // Calculate date range
131 $end_date = gmdate('Y-m-d');
132 $days = (int) str_replace('d', '', $date_range);
133 $start_date = gmdate('Y-m-d', strtotime("-{$days} days"));
134
135 $endpoint = '/sites/' . urlencode($site_url) . '/searchAnalytics/query';
136
137 $request_body = [
138 'startDate' => $start_date,
139 'endDate' => $end_date,
140 'dimensions' => $dimensions,
141 'rowLimit' => $row_limit
142 ];
143
144 $full_url = self::API_BASE_URL . $endpoint . '?key=' . $this->api_key;
145 return $this->make_request($full_url, $request_body, 'POST');
146 }
147
148 /**
149 * Get top search queries
150 *
151 * @param string $site_url Site URL to get data for
152 * @param int $limit Number of queries to retrieve
153 * @return array Top search queries
154 * @throws \Exception If API request fails
155 */
156 public function get_top_queries(string $site_url, int $limit = 10): array {
157 try {
158 $performance_data = $this->get_search_performance($site_url, '30d');
159 $queries = [];
160
161 foreach ($performance_data['rows'] ?? [] as $row) {
162 if (count($queries) >= $limit) {
163 break;
164 }
165
166 $queries[] = [
167 'query' => $row['keys'][0] ?? '',
168 'clicks' => $row['clicks'] ?? 0,
169 'impressions' => $row['impressions'] ?? 0,
170 'ctr' => $row['ctr'] ?? 0,
171 'position' => $row['position'] ?? 0
172 ];
173 }
174
175 return [
176 'queries' => $queries,
177 'site_url' => $site_url,
178 'limit' => $limit
179 ];
180 } catch (\Exception $e) {
181 return [
182 'queries' => [],
183 'site_url' => $site_url,
184 'limit' => $limit,
185 'error' => $e->getMessage()
186 ];
187 }
188 }
189
190 /**
191 * Get page performance data for SEO analytics
192 *
193 * @param string $site_url Site URL to get data for
194 * @param string $date_range Date range for data
195 * @param int $limit Number of pages to retrieve
196 * @return array Page performance data
197 * @throws \Exception If API request fails
198 */
199 public function get_page_performance(string $site_url, string $date_range = '30d', int $limit = 25): array {
200 $result = $this->get_search_performance($site_url, $date_range, ['page'], $limit);
201
202 $pages = [];
203 $rows = $result['rows'] ?? [];
204
205 foreach ($rows as $row) {
206 $pages[] = [
207 'page' => $row['keys'][0] ?? '',
208 'clicks' => $row['clicks'] ?? 0,
209 'impressions' => $row['impressions'] ?? 0,
210 'ctr' => round(($row['ctr'] ?? 0) * 100, 2), // Convert to percentage
211 'position' => round($row['position'] ?? 0, 1)
212 ];
213 }
214
215 return [
216 'pages' => $pages,
217 'site_url' => $site_url,
218 'date_range' => $date_range,
219 'total_pages' => count($pages)
220 ];
221 }
222
223 /**
224 * Get device performance breakdown for mobile SEO insights
225 *
226 * @param string $site_url Site URL to get data for
227 * @param string $date_range Date range for data
228 * @return array Device performance data
229 * @throws \Exception If API request fails
230 */
231 public function get_device_performance(string $site_url, string $date_range = '30d'): array {
232 $result = $this->get_search_performance($site_url, $date_range, ['device'], 10);
233
234 $devices = [];
235 $rows = $result['rows'] ?? [];
236
237 foreach ($rows as $row) {
238 $device = $row['keys'][0] ?? '';
239 $devices[$device] = [
240 'clicks' => $row['clicks'] ?? 0,
241 'impressions' => $row['impressions'] ?? 0,
242 'ctr' => round(($row['ctr'] ?? 0) * 100, 2),
243 'position' => round($row['position'] ?? 0, 1)
244 ];
245 }
246
247 return [
248 'devices' => $devices,
249 'site_url' => $site_url,
250 'date_range' => $date_range
251 ];
252 }
253
254 /**
255 * Get search appearance data for rich results tracking
256 *
257 * @param string $site_url Site URL to get data for
258 * @param string $date_range Date range for data
259 * @return array Search appearance data
260 * @throws \Exception If API request fails
261 */
262 public function get_search_appearance(string $site_url, string $date_range = '30d'): array {
263 $result = $this->get_search_performance($site_url, $date_range, ['searchAppearance'], 20);
264
265 $appearances = [];
266 $rows = $result['rows'] ?? [];
267
268 foreach ($rows as $row) {
269 $appearance = $row['keys'][0] ?? '';
270 $appearances[$appearance] = [
271 'clicks' => $row['clicks'] ?? 0,
272 'impressions' => $row['impressions'] ?? 0,
273 'ctr' => round(($row['ctr'] ?? 0) * 100, 2),
274 'position' => round($row['position'] ?? 0, 1)
275 ];
276 }
277
278 return [
279 'appearances' => $appearances,
280 'site_url' => $site_url,
281 'date_range' => $date_range
282 ];
283 }
284
285 /**
286 * Get site indexing status and coverage data
287 *
288 * @param string $site_url Site URL to check
289 * @return array Indexing status and coverage data
290 * @throws \Exception If API request fails
291 */
292 public function get_indexing_status(string $site_url): array {
293 try {
294 // Get overall search performance to estimate indexed pages
295 $performance = $this->get_search_performance($site_url, '30d', ['page'], 1000);
296 $indexed_pages = count($performance['rows'] ?? []);
297
298 // Get basic site info
299 $sites = $this->list_sites();
300 $site_info = null;
301
302 foreach ($sites['siteEntry'] ?? [] as $site) {
303 if ($site['siteUrl'] === $site_url) {
304 $site_info = $site;
305 break;
306 }
307 }
308
309 return [
310 'site_url' => $site_url,
311 'is_verified' => !is_null($site_info),
312 'indexed_pages_estimate' => $indexed_pages,
313 'permission_level' => $site_info['permissionLevel'] ?? 'none',
314 'last_updated' => gmdate('Y-m-d H:i:s')
315 ];
316 } catch (\Exception $e) {
317 return [
318 'site_url' => $site_url,
319 'is_verified' => false,
320 'indexed_pages_estimate' => 0,
321 'permission_level' => 'none',
322 'error' => $e->getMessage(),
323 'last_updated' => gmdate('Y-m-d H:i:s')
324 ];
325 }
326 }
327
328 /**
329 * Get keyword opportunities for SEO insights
330 * Identifies queries with high impressions but low CTR or position
331 *
332 * @param string $site_url Site URL to analyze
333 * @param string $date_range Date range for analysis
334 * @param int $min_impressions Minimum impressions threshold
335 * @return array Keyword opportunities
336 * @throws \Exception If API request fails
337 */
338 public function get_keyword_opportunities(string $site_url, string $date_range = '30d', int $min_impressions = 100): array {
339 $result = $this->get_search_performance($site_url, $date_range, ['query'], 500);
340
341 $opportunities = [];
342 $rows = $result['rows'] ?? [];
343
344 foreach ($rows as $row) {
345 $impressions = $row['impressions'] ?? 0;
346 $ctr = $row['ctr'] ?? 0;
347 $position = $row['position'] ?? 0;
348 $clicks = $row['clicks'] ?? 0;
349
350 // Identify opportunities: high impressions, low CTR, or position 4-10
351 if ($impressions >= $min_impressions) {
352 $opportunity_score = 0;
353 $opportunity_reasons = [];
354
355 // Low CTR opportunity
356 if ($ctr < 0.05 && $position <= 10) { // Less than 5% CTR in top 10
357 $opportunity_score += 30;
358 $opportunity_reasons[] = 'Low CTR for top 10 position';
359 }
360
361 // Position 4-10 opportunity (could reach top 3)
362 if ($position >= 4 && $position <= 10) {
363 $opportunity_score += 40;
364 $opportunity_reasons[] = 'Ranking 4-10, potential for top 3';
365 }
366
367 // High impressions, low clicks
368 if ($impressions > 500 && $clicks < 25) {
369 $opportunity_score += 20;
370 $opportunity_reasons[] = 'High impressions but low clicks';
371 }
372
373 if ($opportunity_score > 0) {
374 $opportunities[] = [
375 'query' => $row['keys'][0] ?? '',
376 'clicks' => $clicks,
377 'impressions' => $impressions,
378 'ctr' => round($ctr * 100, 2),
379 'position' => round($position, 1),
380 'opportunity_score' => $opportunity_score,
381 'reasons' => $opportunity_reasons
382 ];
383 }
384 }
385 }
386
387 // Sort by opportunity score (highest first)
388 usort($opportunities, function($a, $b) {
389 return $b['opportunity_score'] <=> $a['opportunity_score'];
390 });
391
392 return [
393 'opportunities' => array_slice($opportunities, 0, 50), // Top 50 opportunities
394 'site_url' => $site_url,
395 'date_range' => $date_range,
396 'total_opportunities' => count($opportunities)
397 ];
398 }
399
400 /**
401 * Get rate limit configuration
402 * Following ThinkRank rate limiting patterns
403 *
404 * @return array Rate limit configuration
405 */
406 protected function get_rate_limits(): array {
407 return [
408 'max_requests_per_day' => self::MAX_REQUESTS_PER_DAY,
409 'reset_time' => get_transient(self::RATE_LIMIT_KEY . '_reset') ?: strtotime('tomorrow')
410 ];
411 }
412
413 /**
414 * Get rate limit transient key
415 * Following ThinkRank option naming patterns
416 *
417 * @return string Rate limit key
418 */
419 protected function get_rate_limit_key(): string {
420 return self::RATE_LIMIT_KEY;
421 }
422
423 /**
424 * Get rate limit error message
425 *
426 * @return string Error message
427 */
428 protected function get_rate_limit_error_message(): string {
429 return 'Google Search Console API rate limit exceeded. Try again tomorrow.';
430 }
431 }
432