PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.1.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 / ai / class-metadata-generator.php

class-metadata-generator.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.1.0, at includes/ai/class-metadata-generator.php

430 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Metadata Generator
4 *
5 * Core functionality for generating SEO metadata using AI
6 *
7 * @package ThinkRank\AI
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\AI;
14
15 use ThinkRank\Core\Settings;
16
17 // Prevent direct access
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * Metadata Generator Class
24 *
25 * Single Responsibility: Generate SEO metadata using AI
26 *
27 * @since 1.0.0
28 */
29 class Metadata_Generator {
30
31 /**
32 * AI Manager instance
33 *
34 * @var Manager
35 */
36 private Manager $ai_manager;
37
38 /**
39 * Settings instance
40 *
41 * @var Settings
42 */
43 private Settings $settings;
44
45 /**
46 * Constructor
47 *
48 * @param Manager|null $ai_manager AI manager instance
49 * @param Settings|null $settings Settings instance
50 */
51 public function __construct(?Manager $ai_manager = null, ?Settings $settings = null) {
52 $this->ai_manager = $ai_manager ?? new Manager();
53 $this->settings = $settings ?? new Settings();
54 }
55
56 /**
57 * Generate metadata for post content
58 *
59 * @param string $content Post content
60 * @param array $options Generation options
61 * @return array Generated metadata
62 * @throws \Exception If generation fails
63 */
64 public function generate_for_content(string $content, array $options = []): array {
65 // Validate content
66 if (empty(trim($content))) {
67 throw new \Exception('Content cannot be empty');
68 }
69
70 // Prepare content for AI processing
71 $processed_content = $this->prepare_content($content);
72
73 // Set default options
74 $default_options = [
75 'target_keyword' => '',
76 'content_type' => 'blog_post',
77 'tone' => 'professional',
78 'max_title_length' => $this->settings->get('meta_title_length', 60),
79 'max_description_length' => $this->settings->get('meta_description_length', 160),
80 ];
81
82 $options = array_merge($default_options, $options);
83
84 // Generate metadata using AI
85 $metadata = $this->ai_manager->generate_seo_metadata($processed_content, $options);
86
87 // Post-process and validate metadata
88 return $this->validate_and_enhance_metadata($metadata, $options);
89 }
90
91 /**
92 * Generate metadata for WordPress post
93 *
94 * @param int $post_id Post ID
95 * @param array $options Generation options
96 * @return array Generated metadata
97 * @throws \Exception If generation fails
98 */
99 public function generate_for_post(int $post_id, array $options = []): array {
100 $post = get_post($post_id);
101
102 if (!$post) {
103 throw new \Exception('Post not found');
104 }
105
106 // Extract content from post
107 $content = $this->extract_post_content($post);
108
109 // Add post-specific options
110 $options = array_merge($options, [
111 'content_type' => $this->determine_content_type($post),
112 'post_id' => $post_id,
113 ]);
114
115 // Generate metadata
116 $metadata = $this->generate_for_content($content, $options);
117
118 // Save metadata to post meta
119 $this->save_post_metadata($post_id, $metadata);
120
121 return $metadata;
122 }
123
124 /**
125 * Generate multiple variations of metadata
126 *
127 * @param string $content Content to analyze
128 * @param array $options Generation options
129 * @param int $variations Number of variations to generate
130 * @return array Array of metadata variations
131 */
132 public function generate_variations(string $content, array $options = [], int $variations = 3): array {
133 $results = [];
134
135 for ($i = 0; $i < $variations; $i++) {
136 // Slightly vary the temperature for different results
137 $variation_options = array_merge($options, [
138 'temperature' => 0.3 + ($i * 0.2), // 0.3, 0.5, 0.7
139 'variation' => $i + 1,
140 ]);
141
142 try {
143 $metadata = $this->generate_for_content($content, $variation_options);
144 $metadata['variation_id'] = $i + 1;
145 $results[] = $metadata;
146
147 // Small delay to avoid rate limiting
148 if ($i < $variations - 1) {
149 sleep(1);
150 }
151
152 } catch (\Exception $e) {
153 // Skip failed variations silently
154 }
155 }
156
157 return $results;
158 }
159
160 /**
161 * Analyze content and suggest improvements
162 *
163 * @param string $content Content to analyze
164 * @param array $current_metadata Current metadata
165 * @return array Analysis and suggestions
166 */
167 public function analyze_content(string $content, array $current_metadata = []): array {
168 $analysis = [
169 'content_length' => str_word_count($content),
170 'readability_score' => $this->calculate_readability_score($content),
171 'keyword_density' => $this->analyze_keyword_density($content),
172 'suggestions' => [],
173 ];
174
175 // Analyze current metadata if provided
176 if (!empty($current_metadata)) {
177 $analysis['metadata_analysis'] = $this->analyze_metadata($current_metadata);
178 }
179
180 // Generate improvement suggestions
181 $analysis['suggestions'] = $this->generate_improvement_suggestions($analysis);
182
183 return $analysis;
184 }
185
186 /**
187 * Prepare content for AI processing
188 *
189 * @param string $content Raw content
190 * @return string Processed content
191 */
192 private function prepare_content(string $content): string {
193 // Remove HTML tags
194 $content = wp_strip_all_tags($content);
195
196 // Remove shortcodes
197 $content = strip_shortcodes($content);
198
199 // Normalize whitespace
200 $content = preg_replace('/\s+/', ' ', $content);
201
202 // Trim and limit length for AI processing
203 $content = trim($content);
204 $max_length = 4000; // Reasonable limit for AI processing
205
206 if (strlen($content) > $max_length) {
207 $content = substr($content, 0, $max_length) . '...';
208 }
209
210 return $content;
211 }
212
213 /**
214 * Extract content from WordPress post
215 *
216 * @param \WP_Post $post Post object
217 * @return string Extracted content
218 */
219 private function extract_post_content(\WP_Post $post): string {
220 $content = $post->post_title . "\n\n" . $post->post_content;
221
222 // Add excerpt if available
223 if (!empty($post->post_excerpt)) {
224 $content = $post->post_title . "\n\n" . $post->post_excerpt . "\n\n" . $post->post_content;
225 }
226
227 return $content;
228 }
229
230 /**
231 * Determine content type from post
232 *
233 * @param \WP_Post $post Post object
234 * @return string Content type
235 */
236 private function determine_content_type(\WP_Post $post): string {
237 switch ($post->post_type) {
238 case 'page':
239 return 'page';
240 case 'product':
241 return 'product';
242 case 'post':
243 default:
244 return 'blog_post';
245 }
246 }
247
248 /**
249 * Validate and enhance generated metadata
250 *
251 * @param array $metadata Generated metadata
252 * @param array $options Generation options
253 * @return array Enhanced metadata
254 */
255 private function validate_and_enhance_metadata(array $metadata, array $options): array {
256 // Validate title length
257 if (strlen($metadata['title']) > $options['max_title_length']) {
258 $metadata['title'] = substr($metadata['title'], 0, $options['max_title_length'] - 3) . '...';
259 $metadata['title_truncated'] = true;
260 }
261
262 // Validate description length
263 if (strlen($metadata['description']) > $options['max_description_length']) {
264 $metadata['description'] = substr($metadata['description'], 0, $options['max_description_length'] - 3) . '...';
265 $metadata['description_truncated'] = true;
266 }
267
268 // Add character counts
269 $metadata['title_length'] = strlen($metadata['title']);
270 $metadata['description_length'] = strlen($metadata['description']);
271
272 // Calculate optimization score
273 $metadata['optimization_score'] = $this->calculate_optimization_score($metadata, $options);
274
275 return $metadata;
276 }
277
278 /**
279 * Save metadata to post meta
280 *
281 * @param int $post_id Post ID
282 * @param array $metadata Metadata to save
283 * @return void
284 */
285 private function save_post_metadata(int $post_id, array $metadata): void {
286 update_post_meta($post_id, '_thinkrank_ai_title', $metadata['title']);
287 update_post_meta($post_id, '_thinkrank_ai_description', $metadata['description']);
288 update_post_meta($post_id, '_thinkrank_focus_keyword', $metadata['focus_keyword']);
289 update_post_meta($post_id, '_thinkrank_generated_at', $metadata['generated_at']);
290 update_post_meta($post_id, '_thinkrank_metadata_full', $metadata);
291 }
292
293 /**
294 * Calculate basic readability score
295 *
296 * @param string $content Content to analyze
297 * @return float Readability score
298 */
299 private function calculate_readability_score(string $content): float {
300 $sentences = preg_split('/[.!?]+/', $content);
301 $words = str_word_count($content);
302 $syllables = $this->count_syllables($content);
303
304 if (count($sentences) === 0 || $words === 0) {
305 return 0;
306 }
307
308 // Simplified Flesch Reading Ease formula
309 $score = 206.835 - (1.015 * ($words / count($sentences))) - (84.6 * ($syllables / $words));
310
311 return max(0, min(100, $score));
312 }
313
314 /**
315 * Count syllables in text (simplified)
316 *
317 * @param string $text Text to analyze
318 * @return int Syllable count
319 */
320 private function count_syllables(string $text): int {
321 $words = str_word_count($text, 1);
322 $syllables = 0;
323
324 foreach ($words as $word) {
325 $syllables += max(1, preg_match_all('/[aeiouy]+/i', $word));
326 }
327
328 return $syllables;
329 }
330
331 /**
332 * Analyze keyword density
333 *
334 * @param string $content Content to analyze
335 * @return array Keyword density analysis
336 */
337 private function analyze_keyword_density(string $content): array {
338 $words = str_word_count(strtolower($content), 1);
339 $total_words = count($words);
340 $word_counts = array_count_values($words);
341
342 // Remove common stop words
343 $stop_words = ['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
344 $word_counts = array_diff_key($word_counts, array_flip($stop_words));
345
346 // Calculate density for top keywords
347 arsort($word_counts);
348 $top_keywords = array_slice($word_counts, 0, 10, true);
349
350 $density = [];
351 foreach ($top_keywords as $word => $count) {
352 $density[$word] = round(($count / $total_words) * 100, 2);
353 }
354
355 return $density;
356 }
357
358 /**
359 * Analyze existing metadata
360 *
361 * @param array $metadata Metadata to analyze
362 * @return array Analysis results
363 */
364 private function analyze_metadata(array $metadata): array {
365 return [
366 'title_length_ok' => strlen($metadata['title'] ?? '') <= 60,
367 'description_length_ok' => strlen($metadata['description'] ?? '') <= 160,
368 'has_focus_keyword' => !empty($metadata['focus_keyword'] ?? ''),
369 ];
370 }
371
372 /**
373 * Generate improvement suggestions
374 *
375 * @param array $analysis Content analysis
376 * @return array Suggestions
377 */
378 private function generate_improvement_suggestions(array $analysis): array {
379 $suggestions = [];
380
381 if ($analysis['content_length'] < 300) {
382 $suggestions[] = 'Consider adding more content. Longer content typically performs better in search results.';
383 }
384
385 if ($analysis['readability_score'] < 60) {
386 $suggestions[] = 'Content readability could be improved. Try using shorter sentences and simpler words.';
387 }
388
389 return $suggestions;
390 }
391
392 /**
393 * Calculate optimization score
394 *
395 * @param array $metadata Generated metadata
396 * @param array $options Generation options
397 * @return int Optimization score (0-100)
398 */
399 private function calculate_optimization_score(array $metadata, array $options): int {
400 $score = 0;
401
402 // Title optimization (30 points)
403 if (!empty($metadata['title'])) {
404 $score += 15;
405 if (strlen($metadata['title']) >= 30 && strlen($metadata['title']) <= 60) {
406 $score += 15;
407 }
408 }
409
410 // Description optimization (30 points)
411 if (!empty($metadata['description'])) {
412 $score += 15;
413 if (strlen($metadata['description']) >= 120 && strlen($metadata['description']) <= 160) {
414 $score += 15;
415 }
416 }
417
418 // Keyword optimization (40 points)
419 if (!empty($metadata['focus_keyword'])) {
420 $score += 20;
421 if (!empty($options['target_keyword']) &&
422 stripos($metadata['title'], $options['target_keyword']) !== false) {
423 $score += 20;
424 }
425 }
426
427 return min(100, $score);
428 }
429 }
430