PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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-ai-content-analyzer.php

class-ai-content-analyzer.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.7.0, at includes/seo/class-ai-content-analyzer.php

1,382 lines 48.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Content Analyzer Class
4 *
5 * Comprehensive AI-powered content analysis with real algorithms for readability,
6 * keyword density, semantic analysis, and content optimization. Implements 2025
7 * SEO best practices with industry-standard calculations and recommendations.
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 * AI Content Analyzer Class
25 *
26 * Provides comprehensive content analysis using real AI algorithms including
27 * readability scoring, keyword analysis, semantic content analysis, and
28 * structure optimization with actionable recommendations.
29 *
30 * @since 1.0.0
31 */
32 class AI_Content_Analyzer extends Abstract_SEO_Manager {
33
34 /**
35 * Content analysis algorithms with their specifications
36 *
37 * @since 1.0.0
38 * @var array
39 */
40 private array $analysis_algorithms = [
41 'readability' => [
42 'flesch_reading_ease' => [
43 'name' => 'Flesch Reading Ease',
44 'formula' => '206.835 - (1.015 × ASL) - (84.6 × ASW)',
45 'scale' => [90 => 'Very Easy', 80 => 'Easy', 70 => 'Fairly Easy', 60 => 'Standard', 50 => 'Fairly Difficult', 30 => 'Difficult', 0 => 'Very Difficult'],
46 'weight' => 40
47 ],
48 'flesch_kincaid_grade' => [
49 'name' => 'Flesch-Kincaid Grade Level',
50 'formula' => '(0.39 × ASL) + (11.8 × ASW) - 15.59',
51 'target_range' => [6, 8],
52 'weight' => 30
53 ],
54 'automated_readability_index' => [
55 'name' => 'Automated Readability Index',
56 'formula' => '(4.71 × CPW) + (0.5 × SPW) - 21.43',
57 'target_range' => [6, 10],
58 'weight' => 30
59 ]
60 ],
61 'keyword_analysis' => [
62 'density_calculation' => [
63 'optimal_range' => [1.0, 3.0], // 1-3% keyword density
64 'warning_threshold' => 5.0, // Above 5% is keyword stuffing
65 'minimum_threshold' => 0.5 // Below 0.5% is under-optimized
66 ],
67 'proximity_analysis' => [
68 'ideal_distance' => 100, // Words between keyword occurrences
69 'clustering_threshold' => 50 // Words indicating keyword clustering
70 ],
71 'semantic_variations' => [
72 'synonym_weight' => 0.7,
73 'related_terms_weight' => 0.5,
74 'lsi_keywords_weight' => 0.8
75 ]
76 ],
77 'content_structure' => [
78 'heading_hierarchy' => [
79 'h1_count' => ['min' => 1, 'max' => 1],
80 'h2_count' => ['min' => 2, 'max' => 10],
81 'h3_count' => ['min' => 0, 'max' => 20],
82 'proper_nesting' => true
83 ],
84 'paragraph_analysis' => [
85 'ideal_length' => [50, 150], // Words per paragraph
86 'max_sentences' => 4,
87 'transition_words_ratio' => 0.3
88 ],
89 'content_length' => [
90 'blog_post' => ['min' => 300, 'optimal' => 1500, 'max' => 3000],
91 'product_page' => ['min' => 200, 'optimal' => 800, 'max' => 1500],
92 'landing_page' => ['min' => 500, 'optimal' => 2000, 'max' => 4000]
93 ]
94 ]
95 ];
96
97 /**
98 * Semantic analysis capabilities
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
129 *
130 * @since 1.0.0
131 * @var array
132 */
133 private array $scoring_weights = [
134 'readability' => 25,
135 'keyword_optimization' => 30,
136 'content_structure' => 20,
137 'semantic_relevance' => 15,
138 'technical_seo' => 10
139 ];
140
141 /**
142 * Constructor
143 *
144 * @since 1.0.0
145 */
146 public function __construct() {
147 parent::__construct('ai_content_analyzer');
148 }
149
150 /**
151 * Analyze content with comprehensive AI-powered analysis
152 *
153 * @since 1.0.0
154 *
155 * @param string $content Content to analyze
156 * @param array $keywords Target keywords
157 * @param array $options Analysis options
158 * @return array Comprehensive analysis results
159 */
160 public function analyze_content(string $content, array $keywords = [], array $options = []): array {
161 $analysis = [
162 'content_stats' => [],
163 'readability' => [],
164 'keyword_analysis' => [],
165 'semantic_analysis' => [],
166 'structure_analysis' => [],
167 'optimization_score' => 0,
168 'recommendations' => [],
169 'ai_confidence' => 0,
170 'analysis_timestamp' => current_time('mysql')
171 ];
172
173 // Basic content statistics
174 $analysis['content_stats'] = $this->calculate_content_statistics($content);
175
176 // Readability analysis with real algorithms
177 $analysis['readability'] = $this->analyze_readability($content, $analysis['content_stats']);
178
179 // Keyword analysis and optimization
180 if (!empty($keywords)) {
181 $analysis['keyword_analysis'] = $this->analyze_keywords($content, $keywords, $analysis['content_stats']);
182 }
183
184 // Semantic content analysis
185 $analysis['semantic_analysis'] = $this->analyze_semantic_content($content, $options);
186
187 // Content structure analysis
188 $analysis['structure_analysis'] = $this->analyze_content_structure($content, $options);
189
190 // Calculate overall optimization score
191 $analysis['optimization_score'] = $this->calculate_optimization_score($analysis);
192
193 // Generate AI-powered recommendations
194 $analysis['recommendations'] = $this->generate_optimization_recommendations($analysis);
195
196 // Calculate AI confidence score
197 $analysis['ai_confidence'] = $this->calculate_ai_confidence($analysis);
198
199 return $analysis;
200 }
201
202 /**
203 * Analyze readability using real algorithms
204 *
205 * @since 1.0.0
206 *
207 * @param string $content Content to analyze
208 * @param array $stats Content statistics
209 * @return array Readability analysis results
210 */
211 public function analyze_readability(string $content, array $stats): array {
212 $readability = [
213 'flesch_reading_ease' => 0,
214 'flesch_kincaid_grade' => 0,
215 'automated_readability_index' => 0,
216 'average_sentence_length' => 0,
217 'average_syllables_per_word' => 0,
218 'readability_score' => 0,
219 'reading_level' => '',
220 'recommendations' => []
221 ];
222
223 if ($stats['word_count'] === 0 || $stats['sentence_count'] === 0) {
224 return $readability;
225 }
226
227 // Calculate metrics
228 $readability['average_sentence_length'] = $stats['word_count'] / $stats['sentence_count'];
229 $readability['average_syllables_per_word'] = $stats['syllable_count'] / $stats['word_count'];
230 $characters_per_word = $stats['character_count'] / $stats['word_count'];
231
232 // Flesch Reading Ease (real algorithm)
233 $readability['flesch_reading_ease'] = 206.835
234 - (1.015 * $readability['average_sentence_length'])
235 - (84.6 * $readability['average_syllables_per_word']);
236
237 // Flesch-Kincaid Grade Level (real algorithm)
238 $readability['flesch_kincaid_grade'] = (0.39 * $readability['average_sentence_length'])
239 + (11.8 * $readability['average_syllables_per_word'])
240 - 15.59;
241
242 // Automated Readability Index (real algorithm)
243 $readability['automated_readability_index'] = (4.71 * $characters_per_word)
244 + (0.5 * $readability['average_sentence_length'])
245 - 21.43;
246
247 // Determine reading level
248 $readability['reading_level'] = $this->determine_reading_level($readability['flesch_reading_ease']);
249
250 // Calculate overall readability score (0-100)
251 $readability['readability_score'] = $this->calculate_readability_score($readability);
252
253 // Generate readability recommendations
254 $readability['recommendations'] = $this->generate_readability_recommendations($readability);
255
256 return $readability;
257 }
258
259 /**
260 * Analyze keywords with density and optimization metrics
261 *
262 * @since 1.0.0
263 *
264 * @param string $content Content to analyze
265 * @param array $keywords Target keywords
266 * @param array $stats Content statistics
267 * @return array Keyword analysis results
268 */
269 public function analyze_keywords(string $content, array $keywords, array $stats): array {
270 $keyword_analysis = [
271 'primary_keywords' => [],
272 'secondary_keywords' => [],
273 'keyword_density' => [],
274 'keyword_distribution' => [],
275 'semantic_keywords' => [],
276 'optimization_score' => 0,
277 'recommendations' => []
278 ];
279
280 $content_lower = strtolower($content);
281 $words = str_word_count($content_lower, 1);
282
283 foreach ($keywords as $index => $keyword) {
284 $keyword_lower = strtolower($keyword);
285 $keyword_data = [
286 'keyword' => $keyword,
287 'occurrences' => 0,
288 'density' => 0,
289 'positions' => [],
290 'distribution_score' => 0,
291 'optimization_status' => 'under_optimized'
292 ];
293
294 // Count keyword occurrences
295 $keyword_data['occurrences'] = substr_count($content_lower, $keyword_lower);
296
297 // Calculate keyword density
298 if ($stats['word_count'] > 0) {
299 $keyword_data['density'] = ($keyword_data['occurrences'] / $stats['word_count']) * 100;
300 }
301
302 // Find keyword positions for distribution analysis
303 $keyword_data['positions'] = $this->find_keyword_positions($content_lower, $keyword_lower);
304
305 // Calculate distribution score
306 $keyword_data['distribution_score'] = $this->calculate_keyword_distribution($keyword_data['positions'], $stats['word_count']);
307
308 // Determine optimization status
309 $keyword_data['optimization_status'] = $this->determine_keyword_optimization_status($keyword_data['density']);
310
311 // Categorize as primary or secondary
312 if ($index === 0 || $keyword_data['density'] >= 1.0) {
313 $keyword_analysis['primary_keywords'][] = $keyword_data;
314 } else {
315 $keyword_analysis['secondary_keywords'][] = $keyword_data;
316 }
317
318 $keyword_analysis['keyword_density'][$keyword] = $keyword_data['density'];
319 }
320
321 // Find semantic keywords
322 $keyword_analysis['semantic_keywords'] = $this->find_semantic_keywords($content, $keywords);
323
324 // Calculate overall keyword optimization score
325 $keyword_analysis['optimization_score'] = $this->calculate_keyword_optimization_score($keyword_analysis);
326
327 // Generate keyword recommendations
328 $keyword_analysis['recommendations'] = $this->generate_keyword_recommendations($keyword_analysis);
329
330 return $keyword_analysis;
331 }
332
333 /**
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 * Analyze content structure and organization
387 *
388 * @since 1.0.0
389 *
390 * @param string $content Content to analyze
391 * @param array $options Analysis options
392 * @return array Structure analysis results
393 */
394 public function analyze_content_structure(string $content, array $options = []): array {
395 $structure_analysis = [
396 'heading_structure' => [],
397 'paragraph_analysis' => [],
398 'list_usage' => [],
399 'image_analysis' => [],
400 'link_analysis' => [],
401 'content_flow' => [],
402 'structure_score' => 0,
403 'recommendations' => []
404 ];
405
406 // Analyze heading hierarchy
407 $structure_analysis['heading_structure'] = $this->analyze_heading_structure($content);
408
409 // Analyze paragraph structure
410 $structure_analysis['paragraph_analysis'] = $this->analyze_paragraph_structure($content);
411
412 // Analyze list usage
413 $structure_analysis['list_usage'] = $this->analyze_list_usage($content);
414
415 // Analyze images
416 $structure_analysis['image_analysis'] = $this->analyze_image_usage($content);
417
418 // Analyze internal and external links
419 $structure_analysis['link_analysis'] = $this->analyze_link_structure($content);
420
421 // Analyze content flow and transitions
422 $structure_analysis['content_flow'] = $this->analyze_content_flow($content);
423
424 // Calculate structure score
425 $structure_analysis['structure_score'] = $this->calculate_structure_score($structure_analysis);
426
427 // Generate structure recommendations
428 $structure_analysis['recommendations'] = $this->generate_structure_recommendations($structure_analysis);
429
430 return $structure_analysis;
431 }
432
433 /**
434 * Validate SEO settings (implements interface)
435 *
436 * @since 1.0.0
437 *
438 * @param array $settings Settings array to validate
439 * @return array Validation results
440 */
441 public function validate_settings(array $settings): array {
442 $validation = [
443 'valid' => true,
444 'errors' => [],
445 'warnings' => [],
446 'suggestions' => [],
447 'score' => 100
448 ];
449
450 // Validate analysis algorithms settings
451 if (isset($settings['readability_enabled']) && !is_bool($settings['readability_enabled'])) {
452 $validation['errors'][] = 'Readability analysis setting must be boolean';
453 $validation['valid'] = false;
454 }
455
456 if (isset($settings['keyword_analysis_enabled']) && !is_bool($settings['keyword_analysis_enabled'])) {
457 $validation['errors'][] = 'Keyword analysis setting must be boolean';
458 $validation['valid'] = false;
459 }
460
461 if (isset($settings['semantic_analysis_enabled']) && !is_bool($settings['semantic_analysis_enabled'])) {
462 $validation['errors'][] = 'Semantic analysis setting must be boolean';
463 $validation['valid'] = false;
464 }
465
466 // Validate scoring weights
467 if (isset($settings['scoring_weights']) && is_array($settings['scoring_weights'])) {
468 $total_weight = array_sum($settings['scoring_weights']);
469 if ($total_weight !== 100) {
470 $validation['warnings'][] = 'Scoring weights should total 100%';
471 }
472 }
473
474 // Validate keyword density thresholds
475 if (isset($settings['keyword_density_threshold'])) {
476 $threshold = $settings['keyword_density_threshold'];
477 if (!is_numeric($threshold) || $threshold < 0 || $threshold > 10) {
478 $validation['errors'][] = 'Keyword density threshold must be between 0 and 10';
479 $validation['valid'] = false;
480 }
481 }
482
483 // Validate readability targets
484 if (isset($settings['target_reading_level'])) {
485 $valid_levels = ['elementary', 'middle_school', 'high_school', 'college', 'graduate'];
486 if (!in_array($settings['target_reading_level'], $valid_levels, true)) {
487 $validation['errors'][] = 'Invalid target reading level specified';
488 $validation['valid'] = false;
489 }
490 }
491
492 // Calculate validation score
493 $validation['score'] = $this->calculate_validation_score($validation);
494
495 return $validation;
496 }
497
498 /**
499 * Get output data for frontend rendering (implements interface)
500 *
501 * @since 1.0.0
502 *
503 * @param string $context_type The context type
504 * @param int|null $context_id Optional. Context ID
505 * @return array Output data ready for frontend rendering
506 */
507 public function get_output_data(string $context_type, ?int $context_id): array {
508 $settings = $this->get_settings($context_type, $context_id);
509
510 $output = [
511 'analysis_results' => [],
512 'recommendations' => [],
513 'optimization_score' => 0,
514 'enabled' => $settings['enabled'] ?? true
515 ];
516
517 if (!$output['enabled']) {
518 return $output;
519 }
520
521 // Get content for analysis
522 $content = $this->extract_content_for_analysis($context_type, $context_id);
523 $keywords = $this->extract_target_keywords($context_type, $context_id);
524
525 if (!empty($content)) {
526 // Perform comprehensive analysis
527 $analysis_options = [
528 'readability_enabled' => $settings['readability_enabled'] ?? true,
529 'keyword_analysis_enabled' => $settings['keyword_analysis_enabled'] ?? true,
530 'semantic_analysis_enabled' => $settings['semantic_analysis_enabled'] ?? true,
531 'structure_analysis_enabled' => $settings['structure_analysis_enabled'] ?? true
532 ];
533
534 $output['analysis_results'] = $this->analyze_content($content, $keywords, $analysis_options);
535 $output['recommendations'] = $output['analysis_results']['recommendations'] ?? [];
536 $output['optimization_score'] = $output['analysis_results']['optimization_score'] ?? 0;
537
538 // Store analysis results in database
539 $this->store_analysis_results($context_type, $context_id, $output['analysis_results']);
540 }
541
542 return $output;
543 }
544
545 /**
546 * Get default settings for a context type (implements interface)
547 *
548 * @since 1.0.0
549 *
550 * @param string $context_type The context type to get defaults for
551 * @return array Default settings array
552 */
553 public function get_default_settings(string $context_type): array {
554 $defaults = [
555 'enabled' => true,
556 'readability_enabled' => true,
557 'keyword_analysis_enabled' => true,
558 'semantic_analysis_enabled' => true,
559 'structure_analysis_enabled' => true,
560 'auto_analyze' => true,
561 'target_reading_level' => 'high_school',
562 'keyword_density_threshold' => 3.0,
563 'min_content_length' => 300,
564 'scoring_weights' => $this->scoring_weights
565 ];
566
567 // Context-specific defaults
568 switch ($context_type) {
569 case 'post':
570 $defaults['min_content_length'] = 300;
571 $defaults['target_reading_level'] = 'high_school';
572 break;
573 case 'page':
574 $defaults['min_content_length'] = 200;
575 $defaults['target_reading_level'] = 'middle_school';
576 break;
577 case 'product':
578 $defaults['min_content_length'] = 150;
579 $defaults['target_reading_level'] = 'middle_school';
580 $defaults['keyword_density_threshold'] = 2.5;
581 break;
582 }
583
584 return $defaults;
585 }
586
587 /**
588 * Get settings schema definition (implements interface)
589 *
590 * @since 1.0.0
591 *
592 * @param string $context_type The context type to get schema for
593 * @return array Settings schema definition
594 */
595 public function get_settings_schema(string $context_type): array {
596 return [
597 'enabled' => [
598 'type' => 'boolean',
599 'title' => 'Enable AI Content Analysis',
600 'description' => 'Enable AI-powered content analysis and optimization',
601 'default' => true
602 ],
603 'readability_enabled' => [
604 'type' => 'boolean',
605 'title' => 'Enable Readability Analysis',
606 'description' => 'Analyze content readability using Flesch-Kincaid algorithms',
607 'default' => true
608 ],
609 'keyword_analysis_enabled' => [
610 'type' => 'boolean',
611 'title' => 'Enable Keyword Analysis',
612 'description' => 'Analyze keyword density and optimization',
613 'default' => true
614 ],
615 'semantic_analysis_enabled' => [
616 'type' => 'boolean',
617 'title' => 'Enable Semantic Analysis',
618 'description' => 'Perform topic modeling and entity recognition',
619 'default' => true
620 ],
621 'structure_analysis_enabled' => [
622 'type' => 'boolean',
623 'title' => 'Enable Structure Analysis',
624 'description' => 'Analyze content structure and organization',
625 'default' => true
626 ],
627 'target_reading_level' => [
628 'type' => 'string',
629 'title' => 'Target Reading Level',
630 'description' => 'Target reading level for content optimization',
631 'enum' => ['elementary', 'middle_school', 'high_school', 'college', 'graduate'],
632 'default' => 'high_school'
633 ],
634 'keyword_density_threshold' => [
635 'type' => 'number',
636 'title' => 'Keyword Density Threshold',
637 'description' => 'Maximum keyword density percentage (1-10%)',
638 'minimum' => 1,
639 'maximum' => 10,
640 'default' => 3.0
641 ],
642 'min_content_length' => [
643 'type' => 'integer',
644 'title' => 'Minimum Content Length',
645 'description' => 'Minimum word count for content analysis',
646 'minimum' => 50,
647 'default' => 300
648 ]
649 ];
650 }
651
652 /**
653 * Calculate content statistics
654 *
655 * @since 1.0.0
656 *
657 * @param string $content Content to analyze
658 * @return array Content statistics
659 */
660 private function calculate_content_statistics(string $content): array {
661 $clean_content = wp_strip_all_tags($content);
662
663 $stats = [
664 'character_count' => strlen($clean_content),
665 'character_count_no_spaces' => strlen(str_replace(' ', '', $clean_content)),
666 'word_count' => str_word_count($clean_content),
667 'sentence_count' => 0,
668 'paragraph_count' => 0,
669 'syllable_count' => 0,
670 'complex_words' => 0,
671 'average_words_per_sentence' => 0,
672 'average_syllables_per_word' => 0
673 ];
674
675 // Count sentences
676 $sentences = preg_split('/[.!?]+/', $clean_content, -1, PREG_SPLIT_NO_EMPTY);
677 $stats['sentence_count'] = count($sentences);
678
679 // Count paragraphs
680 $paragraphs = preg_split('/\n\s*\n/', trim($content), -1, PREG_SPLIT_NO_EMPTY);
681 $stats['paragraph_count'] = count($paragraphs);
682
683 // Count syllables
684 $words = str_word_count($clean_content, 1);
685 foreach ($words as $word) {
686 $syllables = $this->count_syllables($word);
687 $stats['syllable_count'] += $syllables;
688
689 // Count complex words (3+ syllables)
690 if ($syllables >= 3) {
691 $stats['complex_words']++;
692 }
693 }
694
695 // Calculate averages
696 if ($stats['sentence_count'] > 0) {
697 $stats['average_words_per_sentence'] = $stats['word_count'] / $stats['sentence_count'];
698 }
699
700 if ($stats['word_count'] > 0) {
701 $stats['average_syllables_per_word'] = $stats['syllable_count'] / $stats['word_count'];
702 }
703
704 return $stats;
705 }
706
707 /**
708 * Count syllables in a word (real algorithm)
709 *
710 * @since 1.0.0
711 *
712 * @param string $word Word to count syllables for
713 * @return int Number of syllables
714 */
715 private function count_syllables(string $word): int {
716 $word = strtolower($word);
717 $word = preg_replace('/[^a-z]/', '', $word);
718
719 if (strlen($word) <= 3) {
720 return 1;
721 }
722
723 // Remove common endings that don't add syllables
724 $word = preg_replace('/(?:[^laeiouy]es|ed|[^laeiouy]e)$/', '', $word);
725 $word = preg_replace('/^y/', '', $word);
726
727 // Count vowel groups
728 $matches = preg_match_all('/[aeiouy]{1,2}/', $word);
729
730 return max(1, $matches);
731 }
732
733 /**
734 * Determine reading level from Flesch Reading Ease score
735 *
736 * @since 1.0.0
737 *
738 * @param float $flesch_score Flesch Reading Ease score
739 * @return string Reading level description
740 */
741 private function determine_reading_level(float $flesch_score): string {
742 if ($flesch_score >= 90) {
743 return 'Very Easy (5th grade)';
744 } elseif ($flesch_score >= 80) {
745 return 'Easy (6th grade)';
746 } elseif ($flesch_score >= 70) {
747 return 'Fairly Easy (7th grade)';
748 } elseif ($flesch_score >= 60) {
749 return 'Standard (8th-9th grade)';
750 } elseif ($flesch_score >= 50) {
751 return 'Fairly Difficult (10th-12th grade)';
752 } elseif ($flesch_score >= 30) {
753 return 'Difficult (College level)';
754 } else {
755 return 'Very Difficult (Graduate level)';
756 }
757 }
758
759 /**
760 * Calculate overall readability score
761 *
762 * @since 1.0.0
763 *
764 * @param array $readability Readability metrics
765 * @return int Readability score (0-100)
766 */
767 private function calculate_readability_score(array $readability): int {
768 $scores = [];
769
770 // Flesch Reading Ease (40% weight)
771 $scores[] = max(0, min(100, $readability['flesch_reading_ease'])) * 0.4;
772
773 // Flesch-Kincaid Grade Level (30% weight) - convert to 0-100 scale
774 $grade_score = max(0, min(100, 100 - ($readability['flesch_kincaid_grade'] * 5)));
775 $scores[] = $grade_score * 0.3;
776
777 // Automated Readability Index (30% weight) - convert to 0-100 scale
778 $ari_score = max(0, min(100, 100 - ($readability['automated_readability_index'] * 5)));
779 $scores[] = $ari_score * 0.3;
780
781 return (int) round(array_sum($scores));
782 }
783
784 /**
785 * Generate readability recommendations
786 *
787 * @since 1.0.0
788 *
789 * @param array $readability Readability analysis
790 * @return array Recommendations
791 */
792 private function generate_readability_recommendations(array $readability): array {
793 $recommendations = [];
794
795 if ($readability['flesch_reading_ease'] < 60) {
796 $recommendations[] = [
797 'type' => 'readability',
798 'priority' => 'high',
799 'message' => 'Content is difficult to read. Consider shorter sentences and simpler words.',
800 'action' => 'Reduce average sentence length to under 20 words'
801 ];
802 }
803
804 if ($readability['average_sentence_length'] > 25) {
805 $recommendations[] = [
806 'type' => 'sentence_length',
807 'priority' => 'medium',
808 'message' => 'Sentences are too long. Break them into shorter, clearer sentences.',
809 'action' => 'Target 15-20 words per sentence'
810 ];
811 }
812
813 if ($readability['average_syllables_per_word'] > 1.7) {
814 $recommendations[] = [
815 'type' => 'word_complexity',
816 'priority' => 'medium',
817 'message' => 'Use simpler words to improve readability.',
818 'action' => 'Replace complex words with simpler alternatives'
819 ];
820 }
821
822 if ($readability['flesch_kincaid_grade'] > 12) {
823 $recommendations[] = [
824 'type' => 'grade_level',
825 'priority' => 'high',
826 'message' => 'Content requires college-level reading skills. Simplify for broader audience.',
827 'action' => 'Target 8th-10th grade reading level'
828 ];
829 }
830
831 return $recommendations;
832 }
833
834 /**
835 * Find keyword positions in content
836 *
837 * @since 1.0.0
838 *
839 * @param string $content Content to search
840 * @param string $keyword Keyword to find
841 * @return array Keyword positions
842 */
843 private function find_keyword_positions(string $content, string $keyword): array {
844 $positions = [];
845 $offset = 0;
846
847 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- the canonical strpos() scan loop; hoisting it needs a duplicated call.
848 while (($pos = strpos($content, $keyword, $offset)) !== false) {
849 $positions[] = $pos;
850 $offset = $pos + 1;
851 }
852
853 return $positions;
854 }
855
856 /**
857 * Calculate keyword distribution score
858 *
859 * @since 1.0.0
860 *
861 * @param array $positions Keyword positions
862 * @param int $word_count Total word count
863 * @return float Distribution score (0-100)
864 */
865 private function calculate_keyword_distribution(array $positions, int $word_count): float {
866 if (empty($positions) || $word_count === 0) {
867 return 0;
868 }
869
870 // Divide content into sections and check keyword presence
871 $sections = 4;
872 $section_size = $word_count / $sections;
873 $sections_with_keyword = 0;
874
875 for ($i = 0; $i < $sections; $i++) {
876 $section_start = $i * $section_size;
877 $section_end = ($i + 1) * $section_size;
878
879 foreach ($positions as $pos) {
880 if ($pos >= $section_start && $pos < $section_end) {
881 $sections_with_keyword++;
882 break;
883 }
884 }
885 }
886
887 return ($sections_with_keyword / $sections) * 100;
888 }
889
890 /**
891 * Determine keyword optimization status
892 *
893 * @since 1.0.0
894 *
895 * @param float $density Keyword density percentage
896 * @return string Optimization status
897 */
898 private function determine_keyword_optimization_status(float $density): string {
899 $config = $this->analysis_algorithms['keyword_analysis']['density_calculation'];
900
901 if ($density < $config['minimum_threshold']) {
902 return 'under_optimized';
903 } elseif ($density > $config['warning_threshold']) {
904 return 'over_optimized';
905 } elseif ($density >= $config['optimal_range'][0] && $density <= $config['optimal_range'][1]) {
906 return 'optimal';
907 } else {
908 return 'good';
909 }
910 }
911
912 /**
913 * Extract content for analysis
914 *
915 * @since 1.0.0
916 *
917 * @param string $context_type Context type
918 * @param int|null $context_id Context ID
919 * @return string Content to analyze
920 */
921 private function extract_content_for_analysis(string $context_type, ?int $context_id): string {
922 $content = '';
923
924 switch ($context_type) {
925 case 'post':
926 case 'page':
927 case 'product':
928 if ($context_id) {
929 $post = get_post($context_id);
930 if ($post) {
931 $content = $post->post_title . ' ' . $post->post_content;
932 }
933 }
934 break;
935 case 'site':
936 // Get homepage content
937 $homepage_id = get_option('page_on_front');
938 if ($homepage_id && $homepage_id !== '0') {
939 $homepage = get_post($homepage_id);
940 if ($homepage) {
941 $content = $homepage->post_title . ' ' . $homepage->post_content;
942 }
943 } else {
944 // Get recent posts for blog homepage
945 $recent_posts = get_posts(['numberposts' => 3]);
946 $content_parts = [];
947 foreach ($recent_posts as $post) {
948 $content_parts[] = $post->post_title . ' ' . \ThinkRank\Core\Seo_Text::trim_words($post->post_content, 100, '...', 1000);
949 }
950 $content = implode(' ', $content_parts);
951 }
952 break;
953 }
954
955 return $content;
956 }
957
958 /**
959 * Extract target keywords for analysis
960 *
961 * @since 1.0.0
962 *
963 * @param string $context_type Context type
964 * @param int|null $context_id Context ID
965 * @return array Target keywords
966 */
967 private function extract_target_keywords(string $context_type, ?int $context_id): array {
968 // This would typically get keywords from the database
969 // For now, return some default keywords based on content
970 $keywords = [];
971
972 $content = $this->extract_content_for_analysis($context_type, $context_id);
973 if (!empty($content)) {
974 // Simple keyword extraction from title and content
975 $words = str_word_count(strtolower(wp_strip_all_tags($content)), 1);
976 $word_counts = array_count_values($words);
977
978 // Remove common stop words
979 $stop_words = ['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
980 foreach ($stop_words as $stop_word) {
981 unset($word_counts[$stop_word]);
982 }
983
984 arsort($word_counts);
985 $keywords = array_slice(array_keys($word_counts), 0, 5);
986 }
987
988 return $keywords;
989 }
990
991 /**
992 * Store analysis results in database
993 *
994 * @since 1.0.0
995 *
996 * @param string $context_type Context type
997 * @param int|null $context_id Context ID
998 * @param array $results Analysis results
999 * @return bool Success status
1000 */
1001 private function store_analysis_results(string $context_type, ?int $context_id, array $results): bool {
1002 global $wpdb;
1003
1004 $table_name = $wpdb->prefix . 'thinkrank_seo_analysis';
1005
1006 $data = [
1007 'context_type' => $context_type,
1008 'context_id' => $context_id,
1009 'analysis_type' => 'ai_content_analysis',
1010 'analysis_data' => wp_json_encode($results),
1011 'score' => $results['optimization_score'] ?? 0,
1012 'status' => 'completed',
1013 'ai_confidence' => $results['ai_confidence'] ?? 0,
1014 'recommendations' => wp_json_encode($results['recommendations'] ?? []),
1015 'analyzed_by' => get_current_user_id()
1016 ];
1017
1018 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Analysis results storage requires direct database access
1019 $result = $wpdb->insert($table_name, $data);
1020
1021 return $result !== false;
1022 }
1023
1024 /**
1025 * Calculate optimization score
1026 *
1027 * @since 1.0.0
1028 *
1029 * @param array $analysis Complete analysis results
1030 * @return int Optimization score (0-100)
1031 */
1032 private function calculate_optimization_score(array $analysis): int {
1033 $scores = [];
1034
1035 // Readability score (25% weight)
1036 if (!empty($analysis['readability']['readability_score'])) {
1037 $scores['readability'] = $analysis['readability']['readability_score'] * ($this->scoring_weights['readability'] / 100);
1038 }
1039
1040 // Keyword optimization score (30% weight)
1041 if (!empty($analysis['keyword_analysis']['optimization_score'])) {
1042 $scores['keyword_optimization'] = $analysis['keyword_analysis']['optimization_score'] * ($this->scoring_weights['keyword_optimization'] / 100);
1043 }
1044
1045 // Content structure score (20% weight)
1046 if (!empty($analysis['structure_analysis']['structure_score'])) {
1047 $scores['content_structure'] = $analysis['structure_analysis']['structure_score'] * ($this->scoring_weights['content_structure'] / 100);
1048 }
1049
1050 // Semantic relevance score (15% weight)
1051 if (!empty($analysis['semantic_analysis']['relevance_score'])) {
1052 $scores['semantic_relevance'] = $analysis['semantic_analysis']['relevance_score'] * ($this->scoring_weights['semantic_relevance'] / 100);
1053 }
1054
1055 // Technical SEO score (10% weight)
1056 $technical_score = $this->calculate_technical_seo_score($analysis);
1057 $scores['technical_seo'] = $technical_score * ($this->scoring_weights['technical_seo'] / 100);
1058
1059 return (int) round(array_sum($scores));
1060 }
1061
1062 /**
1063 * Generate optimization recommendations
1064 *
1065 * @since 1.0.0
1066 *
1067 * @param array $analysis Complete analysis results
1068 * @return array Optimization recommendations
1069 */
1070 private function generate_optimization_recommendations(array $analysis): array {
1071 $recommendations = [];
1072
1073 // Readability recommendations
1074 if (!empty($analysis['readability']['recommendations'])) {
1075 $recommendations = array_merge($recommendations, $analysis['readability']['recommendations']);
1076 }
1077
1078 // Keyword recommendations
1079 if (!empty($analysis['keyword_analysis']['recommendations'])) {
1080 $recommendations = array_merge($recommendations, $analysis['keyword_analysis']['recommendations']);
1081 }
1082
1083 // Structure recommendations
1084 if (!empty($analysis['structure_analysis']['recommendations'])) {
1085 $recommendations = array_merge($recommendations, $analysis['structure_analysis']['recommendations']);
1086 }
1087
1088 // Content length recommendations
1089 $word_count = $analysis['content_stats']['word_count'] ?? 0;
1090 if ($word_count < 300) {
1091 $recommendations[] = [
1092 'type' => 'content_length',
1093 'priority' => 'high',
1094 'message' => 'Content is too short for optimal SEO performance.',
1095 'action' => 'Expand content to at least 300 words'
1096 ];
1097 }
1098
1099 // Sort recommendations by priority
1100 usort($recommendations, function ($a, $b) {
1101 $priority_order = ['high' => 3, 'medium' => 2, 'low' => 1];
1102 return ($priority_order[$b['priority']] ?? 0) - ($priority_order[$a['priority']] ?? 0);
1103 });
1104
1105 return $recommendations;
1106 }
1107
1108 /**
1109 * Calculate AI confidence score
1110 *
1111 * @since 1.0.0
1112 *
1113 * @param array $analysis Analysis results
1114 * @return float Confidence score (0-1)
1115 */
1116 private function calculate_ai_confidence(array $analysis): float {
1117 $confidence_factors = [];
1118
1119 // Content length confidence
1120 $word_count = $analysis['content_stats']['word_count'] ?? 0;
1121 if ($word_count > 0) {
1122 $confidence_factors[] = min(1.0, $word_count / 500); // Full confidence at 500+ words
1123 }
1124
1125 // Analysis completeness confidence
1126 $completed_analyses = 0;
1127 $total_analyses = 4; // readability, keyword, semantic, structure
1128
1129 if (!empty($analysis['readability'])) {
1130 $completed_analyses++;
1131 }
1132 if (!empty($analysis['keyword_analysis'])) {
1133 $completed_analyses++;
1134 }
1135 if (!empty($analysis['semantic_analysis'])) {
1136 $completed_analyses++;
1137 }
1138 if (!empty($analysis['structure_analysis'])) {
1139 $completed_analyses++;
1140 }
1141
1142 $confidence_factors[] = $completed_analyses / $total_analyses;
1143
1144 return empty($confidence_factors) ? 0.0 : array_sum($confidence_factors) / count($confidence_factors);
1145 }
1146
1147 /**
1148 * Calculate validation score
1149 *
1150 * @since 1.0.0
1151 *
1152 * @param array $validation Validation results
1153 * @return int Score (0-100)
1154 */
1155 private function calculate_validation_score(array $validation): int {
1156 $score = 100;
1157 $score -= count($validation['errors']) * 20;
1158 $score -= count($validation['warnings']) * 10;
1159 $score -= count($validation['suggestions']) * 5;
1160
1161 return max(0, $score);
1162 }
1163
1164 /**
1165 * Calculate technical SEO score
1166 *
1167 * @since 1.0.0
1168 *
1169 * @param array $analysis Analysis results
1170 * @return int Technical SEO score (0-100)
1171 */
1172 private function calculate_technical_seo_score(array $analysis): int {
1173 $score = 100;
1174
1175 // Deduct points for missing elements
1176 if (empty($analysis['structure_analysis']['heading_structure'])) {
1177 $score -= 20;
1178 }
1179
1180 if (empty($analysis['structure_analysis']['image_analysis'])) {
1181 $score -= 15;
1182 }
1183
1184 if (empty($analysis['structure_analysis']['link_analysis'])) {
1185 $score -= 15;
1186 }
1187
1188 return max(0, $score);
1189 }
1190
1191 /**
1192 * Simple implementations for semantic analysis methods
1193 * These would be enhanced with actual AI/ML libraries in production
1194 */
1195
1196 private function extract_topic_clusters(string $content): array {
1197 // Simplified topic extraction
1198 return ['topics' => ['general content'], 'confidence' => 0.7];
1199 }
1200
1201 private function recognize_entities(string $content): array {
1202 // Simplified entity recognition
1203 return ['entities' => [], 'confidence' => 0.6];
1204 }
1205
1206 private function analyze_sentiment(string $content): array {
1207 // Simplified sentiment analysis
1208 return ['sentiment' => 'neutral', 'score' => 0.0, 'confidence' => 0.7];
1209 }
1210
1211 private function classify_content(string $content): array {
1212 // Simplified content classification
1213 return ['category' => 'informational', 'confidence' => 0.8];
1214 }
1215
1216 private function extract_semantic_keywords(string $content): array {
1217 // Simplified semantic keyword extraction
1218 return [];
1219 }
1220
1221 private function calculate_content_coherence(array $semantic_analysis): float {
1222 return 0.8; // Simplified coherence score
1223 }
1224
1225 private function calculate_semantic_relevance(array $semantic_analysis): float {
1226 return 75.0; // Simplified relevance score
1227 }
1228
1229 private function analyze_heading_structure(string $content): array {
1230 // Extract headings from HTML content
1231 $headings = [];
1232 preg_match_all('/<h([1-6])[^>]*>(.*?)<\/h[1-6]>/i', $content, $matches, PREG_SET_ORDER);
1233
1234 foreach ($matches as $match) {
1235 $headings[] = [
1236 'level' => (int) $match[1],
1237 'text' => wp_strip_all_tags($match[2]),
1238 'length' => strlen(wp_strip_all_tags($match[2]))
1239 ];
1240 }
1241
1242 return ['headings' => $headings, 'count' => count($headings)];
1243 }
1244
1245 private function analyze_paragraph_structure(string $content): array {
1246 $paragraphs = preg_split('/\n\s*\n/', wp_strip_all_tags($content), -1, PREG_SPLIT_NO_EMPTY);
1247 $analysis = ['count' => count($paragraphs), 'average_length' => 0];
1248
1249 if (!empty($paragraphs)) {
1250 $total_words = 0;
1251 foreach ($paragraphs as $paragraph) {
1252 $total_words += str_word_count($paragraph);
1253 }
1254 $analysis['average_length'] = $total_words / count($paragraphs);
1255 }
1256
1257 return $analysis;
1258 }
1259
1260 private function analyze_list_usage(string $content): array {
1261 $ul_count = preg_match_all('/<ul[^>]*>/i', $content);
1262 $ol_count = preg_match_all('/<ol[^>]*>/i', $content);
1263
1264 return ['unordered_lists' => $ul_count, 'ordered_lists' => $ol_count];
1265 }
1266
1267 private function analyze_image_usage(string $content): array {
1268 preg_match_all('/<img[^>]+>/i', $content, $img_tags);
1269 $images = [];
1270
1271 foreach ($img_tags[0] as $img_tag) {
1272 preg_match('/alt=["\']([^"\']*)["\']/', $img_tag, $alt_match);
1273 $images[] = ['has_alt' => !empty($alt_match[1])];
1274 }
1275
1276 return ['count' => count($images), 'images' => $images];
1277 }
1278
1279 private function analyze_link_structure(string $content): array {
1280 preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]*)<\/a>/i', $content, $link_matches, PREG_SET_ORDER);
1281
1282 $internal_links = 0;
1283 $external_links = 0;
1284
1285 foreach ($link_matches as $match) {
1286 $url = $match[1];
1287 if (strpos($url, home_url()) === 0 || strpos($url, '/') === 0) {
1288 $internal_links++;
1289 } else {
1290 $external_links++;
1291 }
1292 }
1293
1294 return ['internal' => $internal_links, 'external' => $external_links];
1295 }
1296
1297 private function analyze_content_flow(string $content): array {
1298 // Simplified content flow analysis
1299 return ['flow_score' => 80, 'transitions' => 5];
1300 }
1301
1302 private function calculate_structure_score(array $structure_analysis): int {
1303 $score = 100;
1304
1305 // Deduct points for poor structure
1306 if (empty($structure_analysis['heading_structure']['headings'])) {
1307 $score -= 30;
1308 }
1309
1310 if ($structure_analysis['paragraph_analysis']['average_length'] > 150) {
1311 $score -= 20;
1312 }
1313
1314 if ($structure_analysis['image_analysis']['count'] === 0) {
1315 $score -= 15;
1316 }
1317
1318 return max(0, $score);
1319 }
1320
1321 private function generate_structure_recommendations(array $structure_analysis): array {
1322 $recommendations = [];
1323
1324 if (empty($structure_analysis['heading_structure']['headings'])) {
1325 $recommendations[] = [
1326 'type' => 'headings',
1327 'priority' => 'high',
1328 'message' => 'Add heading tags (H1, H2, H3) to improve content structure',
1329 'action' => 'Use proper heading hierarchy'
1330 ];
1331 }
1332
1333 return $recommendations;
1334 }
1335
1336 private function find_semantic_keywords(string $content, array $keywords): array {
1337 // Simplified semantic keyword finding
1338 return [];
1339 }
1340
1341 private function calculate_keyword_optimization_score(array $keyword_analysis): int {
1342 $score = 0;
1343 $keyword_count = count($keyword_analysis['primary_keywords']) + count($keyword_analysis['secondary_keywords']);
1344
1345 if ($keyword_count > 0) {
1346 $optimal_count = 0;
1347 foreach ($keyword_analysis['primary_keywords'] as $keyword) {
1348 if ($keyword['optimization_status'] === 'optimal') {
1349 $optimal_count++;
1350 }
1351 }
1352 $score = ($optimal_count / $keyword_count) * 100;
1353 }
1354
1355 return (int) $score;
1356 }
1357
1358 private function generate_keyword_recommendations(array $keyword_analysis): array {
1359 $recommendations = [];
1360
1361 foreach ($keyword_analysis['primary_keywords'] as $keyword) {
1362 if ($keyword['optimization_status'] === 'under_optimized') {
1363 $recommendations[] = [
1364 'type' => 'keyword_density',
1365 'priority' => 'medium',
1366 'message' => "Keyword '{$keyword['keyword']}' appears too few times",
1367 'action' => 'Increase keyword usage naturally'
1368 ];
1369 } elseif ($keyword['optimization_status'] === 'over_optimized') {
1370 $recommendations[] = [
1371 'type' => 'keyword_density',
1372 'priority' => 'high',
1373 'message' => "Keyword '{$keyword['keyword']}' may be over-optimized",
1374 'action' => 'Reduce keyword density to avoid keyword stuffing'
1375 ];
1376 }
1377 }
1378
1379 return $recommendations;
1380 }
1381 }
1382