PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.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 1.0.2 All 50 releases
← All changes | includes/seo/class-ai-content-analyzer.php +36 -147 1.0.0 → 2.9.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 * AI Content Analyzer Class
20 25 *
21 26 * Provides comprehensive content analysis using real AI algorithms including
@@ -89,49 +94,24 @@
89 94 ]
90 95 ];
91 96
92 97 /**
93 - * Semantic analysis capabilities
98 + * Content optimization scoring weights
94 99 *
95 - * @since 1.0.0
96 - * @var array
97 - */
98 - private array $semantic_capabilities = [
99 - 'topic_modeling' => [
100 - 'enabled' => true,
101 - 'min_topics' => 3,
102 - 'max_topics' => 10,
103 - 'coherence_threshold' => 0.7
104 - ],
105 - 'entity_recognition' => [
106 - 'enabled' => true,
107 - 'entity_types' => ['PERSON', 'ORGANIZATION', 'LOCATION', 'PRODUCT', 'EVENT'],
108 - 'confidence_threshold' => 0.8
109 - ],
110 - 'sentiment_analysis' => [
111 - 'enabled' => true,
112 - 'scale' => [-1.0, 1.0], // Negative to positive
113 - 'neutral_range' => [-0.1, 0.1]
114 - ],
115 - 'content_classification' => [
116 - 'enabled' => true,
117 - 'categories' => ['informational', 'commercial', 'transactional', 'navigational'],
118 - 'confidence_threshold' => 0.6
119 - ]
120 - ];
121 -
122 - /**
123 - * Content optimization scoring weights
100 + * Semantic relevance used to sit here at 15%, fed by a block that returned
101 + * the same numbers for every input (#538). Removing a factor would have
102 + * capped the score at 85, so the remaining four were rescaled by 100/85 and
103 + * rounded to whole numbers, which preserves their former ratio (25:30:20:10)
104 + * and keeps the array at 100.
124 105 *
125 106 * @since 1.0.0
126 107 * @var array
127 108 */
128 109 private array $scoring_weights = [
129 - 'readability' => 25,
130 - 'keyword_optimization' => 30,
131 - 'content_structure' => 20,
132 - 'semantic_relevance' => 15,
133 - 'technical_seo' => 10
110 + 'readability' => 29,
111 + 'keyword_optimization' => 35,
112 + 'content_structure' => 24,
113 + 'technical_seo' => 12
134 114 ];
135 115
136 116 /**
137 117 * Constructor
@@ -156,9 +136,8 @@
156 136 $analysis = [
157 137 'content_stats' => [],
158 138 'readability' => [],
159 139 'keyword_analysis' => [],
160 - 'semantic_analysis' => [],
161 140 'structure_analysis' => [],
162 141 'optimization_score' => 0,
163 142 'recommendations' => [],
164 143 'ai_confidence' => 0,
@@ -175,11 +154,8 @@
175 154 if (!empty($keywords)) {
176 155 $analysis['keyword_analysis'] = $this->analyze_keywords($content, $keywords, $analysis['content_stats']);
177 156 }
178 157
179 - // Semantic content analysis
180 - $analysis['semantic_analysis'] = $this->analyze_semantic_content($content, $options);
181 -
182 158 // Content structure analysis
183 159 $analysis['structure_analysis'] = $this->analyze_content_structure($content, $options);
184 160
185 161 // Calculate overall optimization score
@@ -325,60 +301,8 @@
325 301 return $keyword_analysis;
326 302 }
327 303
328 304 /**
329 - * Analyze semantic content with topic modeling and entity recognition
330 - *
331 - * @since 1.0.0
332 - *
333 - * @param string $content Content to analyze
334 - * @param array $options Analysis options
335 - * @return array Semantic analysis results
336 - */
337 - public function analyze_semantic_content(string $content, array $options = []): array {
338 - $semantic_analysis = [
339 - 'topic_clusters' => [],
340 - 'entities' => [],
341 - 'sentiment' => [],
342 - 'content_classification' => [],
343 - 'semantic_keywords' => [],
344 - 'coherence_score' => 0,
345 - 'relevance_score' => 0
346 - ];
347 -
348 - // Topic modeling and clustering
349 - if ($this->semantic_capabilities['topic_modeling']['enabled']) {
350 - $semantic_analysis['topic_clusters'] = $this->extract_topic_clusters($content);
351 - }
352 -
353 - // Named entity recognition
354 - if ($this->semantic_capabilities['entity_recognition']['enabled']) {
355 - $semantic_analysis['entities'] = $this->recognize_entities($content);
356 - }
357 -
358 - // Sentiment analysis
359 - if ($this->semantic_capabilities['sentiment_analysis']['enabled']) {
360 - $semantic_analysis['sentiment'] = $this->analyze_sentiment($content);
361 - }
362 -
363 - // Content classification
364 - if ($this->semantic_capabilities['content_classification']['enabled']) {
365 - $semantic_analysis['content_classification'] = $this->classify_content($content);
366 - }
367 -
368 - // Extract semantic keywords
369 - $semantic_analysis['semantic_keywords'] = $this->extract_semantic_keywords($content);
370 -
371 - // Calculate coherence score
372 - $semantic_analysis['coherence_score'] = $this->calculate_content_coherence($semantic_analysis);
373 -
374 - // Calculate relevance score
375 - $semantic_analysis['relevance_score'] = $this->calculate_semantic_relevance($semantic_analysis);
376 -
377 - return $semantic_analysis;
378 - }
379 -
380 - /**
381 305 * Analyze content structure and organization
382 306 *
383 307 * @since 1.0.0
384 308 *
@@ -518,12 +442,14 @@
518 442 $keywords = $this->extract_target_keywords($context_type, $context_id);
519 443
520 444 if (!empty($content)) {
521 445 // Perform comprehensive analysis
446 + // semantic_analysis_enabled is deliberately not passed: the block it
447 + // gated was removed in #538 and nothing downstream reads the option.
448 + // The setting itself stays, so stored rows are not purged (#452).
522 449 $analysis_options = [
523 450 'readability_enabled' => $settings['readability_enabled'] ?? true,
524 451 'keyword_analysis_enabled' => $settings['keyword_analysis_enabled'] ?? true,
525 - 'semantic_analysis_enabled' => $settings['semantic_analysis_enabled'] ?? true,
526 452 'structure_analysis_enabled' => $settings['structure_analysis_enabled'] ?? true
527 453 ];
528 454
529 455 $output['analysis_results'] = $this->analyze_content($content, $keywords, $analysis_options);
@@ -549,8 +475,11 @@
549 475 $defaults = [
550 476 'enabled' => true,
551 477 'readability_enabled' => true,
552 478 'keyword_analysis_enabled' => true,
479 + // Inert since #538; see get_settings_schema(). The default stays
480 + // true so the value this endpoint reports for a site that never
481 + // saved the key does not change, and nothing reads it either way.
553 482 'semantic_analysis_enabled' => true,
554 483 'structure_analysis_enabled' => true,
555 484 'auto_analyze' => true,
556 485 'target_reading_level' => 'high_school',
@@ -606,12 +535,17 @@
606 535 'title' => 'Enable Keyword Analysis',
607 536 'description' => 'Analyze keyword density and optimization',
608 537 'default' => true
609 538 ],
539 + // Retired in #538, but still published so a client that stored the
540 + // key sees it described rather than missing. The title and
541 + // description say plainly that it does nothing: the topic modeling
542 + // and entity recognition they used to promise were a block that
543 + // returned the same answer for every input.
610 544 'semantic_analysis_enabled' => [
611 545 'type' => 'boolean',
612 - 'title' => 'Enable Semantic Analysis',
613 - 'description' => 'Perform topic modeling and entity recognition',
546 + 'title' => 'Semantic Analysis (retired)',
547 + 'description' => 'Has no effect. The topic modeling and entity recognition this gated returned fixed values for every input and were removed. The key is still accepted so existing stored settings are not purged.',
614 548 'default' => true
615 549 ],
616 550 'structure_analysis_enabled' => [
617 551 'type' => 'boolean',
@@ -838,8 +772,9 @@
838 772 private function find_keyword_positions(string $content, string $keyword): array {
839 773 $positions = [];
840 774 $offset = 0;
841 775
776 + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- the canonical strpos() scan loop; hoisting it needs a duplicated call.
842 777 while (($pos = strpos($content, $keyword, $offset)) !== false) {
843 778 $positions[] = $pos;
844 779 $offset = $pos + 1;
845 780 }
@@ -938,9 +873,9 @@
938 873 // Get recent posts for blog homepage
939 874 $recent_posts = get_posts(['numberposts' => 3]);
940 875 $content_parts = [];
941 876 foreach ($recent_posts as $post) {
942 - $content_parts[] = $post->post_title . ' ' . wp_trim_words($post->post_content, 100);
877 + $content_parts[] = $post->post_title . ' ' . \ThinkRank\Core\Seo_Text::trim_words($post->post_content, 100, '...', 1000);
943 878 }
944 879 $content = implode(' ', $content_parts);
945 880 }
946 881 break;
@@ -1008,9 +943,9 @@
1008 943 'recommendations' => wp_json_encode($results['recommendations'] ?? []),
1009 944 'analyzed_by' => get_current_user_id()
1010 945 ];
1011 946
1012 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analysis results storage requires direct database access
947 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Analysis results storage requires direct database access
1013 948 $result = $wpdb->insert($table_name, $data);
1014 949
1015 950 return $result !== false;
1016 951 }
@@ -1025,29 +960,24 @@
1025 960 */
1026 961 private function calculate_optimization_score(array $analysis): int {
1027 962 $scores = [];
1028 963
1029 - // Readability score (25% weight)
964 + // Readability score (29% weight)
1030 965 if (!empty($analysis['readability']['readability_score'])) {
1031 966 $scores['readability'] = $analysis['readability']['readability_score'] * ($this->scoring_weights['readability'] / 100);
1032 967 }
1033 968
1034 - // Keyword optimization score (30% weight)
969 + // Keyword optimization score (35% weight)
1035 970 if (!empty($analysis['keyword_analysis']['optimization_score'])) {
1036 971 $scores['keyword_optimization'] = $analysis['keyword_analysis']['optimization_score'] * ($this->scoring_weights['keyword_optimization'] / 100);
1037 972 }
1038 973
1039 - // Content structure score (20% weight)
974 + // Content structure score (24% weight)
1040 975 if (!empty($analysis['structure_analysis']['structure_score'])) {
1041 976 $scores['content_structure'] = $analysis['structure_analysis']['structure_score'] * ($this->scoring_weights['content_structure'] / 100);
1042 977 }
1043 978
1044 - // Semantic relevance score (15% weight)
1045 - if (!empty($analysis['semantic_analysis']['relevance_score'])) {
1046 - $scores['semantic_relevance'] = $analysis['semantic_analysis']['relevance_score'] * ($this->scoring_weights['semantic_relevance'] / 100);
1047 - }
1048 -
1049 - // Technical SEO score (10% weight)
979 + // Technical SEO score (12% weight)
1050 980 $technical_score = $this->calculate_technical_seo_score($analysis);
1051 981 $scores['technical_seo'] = $technical_score * ($this->scoring_weights['technical_seo'] / 100);
1052 982
1053 983 return (int) round(array_sum($scores));
@@ -1117,9 +1047,9 @@
1117 1047 }
1118 1048
1119 1049 // Analysis completeness confidence
1120 1050 $completed_analyses = 0;
1121 - $total_analyses = 4; // readability, keyword, semantic, structure
1051 + $total_analyses = 3; // readability, keyword, structure
1122 1052
1123 1053 if (!empty($analysis['readability'])) {
1124 1054 $completed_analyses++;
1125 1055 }
@@ -1125,11 +1055,8 @@
1125 1055 }
1126 1056 if (!empty($analysis['keyword_analysis'])) {
1127 1057 $completed_analyses++;
1128 1058 }
1129 - if (!empty($analysis['semantic_analysis'])) {
1130 - $completed_analyses++;
1131 - }
1132 1059 if (!empty($analysis['structure_analysis'])) {
1133 1060 $completed_analyses++;
1134 1061 }
1135 1062
@@ -1179,46 +1106,8 @@
1179 1106 $score -= 15;
1180 1107 }
1181 1108
1182 1109 return max(0, $score);
1183 - }
1184 -
1185 - /**
1186 - * Simple implementations for semantic analysis methods
1187 - * These would be enhanced with actual AI/ML libraries in production
1188 - */
1189 -
1190 - private function extract_topic_clusters(string $content): array {
1191 - // Simplified topic extraction
1192 - return ['topics' => ['general content'], 'confidence' => 0.7];
1193 - }
1194 -
1195 - private function recognize_entities(string $content): array {
1196 - // Simplified entity recognition
1197 - return ['entities' => [], 'confidence' => 0.6];
1198 - }
1199 -
1200 - private function analyze_sentiment(string $content): array {
1201 - // Simplified sentiment analysis
1202 - return ['sentiment' => 'neutral', 'score' => 0.0, 'confidence' => 0.7];
1203 - }
1204 -
1205 - private function classify_content(string $content): array {
1206 - // Simplified content classification
1207 - return ['category' => 'informational', 'confidence' => 0.8];
1208 - }
1209 -
1210 - private function extract_semantic_keywords(string $content): array {
1211 - // Simplified semantic keyword extraction
1212 - return [];
1213 - }
1214 -
1215 - private function calculate_content_coherence(array $semantic_analysis): float {
1216 - return 0.8; // Simplified coherence score
1217 - }
1218 -
1219 - private function calculate_semantic_relevance(array $semantic_analysis): float {
1220 - return 75.0; // Simplified relevance score
1221 1110 }
1222 1111
1223 1112 private function analyze_heading_structure(string $content): array {
1224 1113 // Extract headings from HTML content