PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/seo/class-content-optimization-manager.php +43 -21 1.0.1 → 2.10.0 View file →
@@ -14,8 +14,13 @@
14 14 declare(strict_types=1);
15 15
16 16 namespace ThinkRank\SEO;
17 17
18 +// Prevent direct access
19 +if (!defined('ABSPATH')) {
20 + exit;
21 +}
22 +
18 23 /**
19 24 * Content Optimization Manager Class
20 25 *
21 26 * Provides comprehensive content optimization with real-time scoring,
@@ -28,19 +33,28 @@
28 33
29 34 /**
30 35 * Content optimization scoring weights (2025 SEO standards)
31 36 *
37 + * Semantic relevance used to sit here at 5%, fed by a block that returned
38 + * the same numbers for every input (#538). Removing a factor would have
39 + * capped the score at 95, so the remaining six were rescaled by 100/95 and
40 + * rounded to whole numbers, which preserves their former ratio
41 + * (25:20:15:15:10:10) and keeps the array at 100.
42 + *
43 + * Rounding left technical_seo and user_experience, which were equal at 10,
44 + * one point apart at 11 and 10. That is an artefact of landing the spare
45 + * point somewhere, not a judgement that technical SEO matters more.
46 + *
32 47 * @since 1.0.0
33 48 * @var array
34 49 */
35 50 private array $optimization_weights = [
36 - 'content_quality' => 25, // Content depth, uniqueness, value
37 - 'keyword_optimization' => 20, // Keyword usage and distribution
38 - 'readability' => 15, // Reading ease and comprehension
39 - 'structure' => 15, // Headings, paragraphs, formatting
40 - 'technical_seo' => 10, // Meta tags, URLs, schema
41 - 'user_experience' => 10, // Engagement signals, CTR optimization
42 - 'semantic_relevance' => 5 // Topic relevance and entity coverage
51 + 'content_quality' => 26, // Content depth, uniqueness, value
52 + 'keyword_optimization' => 21, // Keyword usage and distribution
53 + 'readability' => 16, // Reading ease and comprehension
54 + 'structure' => 16, // Headings, paragraphs, formatting
55 + 'technical_seo' => 11, // Meta tags, URLs, schema
56 + 'user_experience' => 10 // Engagement signals, CTR optimization
43 57 ];
44 58
45 59 /**
46 60 * SEO content templates for different content types
@@ -707,13 +721,8 @@
707 721 // User experience score (10% weight)
708 722 $ux_score = $this->calculate_user_experience_score($content_analysis, $template_compliance);
709 723 $scores['user_experience'] = $ux_score * ($this->optimization_weights['user_experience'] / 100);
710 724
711 - // Semantic relevance score (5% weight)
712 - if (!empty($content_analysis['semantic_analysis']['relevance_score'])) {
713 - $scores['semantic_relevance'] = $content_analysis['semantic_analysis']['relevance_score'] * ($this->optimization_weights['semantic_relevance'] / 100);
714 - }
715 -
716 725 return (int) round(array_sum($scores));
717 726 }
718 727
719 728 /**
@@ -1044,9 +1053,9 @@
1044 1053 // Get recent posts for blog homepage
1045 1054 $recent_posts = get_posts(['numberposts' => 3]);
1046 1055 $content_parts = [];
1047 1056 foreach ($recent_posts as $post) {
1048 - $content_parts[] = $post->post_title . ' ' . wp_trim_words($post->post_content, 100);
1057 + $content_parts[] = $post->post_title . ' ' . \ThinkRank\Core\Seo_Text::trim_words($post->post_content, 100, '...', 1000);
1049 1058 }
1050 1059 $content = implode(' ', $content_parts);
1051 1060 }
1052 1061 break;
@@ -1137,9 +1146,9 @@
1137 1146 'recommendations' => wp_json_encode($results['recommendations'] ?? []),
1138 1147 'analyzed_by' => get_current_user_id()
1139 1148 ];
1140 1149
1141 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Optimization results storage requires direct database access
1150 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Optimization results storage requires direct database access
1142 1151 $result = $wpdb->insert($table_name, $data);
1143 1152
1144 1153 return $result !== false;
1145 1154 }
@@ -1397,9 +1406,9 @@
1397 1406 // Word count recommendations
1398 1407 if (!empty($compliance['word_count_compliance']) &&
1399 1408 $compliance['word_count_compliance']['status'] === 'non_compliant') {
1400 1409 $needed = $compliance['word_count_compliance']['min_required'] -
1401 - $compliance['word_count_compliance']['current_count'];
1410 + $compliance['word_count_compliance']['current_count'];
1402 1411 $recommendations[] = [
1403 1412 'type' => 'word_count',
1404 1413 'priority' => 'high',
1405 1414 'message' => "Add {$needed} more words to meet minimum requirements",
@@ -1441,11 +1450,14 @@
1441 1450
1442 1451 return max(0, $score);
1443 1452 }
1444 1453
1445 - // Placeholder implementations for methods referenced but not yet implemented
1454 + // Placeholder implementations for methods referenced but not yet implemented.
1455 + // Anything here that cannot measure its subject returns null or an empty
1456 + // set, never a plausible-looking number: a consumer can branch on "not
1457 + // measured", but not on an 80 that was typed rather than computed (#538).
1446 1458 private function calculate_performance_metrics(array $content_analysis, array $template_compliance): array {
1447 - return ['performance_score' => 80, 'metrics' => []];
1459 + return ['performance_score' => null, 'metrics' => []];
1448 1460 }
1449 1461
1450 1462 private function generate_optimization_recommendations(array $content_analysis, array $template_compliance, string $content_type): array {
1451 1463 $recommendations = [];
@@ -1462,9 +1474,12 @@
1462 1474 return $recommendations;
1463 1475 }
1464 1476
1465 1477 private function identify_optimization_opportunities(array $content_analysis, array $template_compliance, string $content_type): array {
1466 - return ['opportunities' => [], 'potential_impact' => 'medium'];
1478 + // No potential_impact: with no opportunities found there is nothing to
1479 + // rate, and 'medium' was a verdict on an empty list. The sibling in
1480 + // Performance_Monitoring_Manager already returns just this shape.
1481 + return ['opportunities' => []];
1467 1482 }
1468 1483
1469 1484 private function generate_content_suggestions(array $content_analysis, string $content_type, array $keywords): array {
1470 1485 return ['suggestions' => [], 'content_ideas' => []];
@@ -1493,10 +1508,13 @@
1493 1508 private function calculate_improvement_rate(array $performance_trends): float {
1494 1509 return 0.0;
1495 1510 }
1496 1511
1497 - private function calculate_performance_score(array $metrics): int {
1498 - return 80;
1512 + private function calculate_performance_score(array $metrics): ?int {
1513 + // Nothing here scores $metrics, so there is no score to report. Null
1514 + // travels into track_performance()'s 'performance_score' and reads as
1515 + // "not measured"; an 80 read as a healthy page (#538).
1516 + return null;
1499 1517 }
1500 1518
1501 1519 private function compare_with_benchmarks(array $metrics, string $context_type): array {
1502 1520 return ['benchmark_comparison' => []];
@@ -1502,7 +1520,11 @@
1502 1520 return ['benchmark_comparison' => []];
1503 1521 }
1504 1522
1505 1523 private function store_performance_data(string $context_type, ?int $context_id, array $tracking): bool {
1506 - return true;
1524 + // Reports failure because it stores nothing. The single caller discards
1525 + // the return, so this changes no behaviour today, but a caller added
1526 + // later must not read "stored successfully" from a method with no
1527 + // storage in it (#538).
1528 + return false;
1507 1529 }
1508 1530 }