PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.2
2.14.3 2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.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 All 58 releases
thinkrank / includes / seo / class-seo-opportunity-detector.php

class-seo-opportunity-detector.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.2, at includes/seo/class-seo-opportunity-detector.php

798 lines 28.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SEO Opportunity Detector Class
4 *
5 * Automatically identifies and prioritizes actionable SEO opportunities from
6 * Google API data. Detects quick wins, content gaps, and performance issues
7 * to help users focus on high-impact optimization tasks.
8 *
9 * @package ThinkRank
10 * @subpackage SEO
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\SEO;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * SEO Opportunity Detector Class
25 *
26 * Single Responsibility: Detect and prioritize SEO optimization opportunities
27 * Following ThinkRank detector patterns from existing codebase
28 *
29 * @since 1.0.0
30 */
31 class SEO_Opportunity_Detector {
32
33 /**
34 * Quick win thresholds for opportunity detection
35 *
36 * @var array
37 */
38 private const QUICK_WIN_THRESHOLDS = [
39 'position_range' => [4, 10], // Positions 4-10 for page 1 potential
40 'min_impressions' => 100, // Minimum impressions for consideration
41 'min_clicks' => 5, // Minimum clicks for relevance
42 'ctr_improvement_potential' => 2.0 // Minimum CTR improvement potential (%)
43 ];
44
45 /**
46 * Performance issue thresholds
47 *
48 * @var array
49 */
50 private const PERFORMANCE_THRESHOLDS = [
51 'ranking_drop' => 5, // Position drop threshold
52 'traffic_decline' => 20, // Traffic decline percentage
53 'ctr_decline' => 15, // CTR decline percentage
54 'impression_decline' => 25 // Impression decline percentage
55 ];
56
57 /**
58 * Content opportunity thresholds
59 *
60 * @var array
61 */
62 private const CONTENT_THRESHOLDS = [
63 'keyword_gap_impressions' => 500, // Minimum impressions for keyword gaps
64 'content_refresh_age' => 180, // Days since last update
65 'low_engagement_bounce' => 70 // Bounce rate threshold
66 ];
67
68 /**
69 * Detect quick win opportunities
70 *
71 * @param array $search_console_data Search Console data
72 * @param array $analytics_data Analytics data
73 * @return array Quick win opportunities
74 */
75 public function detect_quick_wins(array $search_console_data, array $analytics_data): array {
76 $quick_wins = [];
77 $rows = $search_console_data['rows'] ?? [];
78
79 foreach ($rows as $row) {
80 $keyword = $row['keys'][0] ?? '';
81 $clicks = $row['clicks'] ?? 0;
82 $impressions = $row['impressions'] ?? 0;
83 $position = $row['position'] ?? 0;
84 $ctr = $impressions > 0 ? ($clicks / $impressions) * 100 : 0;
85
86 $opportunity = $this->evaluate_quick_win_potential($keyword, $clicks, $impressions, $position, $ctr);
87
88 if ($opportunity['is_quick_win']) {
89 $quick_wins[] = $opportunity;
90 }
91 }
92
93 // Sort by impact score descending
94 usort($quick_wins, function($a, $b) {
95 return $b['impact_score'] <=> $a['impact_score'];
96 });
97
98 return [
99 'opportunities' => array_slice($quick_wins, 0, 15),
100 'total_count' => count($quick_wins),
101 'potential_additional_clicks' => array_sum(array_column($quick_wins, 'potential_clicks')),
102 'summary' => $this->generate_quick_wins_summary($quick_wins)
103 ];
104 }
105
106 /**
107 * Identify content opportunities
108 *
109 * @param array $keyword_data Keyword performance data
110 * @param array $content_data Content performance data
111 * @return array Content opportunities
112 */
113 public function identify_content_opportunities(array $keyword_data, array $content_data): array {
114 $opportunities = [
115 'keyword_gaps' => $this->identify_keyword_gaps($keyword_data),
116 'content_refresh' => $this->identify_content_refresh_opportunities($content_data),
117 'seasonal_content' => $this->identify_seasonal_opportunities($keyword_data),
118 'content_expansion' => $this->identify_content_expansion_opportunities($keyword_data, $content_data)
119 ];
120
121 return [
122 'opportunities' => $opportunities,
123 'priority_actions' => $this->prioritize_content_opportunities($opportunities),
124 'summary' => $this->generate_content_opportunities_summary($opportunities)
125 ];
126 }
127
128 /**
129 * Find performance issues requiring attention
130 *
131 * @param array $trend_data Trend analysis data
132 * @param array $thresholds Custom thresholds
133 * @return array Performance issues
134 */
135 public function find_performance_issues(array $trend_data, array $thresholds = []): array {
136 $thresholds = array_merge(self::PERFORMANCE_THRESHOLDS, $thresholds);
137 $issues = [];
138
139 // Analyze traffic performance issues
140 $traffic_issues = $this->detect_traffic_issues($trend_data, $thresholds);
141 if (!empty($traffic_issues)) {
142 $issues['traffic'] = $traffic_issues;
143 }
144
145 // Analyze keyword performance issues
146 $keyword_issues = $this->detect_keyword_issues($trend_data, $thresholds);
147 if (!empty($keyword_issues)) {
148 $issues['keywords'] = $keyword_issues;
149 }
150
151 // Analyze technical performance issues
152 $technical_issues = $this->detect_technical_issues($trend_data, $thresholds);
153 if (!empty($technical_issues)) {
154 $issues['technical'] = $technical_issues;
155 }
156
157 return [
158 'issues' => $issues,
159 'severity_breakdown' => $this->categorize_issues_by_severity($issues),
160 'recommended_actions' => $this->generate_issue_recommendations($issues),
161 'summary' => $this->generate_performance_issues_summary($issues)
162 ];
163 }
164
165 /**
166 * Prioritize all opportunities using impact/effort matrix
167 *
168 * @param array $opportunities All detected opportunities
169 * @return array Prioritized opportunities
170 */
171 public function prioritize_opportunities(array $opportunities): array {
172 $prioritized = [];
173
174 foreach ($opportunities as $category => $category_opportunities) {
175 if (is_array($category_opportunities)) {
176 foreach ($category_opportunities as $opportunity) {
177 if (!is_array($opportunity)) {
178 continue;
179 }
180 $priority_data = $this->calculate_priority_score($opportunity);
181 $prioritized[] = array_merge($opportunity, [
182 'category' => $category,
183 'priority_score' => $priority_data['score'],
184 'priority_level' => $priority_data['level'],
185 'estimated_effort' => $priority_data['effort'],
186 'estimated_impact' => $priority_data['impact']
187 ]);
188 }
189 }
190 }
191
192 // Sort by priority score descending
193 usort($prioritized, function($a, $b) {
194 return $b['priority_score'] <=> $a['priority_score'];
195 });
196
197 return [
198 'prioritized_opportunities' => $prioritized,
199 'high_priority_count' => count(array_filter($prioritized, function($opp) {
200 return $opp['priority_level'] === 'high';
201 })),
202 'quick_wins_count' => count(array_filter($prioritized, function($opp) {
203 return $opp['estimated_effort'] === 'low' && $opp['estimated_impact'] === 'high';
204 }))
205 ];
206 }
207
208 /**
209 * Calculate impact/effort matrix for opportunities
210 *
211 * @param array $opportunities Opportunities to analyze
212 * @return array Impact/effort matrix
213 */
214 public function calculate_impact_effort_matrix(array $opportunities): array {
215 $matrix = [
216 'high_impact_low_effort' => [], // Quick wins
217 'high_impact_high_effort' => [], // Major projects
218 'low_impact_low_effort' => [], // Fill-ins
219 'low_impact_high_effort' => [] // Avoid
220 ];
221
222 foreach ($opportunities as $opportunity) {
223 $impact = $this->determine_impact_level($opportunity);
224 $effort = $this->determine_effort_level($opportunity);
225
226 $category = $impact . '_impact_' . $effort . '_effort';
227 if (isset($matrix[$category])) {
228 $matrix[$category][] = $opportunity;
229 }
230 }
231
232 return [
233 'matrix' => $matrix,
234 'recommendations' => [
235 'focus_on' => 'high_impact_low_effort', // Quick wins first
236 'plan_for' => 'high_impact_high_effort', // Major projects second
237 'consider' => 'low_impact_low_effort', // Fill-ins when time permits
238 'avoid' => 'low_impact_high_effort' // Not worth the effort
239 ],
240 'summary' => $this->generate_matrix_summary($matrix)
241 ];
242 }
243
244 /**
245 * Evaluate quick win potential for a keyword
246 *
247 * @param string $keyword Keyword
248 * @param int $clicks Current clicks
249 * @param int $impressions Current impressions
250 * @param float $position Current position
251 * @param float $ctr Current CTR
252 * @return array Quick win evaluation
253 */
254 private function evaluate_quick_win_potential(string $keyword, int $clicks, int $impressions, float $position, float $ctr): array {
255 $thresholds = self::QUICK_WIN_THRESHOLDS;
256
257 // Check if keyword meets quick win criteria
258 $is_in_position_range = $position >= $thresholds['position_range'][0] && $position <= $thresholds['position_range'][1];
259 $has_sufficient_impressions = $impressions >= $thresholds['min_impressions'];
260 $has_sufficient_clicks = $clicks >= $thresholds['min_clicks'];
261
262 // Calculate potential improvement
263 $expected_ctr = $this->get_expected_ctr_for_position($position - 1); // If moved up 1 position
264 $ctr_improvement_potential = max(0, $expected_ctr - $ctr);
265 $has_ctr_potential = $ctr_improvement_potential >= $thresholds['ctr_improvement_potential'];
266
267 $is_quick_win = $is_in_position_range && $has_sufficient_impressions && $has_sufficient_clicks && $has_ctr_potential;
268
269 // Calculate impact score
270 $impact_score = $this->calculate_quick_win_impact_score($impressions, $position, $ctr_improvement_potential);
271
272 // Calculate potential additional clicks
273 $potential_clicks = round($impressions * ($ctr_improvement_potential / 100));
274
275 return [
276 'keyword' => $keyword,
277 'is_quick_win' => $is_quick_win,
278 'current_position' => round($position, 1),
279 'current_clicks' => $clicks,
280 'current_impressions' => $impressions,
281 'current_ctr' => round($ctr, 2),
282 'potential_position' => round($position - 1, 1),
283 'expected_ctr' => round($expected_ctr, 2),
284 'ctr_improvement_potential' => round($ctr_improvement_potential, 2),
285 'potential_clicks' => $potential_clicks,
286 'impact_score' => $impact_score,
287 'opportunity_type' => 'quick_win',
288 'recommended_action' => $this->get_quick_win_recommendation($position, $ctr, $ctr_improvement_potential)
289 ];
290 }
291
292 /**
293 * Get expected CTR for a given position
294 *
295 * @param float $position Search position
296 * @return float Expected CTR percentage
297 */
298 private function get_expected_ctr_for_position(float $position): float {
299 // Industry benchmark CTRs by position
300 $ctr_benchmarks = [
301 1 => 31.7, 2 => 24.7, 3 => 18.7, 4 => 13.7, 5 => 9.5,
302 6 => 6.8, 7 => 4.8, 8 => 3.5, 9 => 2.5, 10 => 2.2
303 ];
304
305 $pos = (int) round($position);
306
307 if ($pos <= 10) {
308 return $ctr_benchmarks[$pos] ?? 1.0;
309 } elseif ($pos <= 20) {
310 return 1.0;
311 } else {
312 return 0.5;
313 }
314 }
315
316 /**
317 * Calculate quick win impact score
318 *
319 * @param int $impressions Current impressions
320 * @param float $position Current position
321 * @param float $ctr_potential CTR improvement potential
322 * @return float Impact score (0-100)
323 */
324 private function calculate_quick_win_impact_score(int $impressions, float $position, float $ctr_potential): float {
325 // Weight factors
326 $impression_weight = 0.4;
327 $position_weight = 0.3;
328 $ctr_weight = 0.3;
329
330 // Normalize scores (0-100)
331 $impression_score = min(100, ($impressions / 1000) * 100); // Max score at 1000 impressions
332 $position_score = max(0, (11 - $position) * 10); // Better positions get higher scores
333 $ctr_score = min(100, $ctr_potential * 10); // Max score at 10% CTR potential
334
335 return ($impression_score * $impression_weight) +
336 ($position_score * $position_weight) +
337 ($ctr_score * $ctr_weight);
338 }
339
340 /**
341 * Get quick win recommendation
342 *
343 * @param float $position Current position
344 * @param float $ctr Current CTR
345 * @param float $ctr_potential CTR improvement potential
346 * @return string Recommendation
347 */
348 private function get_quick_win_recommendation(float $position, float $ctr, float $ctr_potential): string {
349 if ($position <= 5 && $ctr_potential > 3) {
350 return 'Optimize title and meta description to improve CTR for this high-ranking keyword';
351 } elseif ($position <= 10 && $ctr_potential > 2) {
352 return 'Focus on content optimization and internal linking to push to page 1';
353 } else {
354 return 'Improve content relevance and user engagement signals';
355 }
356 }
357
358 /**
359 * Generate quick wins summary
360 *
361 * @param array $quick_wins Quick win opportunities
362 * @return string Summary
363 */
364 private function generate_quick_wins_summary(array $quick_wins): string {
365 $count = count($quick_wins);
366 $total_potential = array_sum(array_column($quick_wins, 'potential_clicks'));
367
368 if ($count === 0) {
369 return 'No immediate quick win opportunities detected. Focus on long-term SEO improvements.';
370 }
371
372 return sprintf(
373 'Found %d quick win opportunities with potential for %d additional clicks per month.',
374 $count,
375 $total_potential
376 );
377 }
378
379 /**
380 * Identify keyword gaps
381 *
382 * @param array $keyword_data Keyword data
383 * @return array Keyword gap opportunities
384 */
385 private function identify_keyword_gaps(array $keyword_data): array {
386 $gaps = [];
387 $rows = $keyword_data['rows'] ?? [];
388
389 foreach ($rows as $row) {
390 $keyword = $row['keys'][0] ?? '';
391 $impressions = $row['impressions'] ?? 0;
392 $clicks = $row['clicks'] ?? 0;
393 $position = $row['position'] ?? 0;
394
395 // Identify keywords with high impressions but poor performance
396 if ($impressions >= self::CONTENT_THRESHOLDS['keyword_gap_impressions'] && $position > 20) {
397 $gaps[] = [
398 'keyword' => $keyword,
399 'impressions' => $impressions,
400 'clicks' => $clicks,
401 'position' => round($position, 1),
402 'opportunity_type' => 'keyword_gap',
403 'recommended_action' => 'Create dedicated content targeting this high-volume keyword'
404 ];
405 }
406 }
407
408 return array_slice($gaps, 0, 10);
409 }
410
411 /**
412 * Identify content refresh opportunities
413 *
414 * @param array $content_data Content performance data
415 * @return array Content refresh opportunities
416 */
417 private function identify_content_refresh_opportunities(array $content_data): array {
418 $refresh_opportunities = [];
419 $pages = $content_data['top_pages']['pages'] ?? [];
420
421 foreach ($pages as $page) {
422 $path = $page['path'] ?? '';
423 $pageviews = $page['pageviews'] ?? 0;
424
425 // Identify pages with declining performance (would need historical data)
426 // For now, identify pages with moderate traffic that could be improved
427 if ($pageviews > 50 && $pageviews < 500) {
428 $refresh_opportunities[] = [
429 'page_path' => $path,
430 'current_pageviews' => $pageviews,
431 'opportunity_type' => 'content_refresh',
432 'recommended_action' => 'Update content with fresh information and optimize for current keywords'
433 ];
434 }
435 }
436
437 return array_slice($refresh_opportunities, 0, 5);
438 }
439
440 /**
441 * Identify seasonal opportunities
442 *
443 * @param array $keyword_data Keyword data
444 * @return array Seasonal opportunities
445 */
446 private function identify_seasonal_opportunities(array $keyword_data): array {
447 // Placeholder for seasonal analysis
448 // Would analyze historical patterns to identify seasonal keywords
449 return [
450 [
451 'opportunity_type' => 'seasonal_content',
452 'season' => 'upcoming',
453 'recommended_action' => 'Prepare seasonal content based on historical performance patterns'
454 ]
455 ];
456 }
457
458 /**
459 * Identify content expansion opportunities
460 *
461 * @param array $keyword_data Keyword data
462 * @param array $content_data Content data
463 * @return array Content expansion opportunities
464 */
465 private function identify_content_expansion_opportunities(array $keyword_data, array $content_data): array {
466 $expansion_opportunities = [];
467 $rows = $keyword_data['rows'] ?? [];
468
469 // Look for keyword clusters that could benefit from comprehensive content
470 $keyword_clusters = $this->identify_keyword_clusters($rows);
471
472 foreach ($keyword_clusters as $cluster) {
473 if (count($cluster['keywords']) >= 3) {
474 $expansion_opportunities[] = [
475 'cluster_topic' => $cluster['topic'],
476 'keyword_count' => count($cluster['keywords']),
477 'total_impressions' => $cluster['total_impressions'],
478 'opportunity_type' => 'content_expansion',
479 'recommended_action' => 'Create comprehensive content covering this keyword cluster'
480 ];
481 }
482 }
483
484 return array_slice($expansion_opportunities, 0, 3);
485 }
486
487 /**
488 * Identify keyword clusters
489 *
490 * @param array $rows Keyword rows
491 * @return array Keyword clusters
492 */
493 private function identify_keyword_clusters(array $rows): array {
494 // Simplified clustering based on common words
495 $clusters = [];
496
497 foreach ($rows as $row) {
498 $keyword = $row['keys'][0] ?? '';
499 $impressions = $row['impressions'] ?? 0;
500
501 // Extract main topic (first word for simplification)
502 $words = explode(' ', $keyword);
503 $main_topic = $words[0] ?? 'general';
504
505 if (!isset($clusters[$main_topic])) {
506 $clusters[$main_topic] = [
507 'topic' => $main_topic,
508 'keywords' => [],
509 'total_impressions' => 0
510 ];
511 }
512
513 $clusters[$main_topic]['keywords'][] = $keyword;
514 $clusters[$main_topic]['total_impressions'] += $impressions;
515 }
516
517 return array_values($clusters);
518 }
519
520 /**
521 * Prioritize content opportunities
522 *
523 * @param array $opportunities Content opportunities
524 * @return array Priority actions
525 */
526 private function prioritize_content_opportunities(array $opportunities): array {
527 $priority_actions = [];
528
529 // Prioritize keyword gaps (high impact)
530 if (!empty($opportunities['keyword_gaps'])) {
531 $priority_actions[] = [
532 'action' => 'Address keyword gaps',
533 'priority' => 'high',
534 'count' => count($opportunities['keyword_gaps'])
535 ];
536 }
537
538 // Content refresh (medium impact)
539 if (!empty($opportunities['content_refresh'])) {
540 $priority_actions[] = [
541 'action' => 'Refresh existing content',
542 'priority' => 'medium',
543 'count' => count($opportunities['content_refresh'])
544 ];
545 }
546
547 return $priority_actions;
548 }
549
550 /**
551 * Generate content opportunities summary
552 *
553 * @param array $opportunities Content opportunities
554 * @return string Summary
555 */
556 private function generate_content_opportunities_summary(array $opportunities): string {
557 $total_opportunities = array_sum(array_map('count', array_filter($opportunities, 'is_array')));
558
559 if ($total_opportunities === 0) {
560 return 'No significant content opportunities detected. Current content strategy appears optimized.';
561 }
562
563 return sprintf(
564 'Identified %d content opportunities across keyword gaps, content refresh, and expansion areas.',
565 $total_opportunities
566 );
567 }
568
569 /**
570 * Detect traffic performance issues
571 *
572 * @param array $trend_data Trend data
573 * @param array $thresholds Thresholds
574 * @return array Traffic issues
575 */
576 private function detect_traffic_issues(array $trend_data, array $thresholds): array {
577 $issues = [];
578 $traffic_trends = $trend_data['sessions'] ?? [];
579
580 if (isset($traffic_trends['percentage_change'])) {
581 $change = $traffic_trends['percentage_change'];
582
583 if ($change < -$thresholds['traffic_decline']) {
584 $issues[] = [
585 'type' => 'traffic_decline',
586 'severity' => $change < -40 ? 'critical' : 'high',
587 'description' => sprintf('Traffic declined by %.1f%%', abs($change)),
588 'recommended_action' => 'Investigate traffic sources and optimize underperforming content'
589 ];
590 }
591 }
592
593 return $issues;
594 }
595
596 /**
597 * Detect keyword performance issues
598 *
599 * @param array $trend_data Trend data
600 * @param array $thresholds Thresholds
601 * @return array Keyword issues
602 */
603 private function detect_keyword_issues(array $trend_data, array $thresholds): array {
604 $issues = [];
605
606 // This would analyze keyword trend data for declining performance
607 // For now, return placeholder structure
608 return $issues;
609 }
610
611 /**
612 * Detect technical performance issues
613 *
614 * @param array $trend_data Trend data
615 * @param array $thresholds Thresholds
616 * @return array Technical issues
617 */
618 private function detect_technical_issues(array $trend_data, array $thresholds): array {
619 $issues = [];
620
621 // This would analyze Core Web Vitals and technical metrics
622 // For now, return placeholder structure
623 return $issues;
624 }
625
626 /**
627 * Categorize issues by severity
628 *
629 * @param array $issues All issues
630 * @return array Severity breakdown
631 */
632 private function categorize_issues_by_severity(array $issues): array {
633 $severity_breakdown = [
634 'critical' => 0,
635 'high' => 0,
636 'medium' => 0,
637 'low' => 0
638 ];
639
640 foreach ($issues as $category => $category_issues) {
641 foreach ($category_issues as $issue) {
642 $severity = $issue['severity'] ?? 'medium';
643 if (isset($severity_breakdown[$severity])) {
644 $severity_breakdown[$severity]++;
645 }
646 }
647 }
648
649 return $severity_breakdown;
650 }
651
652 /**
653 * Generate issue recommendations
654 *
655 * @param array $issues All issues
656 * @return array Recommendations
657 */
658 private function generate_issue_recommendations(array $issues): array {
659 $recommendations = [];
660
661 foreach ($issues as $category => $category_issues) {
662 foreach ($category_issues as $issue) {
663 if (isset($issue['recommended_action'])) {
664 $recommendations[] = [
665 'category' => $category,
666 'severity' => $issue['severity'] ?? 'medium',
667 'action' => $issue['recommended_action']
668 ];
669 }
670 }
671 }
672
673 // Sort by severity (critical first)
674 $severity_order = ['critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3];
675 usort($recommendations, function($a, $b) use ($severity_order) {
676 return $severity_order[$a['severity']] <=> $severity_order[$b['severity']];
677 });
678
679 return $recommendations;
680 }
681
682 /**
683 * Generate performance issues summary
684 *
685 * @param array $issues All issues
686 * @return string Summary
687 */
688 private function generate_performance_issues_summary(array $issues): string {
689 $total_issues = 0;
690 foreach ($issues as $category_issues) {
691 $total_issues += count($category_issues);
692 }
693
694 if ($total_issues === 0) {
695 return 'No significant performance issues detected. SEO performance appears stable.';
696 }
697
698 return sprintf(
699 'Detected %d performance issues requiring attention across traffic, keywords, and technical areas.',
700 $total_issues
701 );
702 }
703
704 /**
705 * Calculate priority score for an opportunity
706 *
707 * @param array $opportunity Opportunity data
708 * @return array Priority data
709 */
710 private function calculate_priority_score(array $opportunity): array {
711 $impact = $this->determine_impact_level($opportunity);
712 $effort = $this->determine_effort_level($opportunity);
713
714 // Calculate numeric priority score
715 $impact_score = $impact === 'high' ? 10 : 5;
716 $effort_score = $effort === 'low' ? 10 : 5;
717 $priority_score = $impact_score + $effort_score;
718
719 // Determine priority level
720 if ($impact === 'high' && $effort === 'low') {
721 $level = 'high';
722 } elseif ($impact === 'high' && $effort === 'high') {
723 $level = 'medium';
724 } else {
725 $level = 'low';
726 }
727
728 return [
729 'score' => $priority_score,
730 'level' => $level,
731 'impact' => $impact,
732 'effort' => $effort
733 ];
734 }
735
736 /**
737 * Determine impact level
738 *
739 * @param array $opportunity Opportunity data
740 * @return string Impact level
741 */
742 private function determine_impact_level(array $opportunity): string {
743 $impact_score = $opportunity['impact_score'] ?? 0;
744 $potential_clicks = $opportunity['potential_clicks'] ?? 0;
745 $impressions = $opportunity['current_impressions'] ?? $opportunity['impressions'] ?? 0;
746
747 if ($impact_score >= 70 || $potential_clicks >= 50 || $impressions >= 1000) {
748 return 'high';
749 } else {
750 return 'low';
751 }
752 }
753
754 /**
755 * Determine effort level
756 *
757 * @param array $opportunity Opportunity data
758 * @return string Effort level
759 */
760 private function determine_effort_level(array $opportunity): string {
761 $position = $opportunity['current_position'] ?? $opportunity['position'] ?? 100;
762 $opportunity_type = $opportunity['opportunity_type'] ?? '';
763
764 // Quick wins and CTR optimizations are low effort
765 if ($opportunity_type === 'quick_win' || $position <= 10) {
766 return 'low';
767 } else {
768 return 'high';
769 }
770 }
771
772 /**
773 * Generate matrix summary
774 *
775 * @param array $matrix Impact/effort matrix
776 * @return string Summary
777 */
778 private function generate_matrix_summary(array $matrix): string {
779 $quick_wins = count($matrix['high_impact_low_effort']);
780 $major_projects = count($matrix['high_impact_high_effort']);
781
782 if ($quick_wins > 0) {
783 return sprintf(
784 'Focus on %d quick wins first, then plan for %d major projects.',
785 $quick_wins,
786 $major_projects
787 );
788 } elseif ($major_projects > 0) {
789 return sprintf(
790 'No quick wins available. Focus on planning %d major projects.',
791 $major_projects
792 );
793 } else {
794 return 'Limited optimization opportunities detected. Focus on content creation and link building.';
795 }
796 }
797 }
798