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 +30 -146 1.28.0 → 2.9.0 View file →
@@ -94,49 +94,24 @@
94 94 ]
95 95 ];
96 96
97 97 /**
98 - * Semantic analysis capabilities
98 + * Content optimization scoring weights
99 99 *
100 - * @since 1.0.0
101 - * @var array
102 - */
103 - private array $semantic_capabilities = [
104 - 'topic_modeling' => [
105 - 'enabled' => true,
106 - 'min_topics' => 3,
107 - 'max_topics' => 10,
108 - 'coherence_threshold' => 0.7
109 - ],
110 - 'entity_recognition' => [
111 - 'enabled' => true,
112 - 'entity_types' => ['PERSON', 'ORGANIZATION', 'LOCATION', 'PRODUCT', 'EVENT'],
113 - 'confidence_threshold' => 0.8
114 - ],
115 - 'sentiment_analysis' => [
116 - 'enabled' => true,
117 - 'scale' => [-1.0, 1.0], // Negative to positive
118 - 'neutral_range' => [-0.1, 0.1]
119 - ],
120 - 'content_classification' => [
121 - 'enabled' => true,
122 - 'categories' => ['informational', 'commercial', 'transactional', 'navigational'],
123 - 'confidence_threshold' => 0.6
124 - ]
125 - ];
126 -
127 - /**
128 - * 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.
129 105 *
130 106 * @since 1.0.0
131 107 * @var array
132 108 */
133 109 private array $scoring_weights = [
134 - 'readability' => 25,
135 - 'keyword_optimization' => 30,
136 - 'content_structure' => 20,
137 - 'semantic_relevance' => 15,
138 - 'technical_seo' => 10
110 + 'readability' => 29,
111 + 'keyword_optimization' => 35,
112 + 'content_structure' => 24,
113 + 'technical_seo' => 12
139 114 ];
140 115
141 116 /**
142 117 * Constructor
@@ -161,9 +136,8 @@
161 136 $analysis = [
162 137 'content_stats' => [],
163 138 'readability' => [],
164 139 'keyword_analysis' => [],
165 - 'semantic_analysis' => [],
166 140 'structure_analysis' => [],
167 141 'optimization_score' => 0,
168 142 'recommendations' => [],
169 143 'ai_confidence' => 0,
@@ -180,11 +154,8 @@
180 154 if (!empty($keywords)) {
181 155 $analysis['keyword_analysis'] = $this->analyze_keywords($content, $keywords, $analysis['content_stats']);
182 156 }
183 157
184 - // Semantic content analysis
185 - $analysis['semantic_analysis'] = $this->analyze_semantic_content($content, $options);
186 -
187 158 // Content structure analysis
188 159 $analysis['structure_analysis'] = $this->analyze_content_structure($content, $options);
189 160
190 161 // Calculate overall optimization score
@@ -330,60 +301,8 @@
330 301 return $keyword_analysis;
331 302 }
332 303
333 304 /**
334 - * Analyze semantic content with topic modeling and entity recognition
335 - *
336 - * @since 1.0.0
337 - *
338 - * @param string $content Content to analyze
339 - * @param array $options Analysis options
340 - * @return array Semantic analysis results
341 - */
342 - public function analyze_semantic_content(string $content, array $options = []): array {
343 - $semantic_analysis = [
344 - 'topic_clusters' => [],
345 - 'entities' => [],
346 - 'sentiment' => [],
347 - 'content_classification' => [],
348 - 'semantic_keywords' => [],
349 - 'coherence_score' => 0,
350 - 'relevance_score' => 0
351 - ];
352 -
353 - // Topic modeling and clustering
354 - if ($this->semantic_capabilities['topic_modeling']['enabled']) {
355 - $semantic_analysis['topic_clusters'] = $this->extract_topic_clusters($content);
356 - }
357 -
358 - // Named entity recognition
359 - if ($this->semantic_capabilities['entity_recognition']['enabled']) {
360 - $semantic_analysis['entities'] = $this->recognize_entities($content);
361 - }
362 -
363 - // Sentiment analysis
364 - if ($this->semantic_capabilities['sentiment_analysis']['enabled']) {
365 - $semantic_analysis['sentiment'] = $this->analyze_sentiment($content);
366 - }
367 -
368 - // Content classification
369 - if ($this->semantic_capabilities['content_classification']['enabled']) {
370 - $semantic_analysis['content_classification'] = $this->classify_content($content);
371 - }
372 -
373 - // Extract semantic keywords
374 - $semantic_analysis['semantic_keywords'] = $this->extract_semantic_keywords($content);
375 -
376 - // Calculate coherence score
377 - $semantic_analysis['coherence_score'] = $this->calculate_content_coherence($semantic_analysis);
378 -
379 - // Calculate relevance score
380 - $semantic_analysis['relevance_score'] = $this->calculate_semantic_relevance($semantic_analysis);
381 -
382 - return $semantic_analysis;
383 - }
384 -
385 - /**
386 305 * Analyze content structure and organization
387 306 *
388 307 * @since 1.0.0
389 308 *
@@ -523,12 +442,14 @@
523 442 $keywords = $this->extract_target_keywords($context_type, $context_id);
524 443
525 444 if (!empty($content)) {
526 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).
527 449 $analysis_options = [
528 450 'readability_enabled' => $settings['readability_enabled'] ?? true,
529 451 'keyword_analysis_enabled' => $settings['keyword_analysis_enabled'] ?? true,
530 - 'semantic_analysis_enabled' => $settings['semantic_analysis_enabled'] ?? true,
531 452 'structure_analysis_enabled' => $settings['structure_analysis_enabled'] ?? true
532 453 ];
533 454
534 455 $output['analysis_results'] = $this->analyze_content($content, $keywords, $analysis_options);
@@ -554,8 +475,11 @@
554 475 $defaults = [
555 476 'enabled' => true,
556 477 'readability_enabled' => true,
557 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.
558 482 'semantic_analysis_enabled' => true,
559 483 'structure_analysis_enabled' => true,
560 484 'auto_analyze' => true,
561 485 'target_reading_level' => 'high_school',
@@ -611,12 +535,17 @@
611 535 'title' => 'Enable Keyword Analysis',
612 536 'description' => 'Analyze keyword density and optimization',
613 537 'default' => true
614 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.
615 544 'semantic_analysis_enabled' => [
616 545 'type' => 'boolean',
617 - 'title' => 'Enable Semantic Analysis',
618 - '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.',
619 548 'default' => true
620 549 ],
621 550 'structure_analysis_enabled' => [
622 551 'type' => 'boolean',
@@ -843,8 +772,9 @@
843 772 private function find_keyword_positions(string $content, string $keyword): array {
844 773 $positions = [];
845 774 $offset = 0;
846 775
776 + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- the canonical strpos() scan loop; hoisting it needs a duplicated call.
847 777 while (($pos = strpos($content, $keyword, $offset)) !== false) {
848 778 $positions[] = $pos;
849 779 $offset = $pos + 1;
850 780 }
@@ -943,9 +873,9 @@
943 873 // Get recent posts for blog homepage
944 874 $recent_posts = get_posts(['numberposts' => 3]);
945 875 $content_parts = [];
946 876 foreach ($recent_posts as $post) {
947 - $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);
948 878 }
949 879 $content = implode(' ', $content_parts);
950 880 }
951 881 break;
@@ -1030,29 +960,24 @@
1030 960 */
1031 961 private function calculate_optimization_score(array $analysis): int {
1032 962 $scores = [];
1033 963
1034 - // Readability score (25% weight)
964 + // Readability score (29% weight)
1035 965 if (!empty($analysis['readability']['readability_score'])) {
1036 966 $scores['readability'] = $analysis['readability']['readability_score'] * ($this->scoring_weights['readability'] / 100);
1037 967 }
1038 968
1039 - // Keyword optimization score (30% weight)
969 + // Keyword optimization score (35% weight)
1040 970 if (!empty($analysis['keyword_analysis']['optimization_score'])) {
1041 971 $scores['keyword_optimization'] = $analysis['keyword_analysis']['optimization_score'] * ($this->scoring_weights['keyword_optimization'] / 100);
1042 972 }
1043 973
1044 - // Content structure score (20% weight)
974 + // Content structure score (24% weight)
1045 975 if (!empty($analysis['structure_analysis']['structure_score'])) {
1046 976 $scores['content_structure'] = $analysis['structure_analysis']['structure_score'] * ($this->scoring_weights['content_structure'] / 100);
1047 977 }
1048 978
1049 - // Semantic relevance score (15% weight)
1050 - if (!empty($analysis['semantic_analysis']['relevance_score'])) {
1051 - $scores['semantic_relevance'] = $analysis['semantic_analysis']['relevance_score'] * ($this->scoring_weights['semantic_relevance'] / 100);
1052 - }
1053 -
1054 - // Technical SEO score (10% weight)
979 + // Technical SEO score (12% weight)
1055 980 $technical_score = $this->calculate_technical_seo_score($analysis);
1056 981 $scores['technical_seo'] = $technical_score * ($this->scoring_weights['technical_seo'] / 100);
1057 982
1058 983 return (int) round(array_sum($scores));
@@ -1122,9 +1047,9 @@
1122 1047 }
1123 1048
1124 1049 // Analysis completeness confidence
1125 1050 $completed_analyses = 0;
1126 - $total_analyses = 4; // readability, keyword, semantic, structure
1051 + $total_analyses = 3; // readability, keyword, structure
1127 1052
1128 1053 if (!empty($analysis['readability'])) {
1129 1054 $completed_analyses++;
1130 1055 }
@@ -1130,11 +1055,8 @@
1130 1055 }
1131 1056 if (!empty($analysis['keyword_analysis'])) {
1132 1057 $completed_analyses++;
1133 1058 }
1134 - if (!empty($analysis['semantic_analysis'])) {
1135 - $completed_analyses++;
1136 - }
1137 1059 if (!empty($analysis['structure_analysis'])) {
1138 1060 $completed_analyses++;
1139 1061 }
1140 1062
@@ -1184,46 +1106,8 @@
1184 1106 $score -= 15;
1185 1107 }
1186 1108
1187 1109 return max(0, $score);
1188 - }
1189 -
1190 - /**
1191 - * Simple implementations for semantic analysis methods
1192 - * These would be enhanced with actual AI/ML libraries in production
1193 - */
1194 -
1195 - private function extract_topic_clusters(string $content): array {
1196 - // Simplified topic extraction
1197 - return ['topics' => ['general content'], 'confidence' => 0.7];
1198 - }
1199 -
1200 - private function recognize_entities(string $content): array {
1201 - // Simplified entity recognition
1202 - return ['entities' => [], 'confidence' => 0.6];
1203 - }
1204 -
1205 - private function analyze_sentiment(string $content): array {
1206 - // Simplified sentiment analysis
1207 - return ['sentiment' => 'neutral', 'score' => 0.0, 'confidence' => 0.7];
1208 - }
1209 -
1210 - private function classify_content(string $content): array {
1211 - // Simplified content classification
1212 - return ['category' => 'informational', 'confidence' => 0.8];
1213 - }
1214 -
1215 - private function extract_semantic_keywords(string $content): array {
1216 - // Simplified semantic keyword extraction
1217 - return [];
1218 - }
1219 -
1220 - private function calculate_content_coherence(array $semantic_analysis): float {
1221 - return 0.8; // Simplified coherence score
1222 - }
1223 -
1224 - private function calculate_semantic_relevance(array $semantic_analysis): float {
1225 - return 75.0; // Simplified relevance score
1226 1110 }
1227 1111
1228 1112 private function analyze_heading_structure(string $content): array {
1229 1113 // Extract headings from HTML content