PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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 1.28.0, at includes/seo/class-ai-content-analyzer.php

1,381 lines 48.6 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 while (($pos = strpos($content, $keyword, $offset)) !== false) {
848 $positions[] = $pos;
849 $offset = $pos + 1;
850 }
851
852 return $positions;
853 }
854
855 /**
856 * Calculate keyword distribution score
857 *
858 * @since 1.0.0
859 *
860 * @param array $positions Keyword positions
861 * @param int $word_count Total word count
862 * @return float Distribution score (0-100)
863 */
864 private function calculate_keyword_distribution(array $positions, int $word_count): float {
865 if (empty($positions) || $word_count === 0) {
866 return 0;
867 }
868
869 // Divide content into sections and check keyword presence
870 $sections = 4;
871 $section_size = $word_count / $sections;
872 $sections_with_keyword = 0;
873
874 for ($i = 0; $i < $sections; $i++) {
875 $section_start = $i * $section_size;
876 $section_end = ($i + 1) * $section_size;
877
878 foreach ($positions as $pos) {
879 if ($pos >= $section_start && $pos < $section_end) {
880 $sections_with_keyword++;
881 break;
882 }
883 }
884 }
885
886 return ($sections_with_keyword / $sections) * 100;
887 }
888
889 /**
890 * Determine keyword optimization status
891 *
892 * @since 1.0.0
893 *
894 * @param float $density Keyword density percentage
895 * @return string Optimization status
896 */
897 private function determine_keyword_optimization_status(float $density): string {
898 $config = $this->analysis_algorithms['keyword_analysis']['density_calculation'];
899
900 if ($density < $config['minimum_threshold']) {
901 return 'under_optimized';
902 } elseif ($density > $config['warning_threshold']) {
903 return 'over_optimized';
904 } elseif ($density >= $config['optimal_range'][0] && $density <= $config['optimal_range'][1]) {
905 return 'optimal';
906 } else {
907 return 'good';
908 }
909 }
910
911 /**
912 * Extract content for analysis
913 *
914 * @since 1.0.0
915 *
916 * @param string $context_type Context type
917 * @param int|null $context_id Context ID
918 * @return string Content to analyze
919 */
920 private function extract_content_for_analysis(string $context_type, ?int $context_id): string {
921 $content = '';
922
923 switch ($context_type) {
924 case 'post':
925 case 'page':
926 case 'product':
927 if ($context_id) {
928 $post = get_post($context_id);
929 if ($post) {
930 $content = $post->post_title . ' ' . $post->post_content;
931 }
932 }
933 break;
934 case 'site':
935 // Get homepage content
936 $homepage_id = get_option('page_on_front');
937 if ($homepage_id && $homepage_id !== '0') {
938 $homepage = get_post($homepage_id);
939 if ($homepage) {
940 $content = $homepage->post_title . ' ' . $homepage->post_content;
941 }
942 } else {
943 // Get recent posts for blog homepage
944 $recent_posts = get_posts(['numberposts' => 3]);
945 $content_parts = [];
946 foreach ($recent_posts as $post) {
947 $content_parts[] = $post->post_title . ' ' . wp_trim_words($post->post_content, 100);
948 }
949 $content = implode(' ', $content_parts);
950 }
951 break;
952 }
953
954 return $content;
955 }
956
957 /**
958 * Extract target keywords for analysis
959 *
960 * @since 1.0.0
961 *
962 * @param string $context_type Context type
963 * @param int|null $context_id Context ID
964 * @return array Target keywords
965 */
966 private function extract_target_keywords(string $context_type, ?int $context_id): array {
967 // This would typically get keywords from the database
968 // For now, return some default keywords based on content
969 $keywords = [];
970
971 $content = $this->extract_content_for_analysis($context_type, $context_id);
972 if (!empty($content)) {
973 // Simple keyword extraction from title and content
974 $words = str_word_count(strtolower(wp_strip_all_tags($content)), 1);
975 $word_counts = array_count_values($words);
976
977 // Remove common stop words
978 $stop_words = ['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
979 foreach ($stop_words as $stop_word) {
980 unset($word_counts[$stop_word]);
981 }
982
983 arsort($word_counts);
984 $keywords = array_slice(array_keys($word_counts), 0, 5);
985 }
986
987 return $keywords;
988 }
989
990 /**
991 * Store analysis results in database
992 *
993 * @since 1.0.0
994 *
995 * @param string $context_type Context type
996 * @param int|null $context_id Context ID
997 * @param array $results Analysis results
998 * @return bool Success status
999 */
1000 private function store_analysis_results(string $context_type, ?int $context_id, array $results): bool {
1001 global $wpdb;
1002
1003 $table_name = $wpdb->prefix . 'thinkrank_seo_analysis';
1004
1005 $data = [
1006 'context_type' => $context_type,
1007 'context_id' => $context_id,
1008 'analysis_type' => 'ai_content_analysis',
1009 'analysis_data' => wp_json_encode($results),
1010 'score' => $results['optimization_score'] ?? 0,
1011 'status' => 'completed',
1012 'ai_confidence' => $results['ai_confidence'] ?? 0,
1013 'recommendations' => wp_json_encode($results['recommendations'] ?? []),
1014 'analyzed_by' => get_current_user_id()
1015 ];
1016
1017 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Analysis results storage requires direct database access
1018 $result = $wpdb->insert($table_name, $data);
1019
1020 return $result !== false;
1021 }
1022
1023 /**
1024 * Calculate optimization score
1025 *
1026 * @since 1.0.0
1027 *
1028 * @param array $analysis Complete analysis results
1029 * @return int Optimization score (0-100)
1030 */
1031 private function calculate_optimization_score(array $analysis): int {
1032 $scores = [];
1033
1034 // Readability score (25% weight)
1035 if (!empty($analysis['readability']['readability_score'])) {
1036 $scores['readability'] = $analysis['readability']['readability_score'] * ($this->scoring_weights['readability'] / 100);
1037 }
1038
1039 // Keyword optimization score (30% weight)
1040 if (!empty($analysis['keyword_analysis']['optimization_score'])) {
1041 $scores['keyword_optimization'] = $analysis['keyword_analysis']['optimization_score'] * ($this->scoring_weights['keyword_optimization'] / 100);
1042 }
1043
1044 // Content structure score (20% weight)
1045 if (!empty($analysis['structure_analysis']['structure_score'])) {
1046 $scores['content_structure'] = $analysis['structure_analysis']['structure_score'] * ($this->scoring_weights['content_structure'] / 100);
1047 }
1048
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)
1055 $technical_score = $this->calculate_technical_seo_score($analysis);
1056 $scores['technical_seo'] = $technical_score * ($this->scoring_weights['technical_seo'] / 100);
1057
1058 return (int) round(array_sum($scores));
1059 }
1060
1061 /**
1062 * Generate optimization recommendations
1063 *
1064 * @since 1.0.0
1065 *
1066 * @param array $analysis Complete analysis results
1067 * @return array Optimization recommendations
1068 */
1069 private function generate_optimization_recommendations(array $analysis): array {
1070 $recommendations = [];
1071
1072 // Readability recommendations
1073 if (!empty($analysis['readability']['recommendations'])) {
1074 $recommendations = array_merge($recommendations, $analysis['readability']['recommendations']);
1075 }
1076
1077 // Keyword recommendations
1078 if (!empty($analysis['keyword_analysis']['recommendations'])) {
1079 $recommendations = array_merge($recommendations, $analysis['keyword_analysis']['recommendations']);
1080 }
1081
1082 // Structure recommendations
1083 if (!empty($analysis['structure_analysis']['recommendations'])) {
1084 $recommendations = array_merge($recommendations, $analysis['structure_analysis']['recommendations']);
1085 }
1086
1087 // Content length recommendations
1088 $word_count = $analysis['content_stats']['word_count'] ?? 0;
1089 if ($word_count < 300) {
1090 $recommendations[] = [
1091 'type' => 'content_length',
1092 'priority' => 'high',
1093 'message' => 'Content is too short for optimal SEO performance.',
1094 'action' => 'Expand content to at least 300 words'
1095 ];
1096 }
1097
1098 // Sort recommendations by priority
1099 usort($recommendations, function ($a, $b) {
1100 $priority_order = ['high' => 3, 'medium' => 2, 'low' => 1];
1101 return ($priority_order[$b['priority']] ?? 0) - ($priority_order[$a['priority']] ?? 0);
1102 });
1103
1104 return $recommendations;
1105 }
1106
1107 /**
1108 * Calculate AI confidence score
1109 *
1110 * @since 1.0.0
1111 *
1112 * @param array $analysis Analysis results
1113 * @return float Confidence score (0-1)
1114 */
1115 private function calculate_ai_confidence(array $analysis): float {
1116 $confidence_factors = [];
1117
1118 // Content length confidence
1119 $word_count = $analysis['content_stats']['word_count'] ?? 0;
1120 if ($word_count > 0) {
1121 $confidence_factors[] = min(1.0, $word_count / 500); // Full confidence at 500+ words
1122 }
1123
1124 // Analysis completeness confidence
1125 $completed_analyses = 0;
1126 $total_analyses = 4; // readability, keyword, semantic, structure
1127
1128 if (!empty($analysis['readability'])) {
1129 $completed_analyses++;
1130 }
1131 if (!empty($analysis['keyword_analysis'])) {
1132 $completed_analyses++;
1133 }
1134 if (!empty($analysis['semantic_analysis'])) {
1135 $completed_analyses++;
1136 }
1137 if (!empty($analysis['structure_analysis'])) {
1138 $completed_analyses++;
1139 }
1140
1141 $confidence_factors[] = $completed_analyses / $total_analyses;
1142
1143 return empty($confidence_factors) ? 0.0 : array_sum($confidence_factors) / count($confidence_factors);
1144 }
1145
1146 /**
1147 * Calculate validation score
1148 *
1149 * @since 1.0.0
1150 *
1151 * @param array $validation Validation results
1152 * @return int Score (0-100)
1153 */
1154 private function calculate_validation_score(array $validation): int {
1155 $score = 100;
1156 $score -= count($validation['errors']) * 20;
1157 $score -= count($validation['warnings']) * 10;
1158 $score -= count($validation['suggestions']) * 5;
1159
1160 return max(0, $score);
1161 }
1162
1163 /**
1164 * Calculate technical SEO score
1165 *
1166 * @since 1.0.0
1167 *
1168 * @param array $analysis Analysis results
1169 * @return int Technical SEO score (0-100)
1170 */
1171 private function calculate_technical_seo_score(array $analysis): int {
1172 $score = 100;
1173
1174 // Deduct points for missing elements
1175 if (empty($analysis['structure_analysis']['heading_structure'])) {
1176 $score -= 20;
1177 }
1178
1179 if (empty($analysis['structure_analysis']['image_analysis'])) {
1180 $score -= 15;
1181 }
1182
1183 if (empty($analysis['structure_analysis']['link_analysis'])) {
1184 $score -= 15;
1185 }
1186
1187 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 }
1227
1228 private function analyze_heading_structure(string $content): array {
1229 // Extract headings from HTML content
1230 $headings = [];
1231 preg_match_all('/<h([1-6])[^>]*>(.*?)<\/h[1-6]>/i', $content, $matches, PREG_SET_ORDER);
1232
1233 foreach ($matches as $match) {
1234 $headings[] = [
1235 'level' => (int) $match[1],
1236 'text' => wp_strip_all_tags($match[2]),
1237 'length' => strlen(wp_strip_all_tags($match[2]))
1238 ];
1239 }
1240
1241 return ['headings' => $headings, 'count' => count($headings)];
1242 }
1243
1244 private function analyze_paragraph_structure(string $content): array {
1245 $paragraphs = preg_split('/\n\s*\n/', wp_strip_all_tags($content), -1, PREG_SPLIT_NO_EMPTY);
1246 $analysis = ['count' => count($paragraphs), 'average_length' => 0];
1247
1248 if (!empty($paragraphs)) {
1249 $total_words = 0;
1250 foreach ($paragraphs as $paragraph) {
1251 $total_words += str_word_count($paragraph);
1252 }
1253 $analysis['average_length'] = $total_words / count($paragraphs);
1254 }
1255
1256 return $analysis;
1257 }
1258
1259 private function analyze_list_usage(string $content): array {
1260 $ul_count = preg_match_all('/<ul[^>]*>/i', $content);
1261 $ol_count = preg_match_all('/<ol[^>]*>/i', $content);
1262
1263 return ['unordered_lists' => $ul_count, 'ordered_lists' => $ol_count];
1264 }
1265
1266 private function analyze_image_usage(string $content): array {
1267 preg_match_all('/<img[^>]+>/i', $content, $img_tags);
1268 $images = [];
1269
1270 foreach ($img_tags[0] as $img_tag) {
1271 preg_match('/alt=["\']([^"\']*)["\']/', $img_tag, $alt_match);
1272 $images[] = ['has_alt' => !empty($alt_match[1])];
1273 }
1274
1275 return ['count' => count($images), 'images' => $images];
1276 }
1277
1278 private function analyze_link_structure(string $content): array {
1279 preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]*)<\/a>/i', $content, $link_matches, PREG_SET_ORDER);
1280
1281 $internal_links = 0;
1282 $external_links = 0;
1283
1284 foreach ($link_matches as $match) {
1285 $url = $match[1];
1286 if (strpos($url, home_url()) === 0 || strpos($url, '/') === 0) {
1287 $internal_links++;
1288 } else {
1289 $external_links++;
1290 }
1291 }
1292
1293 return ['internal' => $internal_links, 'external' => $external_links];
1294 }
1295
1296 private function analyze_content_flow(string $content): array {
1297 // Simplified content flow analysis
1298 return ['flow_score' => 80, 'transitions' => 5];
1299 }
1300
1301 private function calculate_structure_score(array $structure_analysis): int {
1302 $score = 100;
1303
1304 // Deduct points for poor structure
1305 if (empty($structure_analysis['heading_structure']['headings'])) {
1306 $score -= 30;
1307 }
1308
1309 if ($structure_analysis['paragraph_analysis']['average_length'] > 150) {
1310 $score -= 20;
1311 }
1312
1313 if ($structure_analysis['image_analysis']['count'] === 0) {
1314 $score -= 15;
1315 }
1316
1317 return max(0, $score);
1318 }
1319
1320 private function generate_structure_recommendations(array $structure_analysis): array {
1321 $recommendations = [];
1322
1323 if (empty($structure_analysis['heading_structure']['headings'])) {
1324 $recommendations[] = [
1325 'type' => 'headings',
1326 'priority' => 'high',
1327 'message' => 'Add heading tags (H1, H2, H3) to improve content structure',
1328 'action' => 'Use proper heading hierarchy'
1329 ];
1330 }
1331
1332 return $recommendations;
1333 }
1334
1335 private function find_semantic_keywords(string $content, array $keywords): array {
1336 // Simplified semantic keyword finding
1337 return [];
1338 }
1339
1340 private function calculate_keyword_optimization_score(array $keyword_analysis): int {
1341 $score = 0;
1342 $keyword_count = count($keyword_analysis['primary_keywords']) + count($keyword_analysis['secondary_keywords']);
1343
1344 if ($keyword_count > 0) {
1345 $optimal_count = 0;
1346 foreach ($keyword_analysis['primary_keywords'] as $keyword) {
1347 if ($keyword['optimization_status'] === 'optimal') {
1348 $optimal_count++;
1349 }
1350 }
1351 $score = ($optimal_count / $keyword_count) * 100;
1352 }
1353
1354 return (int) $score;
1355 }
1356
1357 private function generate_keyword_recommendations(array $keyword_analysis): array {
1358 $recommendations = [];
1359
1360 foreach ($keyword_analysis['primary_keywords'] as $keyword) {
1361 if ($keyword['optimization_status'] === 'under_optimized') {
1362 $recommendations[] = [
1363 'type' => 'keyword_density',
1364 'priority' => 'medium',
1365 'message' => "Keyword '{$keyword['keyword']}' appears too few times",
1366 'action' => 'Increase keyword usage naturally'
1367 ];
1368 } elseif ($keyword['optimization_status'] === 'over_optimized') {
1369 $recommendations[] = [
1370 'type' => 'keyword_density',
1371 'priority' => 'high',
1372 'message' => "Keyword '{$keyword['keyword']}' may be over-optimized",
1373 'action' => 'Reduce keyword density to avoid keyword stuffing'
1374 ];
1375 }
1376 }
1377
1378 return $recommendations;
1379 }
1380 }
1381