PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.4.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.4.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 / seo / class-seo-scoring-engine.php

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

778 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SEO Scoring Engine Class
4 *
5 * Calculates SEO health scores and performance ratings based on Google API data.
6 * Provides quantified SEO assessment for overall site health, page performance,
7 * and keyword opportunities with industry benchmarks.
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 Scoring Engine Class
25 *
26 * Single Responsibility: Calculate SEO performance scores and ratings
27 * Following ThinkRank scoring patterns from existing SEO_Score_Calculator
28 *
29 * @since 1.0.0
30 */
31 class SEO_Scoring_Engine {
32
33 /**
34 * Maximum SEO health score
35 *
36 * @var int
37 */
38 private const MAX_HEALTH_SCORE = 100;
39
40 /**
41 * Score weight distribution for SEO health calculation
42 *
43 * @var array
44 */
45 private const HEALTH_SCORE_WEIGHTS = [
46 'traffic_growth' => 25,
47 'keyword_performance' => 25,
48 'content_engagement' => 25,
49 'technical_performance' => 25
50 ];
51
52 /**
53 * Industry benchmark CTR values by position
54 *
55 * @var array
56 */
57 private const CTR_BENCHMARKS = [
58 1 => 31.7,
59 2 => 24.7,
60 3 => 18.7,
61 4 => 13.7,
62 5 => 9.5,
63 6 => 6.8,
64 7 => 4.8,
65 8 => 3.5,
66 9 => 2.5,
67 10 => 2.2
68 ];
69
70 /**
71 * Calculate overall SEO health score
72 *
73 * @param array $analytics_data Google Analytics data
74 * @param array $search_data Search Console data
75 * @return array SEO health score analysis
76 */
77 public function calculate_seo_health_score(array $analytics_data, array $search_data): array {
78 $scores = [
79 'traffic_growth' => $this->score_traffic_growth($analytics_data),
80 'keyword_performance' => $this->score_keyword_performance($search_data),
81 'content_engagement' => $this->score_content_engagement($analytics_data),
82 'technical_performance' => $this->score_technical_performance($analytics_data)
83 ];
84
85 $overall_score = $this->calculate_weighted_score($scores, self::HEALTH_SCORE_WEIGHTS);
86 $grade = $this->determine_grade($overall_score);
87
88 return [
89 'overall_score' => $overall_score,
90 'grade' => $grade,
91 'component_scores' => $scores,
92 'interpretation' => $this->generate_score_interpretation($overall_score, $grade),
93 'recommendations' => $this->generate_score_recommendations($scores),
94 'last_calculated' => current_time('mysql')
95 ];
96 }
97
98 /**
99 * Score individual page performance
100 *
101 * @param array $page_data Page performance data
102 * @param array $benchmarks Industry benchmarks
103 * @return array Page performance score
104 */
105 public function score_page_performance(array $page_data, array $benchmarks): array {
106 $scores = [
107 'traffic_contribution' => $this->score_traffic_contribution($page_data),
108 'search_visibility' => $this->score_search_visibility($page_data),
109 'engagement_quality' => $this->score_engagement_quality($page_data),
110 'technical_health' => $this->score_technical_health($page_data)
111 ];
112
113 $overall_score = array_sum($scores) / count($scores);
114 $grade = $this->determine_grade($overall_score);
115
116 return [
117 'overall_score' => round($overall_score, 1),
118 'grade' => $grade,
119 'component_scores' => $scores,
120 'page_path' => $page_data['path'] ?? '',
121 'recommendations' => $this->generate_page_recommendations($scores, $page_data)
122 ];
123 }
124
125 /**
126 * Score keyword opportunities
127 *
128 * @param array $keyword_data Keyword performance data
129 * @return array Keyword opportunity scores
130 */
131 public function score_keyword_opportunities(array $keyword_data): array {
132 $opportunities = [];
133 $rows = $keyword_data['rows'] ?? [];
134
135 foreach ($rows as $row) {
136 $keyword = $row['keys'][0] ?? '';
137 $clicks = $row['clicks'] ?? 0;
138 $impressions = $row['impressions'] ?? 0;
139 $position = $row['position'] ?? 0;
140 $ctr = $impressions > 0 ? ($clicks / $impressions) * 100 : 0;
141
142 $opportunity_score = $this->calculate_keyword_opportunity_score($clicks, $impressions, $position, $ctr);
143
144 if ($opportunity_score > 0) {
145 $opportunities[] = [
146 'keyword' => $keyword,
147 'opportunity_score' => $opportunity_score,
148 'current_position' => round($position, 1),
149 'current_ctr' => round($ctr, 2),
150 'expected_ctr' => $this->get_expected_ctr($position),
151 'clicks' => $clicks,
152 'impressions' => $impressions,
153 'potential_impact' => $this->calculate_potential_impact($impressions, $position, $ctr)
154 ];
155 }
156 }
157
158 // Sort by opportunity score descending
159 usort($opportunities, function($a, $b) {
160 return $b['opportunity_score'] <=> $a['opportunity_score'];
161 });
162
163 return [
164 'opportunities' => array_slice($opportunities, 0, 20),
165 'total_opportunities' => count($opportunities),
166 'high_impact_count' => count(array_filter($opportunities, function($opp) {
167 return $opp['opportunity_score'] >= 70;
168 }))
169 ];
170 }
171
172 /**
173 * Generate priority matrix for opportunities
174 *
175 * @param array $scores Various performance scores
176 * @return array Priority matrix
177 */
178 public function generate_priority_matrix(array $scores): array {
179 $matrix = [
180 'high_impact_low_effort' => [],
181 'high_impact_high_effort' => [],
182 'low_impact_low_effort' => [],
183 'low_impact_high_effort' => []
184 ];
185
186 foreach ($scores as $item) {
187 $impact = $this->determine_impact_level($item);
188 $effort = $this->determine_effort_level($item);
189
190 $category = $impact . '_impact_' . $effort . '_effort';
191 if (isset($matrix[$category])) {
192 $matrix[$category][] = $item;
193 }
194 }
195
196 return $matrix;
197 }
198
199 /**
200 * Get industry benchmarks for comparison
201 *
202 * @param string $industry Industry type
203 * @return array Industry benchmarks
204 */
205 public function get_industry_benchmarks(string $industry = 'general'): array {
206 // Default benchmarks - could be expanded with industry-specific data
207 return [
208 'average_ctr' => 2.5,
209 'average_position' => 15.0,
210 'bounce_rate_threshold' => 70.0,
211 'session_duration_threshold' => 120, // seconds
212 'pages_per_session_threshold' => 2.0,
213 'core_web_vitals' => [
214 'lcp_threshold' => 2.5, // seconds
215 'inp_threshold' => 200, // milliseconds
216 'cls_threshold' => 0.1
217 ]
218 ];
219 }
220
221 /**
222 * Score traffic growth component
223 *
224 * @param array $analytics_data Analytics data
225 * @return float Traffic growth score
226 */
227 private function score_traffic_growth(array $analytics_data): float {
228 $traffic = $analytics_data['traffic'] ?? [];
229 $organic_traffic = $analytics_data['organic_traffic'] ?? [];
230
231 $sessions = $traffic['sessions'] ?? 0;
232 $organic_sessions = $organic_traffic['organic_traffic']['sessions'] ?? 0;
233
234 // Score based on organic traffic percentage and absolute numbers
235 $organic_percentage = $sessions > 0 ? ($organic_sessions / $sessions) * 100 : 0;
236
237 if ($organic_percentage >= 60) {
238 return 25.0; // Excellent organic traffic share
239 } elseif ($organic_percentage >= 40) {
240 return 20.0; // Good organic traffic share
241 } elseif ($organic_percentage >= 20) {
242 return 15.0; // Fair organic traffic share
243 } else {
244 return 10.0; // Poor organic traffic share
245 }
246 }
247
248 /**
249 * Score keyword performance component
250 *
251 * @param array $search_data Search Console data
252 * @return float Keyword performance score
253 */
254 private function score_keyword_performance(array $search_data): float {
255 $search_performance = $search_data['search_performance'] ?? [];
256 $rows = $search_performance['rows'] ?? [];
257
258 if (empty($rows)) {
259 return 0.0;
260 }
261
262 $total_clicks = 0;
263 $total_impressions = 0;
264 $position_sum = 0;
265 $keyword_count = 0;
266
267 foreach ($rows as $row) {
268 $total_clicks += $row['clicks'] ?? 0;
269 $total_impressions += $row['impressions'] ?? 0;
270 $position_sum += $row['position'] ?? 0;
271 $keyword_count++;
272 }
273
274 $average_position = $keyword_count > 0 ? $position_sum / $keyword_count : 0;
275 $overall_ctr = $total_impressions > 0 ? ($total_clicks / $total_impressions) * 100 : 0;
276
277 // Score based on average position and CTR
278 $position_score = $this->score_average_position($average_position);
279 $ctr_score = $this->score_ctr_performance($overall_ctr);
280
281 return ($position_score + $ctr_score) / 2;
282 }
283
284 /**
285 * Score content engagement component
286 *
287 * @param array $analytics_data Analytics data
288 * @return float Content engagement score
289 */
290 private function score_content_engagement(array $analytics_data): float {
291 $traffic = $analytics_data['traffic'] ?? [];
292
293 $bounce_rate = $traffic['bounce_rate'] ?? 0;
294 $avg_session_duration = $traffic['avg_session_duration'] ?? 0;
295
296 // Score based on engagement metrics (lower bounce rate and higher session duration is better)
297 $bounce_score = $this->score_bounce_rate($bounce_rate);
298 $duration_score = $this->score_session_duration($avg_session_duration);
299
300 return ($bounce_score + $duration_score) / 2;
301 }
302
303 /**
304 * Score technical performance component
305 *
306 * @param array $analytics_data Analytics data
307 * @return float Technical performance score
308 */
309 private function score_technical_performance(array $analytics_data): float {
310 $core_web_vitals = $analytics_data['core_web_vitals'] ?? [];
311
312 if (empty($core_web_vitals)) {
313 return 15.0; // Default score when no Core Web Vitals data available
314 }
315
316 // Score based on Core Web Vitals metrics
317 // Both sides of this merge are needed: extract_vital_value() unwraps the
318 // structured vital payload, and the metric is INP rather than the
319 // retired FID.
320 $lcp_score = $this->score_lcp($this->extract_vital_value($core_web_vitals['lcp'] ?? 0));
321 $inp_score = $this->score_inp($this->extract_vital_value($core_web_vitals['inp'] ?? 0));
322 $cls_score = $this->score_cls($this->extract_vital_value($core_web_vitals['cls'] ?? 0));
323
324 return ($lcp_score + $inp_score + $cls_score) / 3;
325 }
326
327 /**
328 * Extract the numeric value from a Core Web Vital entry
329 *
330 * The PageSpeed client represents each vital as a structured array
331 * (['value' => 14.7652, 'unit' => 's', 'score' => 30, ...]); older or
332 * cached payloads may carry a bare scalar. Accept both.
333 *
334 * @param mixed $vital Structured vital array or scalar value
335 * @return float Numeric metric value
336 */
337 private function extract_vital_value($vital): float {
338 if (is_array($vital)) {
339 $vital = $vital['value'] ?? 0;
340 }
341
342 return is_numeric($vital) ? (float) $vital : 0.0;
343 }
344
345 /**
346 * Calculate weighted score from component scores
347 *
348 * @param array $scores Component scores
349 * @param array $weights Score weights
350 * @return float Weighted overall score
351 */
352 private function calculate_weighted_score(array $scores, array $weights): float {
353 $total_score = 0;
354 $total_weight = 0;
355
356 foreach ($scores as $component => $score) {
357 $weight = $weights[$component] ?? 0;
358 $total_score += $score * ($weight / 100);
359 $total_weight += $weight;
360 }
361
362 return $total_weight > 0 ? ($total_score / $total_weight) * 100 : 0;
363 }
364
365 /**
366 * Determine grade from score
367 *
368 * @param float $score Numeric score
369 * @return string Letter grade
370 */
371 private function determine_grade(float $score): string {
372 if ($score >= 90) {
373 return 'A+';
374 } elseif ($score >= 80) {
375 return 'A';
376 } elseif ($score >= 70) {
377 return 'B';
378 } elseif ($score >= 60) {
379 return 'C';
380 } elseif ($score >= 50) {
381 return 'D';
382 } else {
383 return 'F';
384 }
385 }
386
387 /**
388 * Generate score interpretation
389 *
390 * @param float $score Overall score
391 * @param string $grade Letter grade
392 * @return string Score interpretation
393 */
394 private function generate_score_interpretation(float $score, string $grade): string {
395 switch ($grade) {
396 case 'A+':
397 return 'Excellent SEO performance with strong metrics across all areas.';
398 case 'A':
399 return 'Very good SEO performance with minor areas for improvement.';
400 case 'B':
401 return 'Good SEO performance with some optimization opportunities.';
402 case 'C':
403 return 'Fair SEO performance with several areas needing attention.';
404 case 'D':
405 return 'Poor SEO performance requiring significant improvements.';
406 case 'F':
407 return 'Critical SEO issues requiring immediate attention.';
408 default:
409 return sprintf('SEO performance score: %.1f/100', $score);
410 }
411 }
412
413 /**
414 * Generate score-based recommendations
415 *
416 * @param array $scores Component scores
417 * @return array Recommendations
418 */
419 private function generate_score_recommendations(array $scores): array {
420 $recommendations = [];
421
422 foreach ($scores as $component => $score) {
423 if ($score < 15) {
424 $recommendations[] = $this->get_component_recommendation($component, 'critical');
425 } elseif ($score < 20) {
426 $recommendations[] = $this->get_component_recommendation($component, 'high');
427 } elseif ($score < 22) {
428 $recommendations[] = $this->get_component_recommendation($component, 'medium');
429 }
430 }
431
432 return $recommendations;
433 }
434
435 /**
436 * Get component-specific recommendation
437 *
438 * @param string $component Component name
439 * @param string $priority Priority level
440 * @return array Recommendation
441 */
442 private function get_component_recommendation(string $component, string $priority): array {
443 $recommendations = [
444 'traffic_growth' => [
445 'critical' => 'Focus on organic traffic growth through content optimization and keyword targeting.',
446 'high' => 'Improve organic traffic share by optimizing existing content and building quality backlinks.',
447 'medium' => 'Continue growing organic traffic through consistent content creation and SEO optimization.'
448 ],
449 'keyword_performance' => [
450 'critical' => 'Urgent keyword optimization needed - focus on improving rankings for target keywords.',
451 'high' => 'Optimize keyword targeting and improve content relevance for better rankings.',
452 'medium' => 'Fine-tune keyword strategy and monitor ranking improvements.'
453 ],
454 'content_engagement' => [
455 'critical' => 'Critical engagement issues - review content quality and user experience immediately.',
456 'high' => 'Improve content engagement through better formatting, internal linking, and user experience.',
457 'medium' => 'Enhance content engagement with multimedia elements and improved readability.'
458 ],
459 'technical_performance' => [
460 'critical' => 'Critical technical issues affecting SEO - address Core Web Vitals and site speed immediately.',
461 'high' => 'Improve technical SEO by optimizing page speed and Core Web Vitals.',
462 'medium' => 'Fine-tune technical performance for better user experience and SEO.'
463 ]
464 ];
465
466 return [
467 'component' => $component,
468 'priority' => $priority,
469 'recommendation' => $recommendations[$component][$priority] ?? 'Optimize this component for better SEO performance.'
470 ];
471 }
472
473 /**
474 * Calculate keyword opportunity score
475 *
476 * @param int $clicks Current clicks
477 * @param int $impressions Current impressions
478 * @param float $position Current position
479 * @param float $ctr Current CTR
480 * @return float Opportunity score (0-100)
481 */
482 private function calculate_keyword_opportunity_score(int $clicks, int $impressions, float $position, float $ctr): float {
483 // No opportunity if no impressions
484 if ($impressions < 10) {
485 return 0;
486 }
487
488 $expected_ctr = $this->get_expected_ctr($position);
489 $ctr_gap = max(0, $expected_ctr - $ctr);
490
491 // Higher score for keywords with:
492 // 1. High impressions (more potential)
493 // 2. Position 4-10 (page 1 potential)
494 // 3. CTR below expected (optimization opportunity)
495
496 $impression_score = min(40, $impressions / 100); // Max 40 points for impressions
497 $position_score = $position <= 10 ? (11 - $position) * 3 : 0; // Max 30 points for position
498 $ctr_opportunity_score = min(30, $ctr_gap * 10); // Max 30 points for CTR gap
499
500 return min(100, $impression_score + $position_score + $ctr_opportunity_score);
501 }
502
503 /**
504 * Get expected CTR for position
505 *
506 * @param float $position Search position
507 * @return float Expected CTR percentage
508 */
509 private function get_expected_ctr(float $position): float {
510 $pos = (int) round($position);
511
512 if ($pos <= 10) {
513 return self::CTR_BENCHMARKS[$pos] ?? 1.0;
514 } elseif ($pos <= 20) {
515 return 1.0;
516 } else {
517 return 0.5;
518 }
519 }
520
521 /**
522 * Calculate potential impact of optimization
523 *
524 * @param int $impressions Current impressions
525 * @param float $position Current position
526 * @param float $current_ctr Current CTR
527 * @return array Potential impact analysis
528 */
529 private function calculate_potential_impact(int $impressions, float $position, float $current_ctr): array {
530 $expected_ctr = $this->get_expected_ctr($position);
531 $potential_additional_clicks = $impressions * (($expected_ctr - $current_ctr) / 100);
532
533 return [
534 'additional_clicks_potential' => max(0, round($potential_additional_clicks)),
535 'ctr_improvement_potential' => max(0, round($expected_ctr - $current_ctr, 2)),
536 'impact_level' => $potential_additional_clicks > 50 ? 'high' : ($potential_additional_clicks > 10 ? 'medium' : 'low')
537 ];
538 }
539
540 /**
541 * Score average position performance
542 *
543 * @param float $average_position Average search position
544 * @return float Position score
545 */
546 private function score_average_position(float $average_position): float {
547 if ($average_position <= 3) {
548 return 12.5; // Excellent - top 3 positions
549 } elseif ($average_position <= 10) {
550 return 10.0; // Good - page 1
551 } elseif ($average_position <= 20) {
552 return 7.5; // Fair - page 2
553 } else {
554 return 5.0; // Poor - beyond page 2
555 }
556 }
557
558 /**
559 * Score CTR performance
560 *
561 * @param float $ctr Click-through rate percentage
562 * @return float CTR score
563 */
564 private function score_ctr_performance(float $ctr): float {
565 if ($ctr >= 5.0) {
566 return 12.5; // Excellent CTR
567 } elseif ($ctr >= 3.0) {
568 return 10.0; // Good CTR
569 } elseif ($ctr >= 1.5) {
570 return 7.5; // Fair CTR
571 } else {
572 return 5.0; // Poor CTR
573 }
574 }
575
576 /**
577 * Score bounce rate performance
578 *
579 * @param float $bounce_rate Bounce rate percentage
580 * @return float Bounce rate score
581 */
582 private function score_bounce_rate(float $bounce_rate): float {
583 if ($bounce_rate <= 40) {
584 return 12.5; // Excellent engagement
585 } elseif ($bounce_rate <= 55) {
586 return 10.0; // Good engagement
587 } elseif ($bounce_rate <= 70) {
588 return 7.5; // Fair engagement
589 } else {
590 return 5.0; // Poor engagement
591 }
592 }
593
594 /**
595 * Score session duration performance
596 *
597 * @param float $duration Average session duration in seconds
598 * @return float Duration score
599 */
600 private function score_session_duration(float $duration): float {
601 if ($duration >= 180) { // 3+ minutes
602 return 12.5; // Excellent engagement
603 } elseif ($duration >= 120) { // 2+ minutes
604 return 10.0; // Good engagement
605 } elseif ($duration >= 60) { // 1+ minute
606 return 7.5; // Fair engagement
607 } else {
608 return 5.0; // Poor engagement
609 }
610 }
611
612 /**
613 * Score Largest Contentful Paint (LCP)
614 *
615 * @param float $lcp LCP value in seconds
616 * @return float LCP score
617 */
618 private function score_lcp(float $lcp): float {
619 if ($lcp <= 2.5) {
620 return 8.33; // Good
621 } elseif ($lcp <= 4.0) {
622 return 6.0; // Needs improvement
623 } else {
624 return 3.0; // Poor
625 }
626 }
627
628 /**
629 * Score Interaction to Next Paint (INP)
630 *
631 * INP replaced FID as a Core Web Vital in March 2024; its thresholds are
632 * 200ms (good) and 500ms (needs improvement).
633 *
634 * @param float $inp INP value in milliseconds
635 * @return float INP score
636 */
637 private function score_inp(float $inp): float {
638 if ($inp <= 200) {
639 return 8.33; // Good
640 } elseif ($inp <= 500) {
641 return 6.0; // Needs improvement
642 } else {
643 return 3.0; // Poor
644 }
645 }
646
647 /**
648 * Score Cumulative Layout Shift (CLS)
649 *
650 * @param float $cls CLS value
651 * @return float CLS score
652 */
653 private function score_cls(float $cls): float {
654 if ($cls <= 0.1) {
655 return 8.33; // Good
656 } elseif ($cls <= 0.25) {
657 return 6.0; // Needs improvement
658 } else {
659 return 3.0; // Poor
660 }
661 }
662
663 /**
664 * Score traffic contribution for a page
665 *
666 * @param array $page_data Page data
667 * @return float Traffic contribution score
668 */
669 private function score_traffic_contribution(array $page_data): float {
670 $pageviews = $page_data['pageviews'] ?? 0;
671
672 if ($pageviews >= 1000) {
673 return 25.0; // High traffic page
674 } elseif ($pageviews >= 500) {
675 return 20.0; // Medium-high traffic
676 } elseif ($pageviews >= 100) {
677 return 15.0; // Medium traffic
678 } else {
679 return 10.0; // Low traffic
680 }
681 }
682
683 /**
684 * Score search visibility for a page
685 *
686 * @param array $page_data Page data
687 * @return float Search visibility score
688 */
689 private function score_search_visibility(array $page_data): float {
690 // This would be enhanced with actual search console data for the specific page
691 // For now, return a default score
692 return 15.0;
693 }
694
695 /**
696 * Score engagement quality for a page
697 *
698 * @param array $page_data Page data
699 * @return float Engagement quality score
700 */
701 private function score_engagement_quality(array $page_data): float {
702 $sessions = $page_data['sessions'] ?? 0;
703 $pageviews = $page_data['pageviews'] ?? 0;
704
705 $pages_per_session = $sessions > 0 ? $pageviews / $sessions : 1;
706
707 if ($pages_per_session >= 3.0) {
708 return 25.0; // Excellent engagement
709 } elseif ($pages_per_session >= 2.0) {
710 return 20.0; // Good engagement
711 } elseif ($pages_per_session >= 1.5) {
712 return 15.0; // Fair engagement
713 } else {
714 return 10.0; // Poor engagement
715 }
716 }
717
718 /**
719 * Score technical health for a page
720 *
721 * @param array $page_data Page data
722 * @return float Technical health score
723 */
724 private function score_technical_health(array $page_data): float {
725 // This would be enhanced with actual Core Web Vitals data for the specific page
726 // For now, return a default score
727 return 20.0;
728 }
729
730 /**
731 * Generate page-specific recommendations
732 *
733 * @param array $scores Component scores
734 * @param array $page_data Page data
735 * @return array Page recommendations
736 */
737 private function generate_page_recommendations(array $scores, array $page_data): array {
738 $recommendations = [];
739
740 if ($scores['traffic_contribution'] < 15) {
741 $recommendations[] = 'Improve content quality and SEO optimization to increase traffic';
742 }
743
744 if ($scores['engagement_quality'] < 15) {
745 $recommendations[] = 'Enhance content engagement with better formatting and internal links';
746 }
747
748 if ($scores['technical_health'] < 15) {
749 $recommendations[] = 'Optimize page speed and Core Web Vitals performance';
750 }
751
752 return $recommendations;
753 }
754
755 /**
756 * Determine impact level for priority matrix
757 *
758 * @param array $item Item to evaluate
759 * @return string Impact level (high/low)
760 */
761 private function determine_impact_level(array $item): string {
762 $score = $item['opportunity_score'] ?? 0;
763 return $score >= 50 ? 'high' : 'low';
764 }
765
766 /**
767 * Determine effort level for priority matrix
768 *
769 * @param array $item Item to evaluate
770 * @return string Effort level (high/low)
771 */
772 private function determine_effort_level(array $item): string {
773 $position = $item['current_position'] ?? 100;
774 // Assume lower positions require more effort to improve
775 return $position <= 20 ? 'low' : 'high';
776 }
777 }
778