PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.2
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-social-meta-manager.php

class-social-meta-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.2, at includes/seo/class-social-meta-manager.php

2,384 lines 89.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Social Meta Manager Class
4 *
5 * Universal social media meta tag generation and optimization for all platforms.
6 * Implements 2025 social media SEO best practices with real Open Graph and Twitter Card
7 * specifications, social image optimization, and platform-specific handling.
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 /**
19 * Social Meta Manager Class
20 *
21 * Generates and validates social media meta tags for all supported platforms.
22 * Provides context-aware social meta generation with image optimization and
23 * platform-specific meta tag handling.
24 *
25 * @since 1.0.0
26 */
27 class Social_Meta_Manager extends Abstract_SEO_Manager {
28
29 /**
30 * Supported social media platforms with their specifications
31 *
32 * @since 1.0.0
33 * @var array
34 */
35 private array $supported_platforms = [
36 'facebook' => [
37 'og_required' => ['og:title', 'og:type', 'og:image', 'og:url'],
38 'og_recommended' => ['og:description', 'og:site_name', 'og:locale'],
39 'og_optional' => ['og:updated_time', 'og:see_also', 'og:video', 'og:audio'],
40 'image_min_width' => 600,
41 'image_min_height' => 315,
42 'image_recommended_ratio' => 1.91,
43 'title_max_length' => 60,
44 'description_max_length' => 160
45 ],
46 'twitter' => [
47 'card_types' => ['summary', 'summary_large_image', 'app', 'player'],
48 'required' => ['twitter:card', 'twitter:title'],
49 'recommended' => ['twitter:description', 'twitter:image', 'twitter:site'],
50 'optional' => ['twitter:creator', 'twitter:player', 'twitter:app:name'],
51 'image_min_width' => 300,
52 'image_min_height' => 157,
53 'image_max_size' => 5242880, // 5MB
54 'title_max_length' => 70,
55 'description_max_length' => 200
56 ],
57 'linkedin' => [
58 'og_required' => ['og:title', 'og:type', 'og:image', 'og:url'],
59 'og_recommended' => ['og:description'],
60 'image_min_width' => 1200,
61 'image_min_height' => 627,
62 'image_recommended_ratio' => 1.91,
63 'title_max_length' => 70,
64 'description_max_length' => 160
65 ],
66 'pinterest' => [
67 'required' => ['og:title', 'og:type', 'og:image', 'og:url'],
68 'recommended' => ['og:description', 'og:site_name'],
69 'image_min_width' => 600,
70 'image_min_height' => 900,
71 'image_recommended_ratio' => 0.67, // 2:3 ratio
72 'title_max_length' => 100,
73 'description_max_length' => 500
74 ],
75 'whatsapp' => [
76 'og_required' => ['og:title', 'og:type', 'og:image', 'og:url'],
77 'og_recommended' => ['og:description'],
78 'image_min_width' => 300,
79 'image_min_height' => 200,
80 'title_max_length' => 65,
81 'description_max_length' => 160
82 ]
83 ];
84
85 /**
86 * Open Graph types with their specifications
87 *
88 * @since 1.0.0
89 * @var array
90 */
91 private array $og_types = [
92 'website' => ['og:title', 'og:type', 'og:image', 'og:url'],
93 'article' => ['og:title', 'og:type', 'og:image', 'og:url', 'article:author', 'article:published_time'],
94 'product' => ['og:title', 'og:type', 'og:image', 'og:url', 'product:price:amount', 'product:price:currency'],
95 'video' => ['og:title', 'og:type', 'og:image', 'og:url', 'og:video', 'video:duration'],
96 'music' => ['og:title', 'og:type', 'og:image', 'og:url', 'music:duration', 'music:album'],
97 'book' => ['og:title', 'og:type', 'og:image', 'og:url', 'book:author', 'book:isbn'],
98 'profile' => ['og:title', 'og:type', 'og:image', 'og:url', 'profile:first_name', 'profile:last_name']
99 ];
100
101 /**
102 * Constructor
103 *
104 * @since 1.0.0
105 */
106 public function __construct() {
107 parent::__construct('social_meta');
108 }
109
110 /**
111 * Generate Open Graph tags for content
112 *
113 * @since 1.0.0
114 *
115 * @param array $data Content data for OG generation
116 * @param string $context Context type ('site', 'post', 'page', etc.)
117 * @param string $platform Target platform ('facebook', 'linkedin', etc.)
118 * @return array Generated Open Graph tags
119 */
120 public function generate_og_tags(array $data, string $context = 'site', string $platform = 'facebook'): array {
121 $platform = strtolower($platform);
122 if (!isset($this->supported_platforms[$platform])) {
123 $platform = 'facebook'; // Default fallback
124 }
125
126 $platform_spec = $this->supported_platforms[$platform];
127 $og_tags = [];
128
129 // Determine OG type based on context
130 $og_type = $this->determine_og_type($context, $data);
131 $og_tags['og:type'] = $og_type;
132
133 // Required OG tags
134 $og_tags['og:title'] = $this->optimize_title_for_platform($data['title'] ?? '', $platform);
135 $og_tags['og:url'] = $data['url'] ?? $this->get_current_url();
136
137 // Image handling with optimization
138 if (!empty($data['image'])) {
139 $optimized_image = $this->optimize_image_for_platform($data['image'], $platform);
140 $og_tags['og:image'] = $optimized_image['url'];
141 if (!empty($optimized_image['width'])) {
142 $og_tags['og:image:width'] = $optimized_image['width'];
143 }
144 if (!empty($optimized_image['height'])) {
145 $og_tags['og:image:height'] = $optimized_image['height'];
146 }
147 if (!empty($optimized_image['type'])) {
148 $og_tags['og:image:type'] = $optimized_image['type'];
149 }
150 if (!empty($optimized_image['alt'])) {
151 $og_tags['og:image:alt'] = $optimized_image['alt'];
152 }
153 } else {
154 // Fallback to site default image
155 $default_image = $this->get_default_social_image();
156 if ($default_image) {
157 $og_tags['og:image'] = $default_image;
158 }
159 }
160
161 // Recommended OG tags
162 if (!empty($data['description'])) {
163 $og_tags['og:description'] = $this->optimize_description_for_platform($data['description'], $platform);
164 }
165
166 $og_tags['og:site_name'] = $data['site_name'] ?? get_bloginfo('name');
167 $og_tags['og:locale'] = $data['locale'] ?? $this->get_og_locale();
168
169 // Context-specific OG tags
170 $og_tags = $this->add_context_specific_og_tags($og_tags, $context, $data, $og_type);
171
172 // Platform-specific optimizations
173 $og_tags = $this->apply_platform_specific_optimizations($og_tags, $platform, $data);
174
175 return $og_tags;
176 }
177
178 /**
179 * Generate Twitter Card tags for content
180 *
181 * @since 1.0.0
182 *
183 * @param array $data Content data for Twitter Card generation
184 * @param string $context Context type ('site', 'post', 'page', etc.)
185 * @return array Generated Twitter Card tags
186 */
187 public function generate_twitter_tags(array $data, string $context = 'site'): array {
188 $twitter_tags = [];
189
190 // Determine card type based on content
191 $card_type = $this->determine_twitter_card_type($data, $context);
192 $twitter_tags['twitter:card'] = $card_type;
193
194 // Required tags
195 $twitter_tags['twitter:title'] = $this->optimize_title_for_platform($data['title'] ?? '', 'twitter');
196
197 // Recommended tags
198 if (!empty($data['description'])) {
199 $twitter_tags['twitter:description'] = $this->optimize_description_for_platform($data['description'], 'twitter');
200 }
201
202 // Image handling - prioritize Twitter-specific image
203 $twitter_image = !empty($data['twitter_image']) ? $data['twitter_image'] : $data['image'];
204 if (!empty($twitter_image)) {
205 $optimized_image = $this->optimize_image_for_platform($twitter_image, 'twitter');
206 $twitter_tags['twitter:image'] = $optimized_image['url'];
207 if (!empty($optimized_image['alt'])) {
208 $twitter_tags['twitter:image:alt'] = $optimized_image['alt'];
209 }
210 }
211
212 // Site and creator information
213 $twitter_site = $this->get_twitter_site_handle();
214 if ($twitter_site) {
215 $twitter_tags['twitter:site'] = $twitter_site;
216 }
217
218 $twitter_creator = $this->get_twitter_creator_handle();
219 if ($twitter_creator) {
220 $twitter_tags['twitter:creator'] = $twitter_creator;
221 } elseif (!empty($data['author']['twitter'])) {
222 // Fallback to author data if available
223 $twitter_tags['twitter:creator'] = $data['author']['twitter'];
224 }
225
226 // Card-specific tags
227 $twitter_tags = $this->add_twitter_card_specific_tags($twitter_tags, $card_type, $data, $context);
228
229 return $twitter_tags;
230 }
231
232 /**
233 * Optimize social image for platform requirements
234 *
235 * @since 1.0.0
236 *
237 * @param string $image_url Image URL to optimize
238 * @param string $platform Target platform
239 * @return array Optimized image data
240 */
241 public function optimize_social_image(string $image_url, string $platform): array {
242 return $this->optimize_image_for_platform($image_url, $platform);
243 }
244
245 /**
246 * Generate social media preview data
247 *
248 * @since 1.0.0
249 *
250 * @param array $data Content data
251 * @param string $platform Target platform
252 * @return array Preview data for the platform
253 */
254 public function preview_social_post(array $data, string $platform): array {
255 $preview = [
256 'platform' => $platform,
257 'valid' => false,
258 'preview_url' => '',
259 'title' => '',
260 'description' => '',
261 'image' => '',
262 'warnings' => [],
263 'suggestions' => []
264 ];
265
266 switch ($platform) {
267 case 'facebook':
268 case 'linkedin':
269 $og_tags = $this->generate_og_tags($data, 'post', $platform);
270 $preview = $this->generate_og_preview($og_tags, $platform, $preview);
271 break;
272 case 'twitter':
273 $twitter_tags = $this->generate_twitter_tags($data, 'post');
274 $preview = $this->generate_twitter_preview($twitter_tags, $preview);
275 break;
276 case 'pinterest':
277 $og_tags = $this->generate_og_tags($data, 'post', $platform);
278 $preview = $this->generate_pinterest_preview($og_tags, $preview);
279 break;
280 default:
281 $preview['warnings'][] = 'Unsupported platform for preview generation';
282 }
283
284 return $preview;
285 }
286
287 /**
288 * Validate SEO settings (implements interface)
289 *
290 * @since 1.0.0
291 *
292 * @param array $settings Settings array to validate
293 * @param string $context Optional context for focused validation
294 * @return array Validation results
295 */
296 public function validate_settings(array $settings, string $context = 'all'): array {
297 $validation = [
298 'valid' => true,
299 'errors' => [],
300 'warnings' => [],
301 'suggestions' => [],
302 'score' => 100
303 ];
304
305 // Add detailed field validation breakdown
306 $field_details = $this->get_detailed_field_validation($settings, $context);
307 $validation['field_details'] = $field_details;
308
309 // Convert field details to validation format
310 foreach ($field_details as $field) {
311 switch ($field['status']) {
312 case 'error':
313 $validation['errors'][] = $field['label'];
314 $validation['valid'] = false;
315 $validation['score'] -= 15;
316 break;
317 case 'warning':
318 $validation['warnings'][] = $field['label'];
319 $validation['score'] -= 8;
320 break;
321 case 'suggestion':
322 $validation['suggestions'][] = $field['label'];
323 $validation['score'] -= 3;
324 break;
325 }
326 }
327
328 // Ensure score doesn't go below 0
329 $validation['score'] = max(0, $validation['score']);
330
331
332
333 // Validate general enabled setting (Social Media tab format)
334 if (isset($settings['enabled'])) {
335 // Convert string booleans to actual booleans
336 if (is_string($settings['enabled'])) {
337 $settings['enabled'] = filter_var($settings['enabled'], FILTER_VALIDATE_BOOLEAN);
338 }
339 if (!is_bool($settings['enabled'])) {
340 $validation['errors'][] = 'enabled must be a boolean value';
341 $validation['valid'] = false;
342 }
343 }
344
345 // Validate Open Graph settings (Social Media tab format)
346 if (isset($settings['enable_open_graph'])) {
347 // Convert string booleans to actual booleans
348 if (is_string($settings['enable_open_graph'])) {
349 $settings['enable_open_graph'] = filter_var($settings['enable_open_graph'], FILTER_VALIDATE_BOOLEAN);
350 }
351 if (!is_bool($settings['enable_open_graph'])) {
352 $validation['errors'][] = 'enable_open_graph must be a boolean value';
353 $validation['valid'] = false;
354 }
355 }
356
357 // Validate Twitter Cards settings (Social Media tab format)
358 if (isset($settings['enable_twitter_cards'])) {
359 // Convert string booleans to actual booleans
360 if (is_string($settings['enable_twitter_cards'])) {
361 $settings['enable_twitter_cards'] = filter_var($settings['enable_twitter_cards'], FILTER_VALIDATE_BOOLEAN);
362 }
363 if (!is_bool($settings['enable_twitter_cards'])) {
364 $validation['errors'][] = 'enable_twitter_cards must be a boolean value';
365 $validation['valid'] = false;
366 }
367 }
368
369 // Validate Twitter settings (only when relevant)
370 if ($context === 'all' || $context === 'twitter' || $context === 'twitter-cards') {
371 // Validate Twitter username format
372 if (isset($settings['twitter_username']) && !empty($settings['twitter_username'])) {
373 $username = trim($settings['twitter_username']);
374
375 // Remove @ if present (we store without @, add @ in output)
376 $clean_username = ltrim($username, '@');
377
378 if (empty($clean_username)) {
379 $validation['errors'][] = 'twitter_username cannot be empty';
380 $validation['valid'] = false;
381 } elseif (strlen($clean_username) > 15) {
382 $validation['errors'][] = 'twitter_username must be 15 characters or less';
383 $validation['valid'] = false;
384 } elseif (!preg_match('/^[A-Za-z0-9_]+$/', $clean_username)) {
385 $validation['errors'][] = 'twitter_username can only contain letters, numbers, and underscores';
386 $validation['valid'] = false;
387 }
388
389 // Update the setting to store without @ for consistency
390 $settings['twitter_username'] = $clean_username;
391 }
392
393 // Validate Twitter creator format
394 if (isset($settings['twitter_creator']) && !empty($settings['twitter_creator'])) {
395 $creator = trim($settings['twitter_creator']);
396
397 // Remove @ if present (we store without @, add @ in output)
398 $clean_creator = ltrim($creator, '@');
399
400 if (empty($clean_creator)) {
401 $validation['errors'][] = 'twitter_creator cannot be empty';
402 $validation['valid'] = false;
403 } elseif (strlen($clean_creator) > 15) {
404 $validation['errors'][] = 'twitter_creator must be 15 characters or less';
405 $validation['valid'] = false;
406 } elseif (!preg_match('/^[A-Za-z0-9_]+$/', $clean_creator)) {
407 $validation['errors'][] = 'twitter_creator can only contain letters, numbers, and underscores';
408 $validation['valid'] = false;
409 }
410
411 // Update the setting to store without @ for consistency
412 $settings['twitter_creator'] = $clean_creator;
413 }
414
415 // Validate Twitter card type
416 if (isset($settings['twitter_card_type'])) {
417 $valid_types = ['summary', 'summary_large_image', 'app', 'player'];
418 if (!in_array($settings['twitter_card_type'], $valid_types, true)) {
419 $validation['errors'][] = 'twitter_card_type must be one of: ' . implode(', ', $valid_types);
420 $validation['valid'] = false;
421 }
422 }
423 }
424
425 // Validate OG type
426 if (isset($settings['og_type'])) {
427 $valid_types = ['website', 'article', 'book', 'profile', 'music.song', 'music.album', 'video.movie', 'video.episode'];
428 if (!in_array($settings['og_type'], $valid_types, true)) {
429 $validation['warnings'][] = 'og_type should be one of the standard Open Graph types for best compatibility';
430 }
431 }
432
433 // Validate image dimensions
434 if (isset($settings['og_image_width']) && (!is_numeric($settings['og_image_width']) || $settings['og_image_width'] < 200)) {
435 $validation['warnings'][] = 'og_image_width should be at least 200 pixels for optimal social sharing';
436 }
437
438 if (isset($settings['og_image_height']) && (!is_numeric($settings['og_image_height']) || $settings['og_image_height'] < 200)) {
439 $validation['warnings'][] = 'og_image_height should be at least 200 pixels for optimal social sharing';
440 }
441
442 // Validate description length
443 if (isset($settings['max_description_length']) && (!is_numeric($settings['max_description_length']) || $settings['max_description_length'] < 50 || $settings['max_description_length'] > 300)) {
444 $validation['warnings'][] = 'max_description_length should be between 50 and 300 characters';
445 }
446
447 // Legacy validation for backward compatibility (only when legacy fields are actually present)
448 if (isset($settings['og_enabled'])) {
449 // Convert string booleans to actual booleans
450 if (is_string($settings['og_enabled'])) {
451 $settings['og_enabled'] = filter_var($settings['og_enabled'], FILTER_VALIDATE_BOOLEAN);
452 }
453 if (!is_bool($settings['og_enabled'])) {
454 $validation['errors'][] = 'og_enabled must be a boolean value';
455 $validation['valid'] = false;
456 }
457 }
458
459 if (isset($settings['twitter_enabled'])) {
460 // Convert string booleans to actual booleans
461 if (is_string($settings['twitter_enabled'])) {
462 $settings['twitter_enabled'] = filter_var($settings['twitter_enabled'], FILTER_VALIDATE_BOOLEAN);
463 }
464 if (!is_bool($settings['twitter_enabled'])) {
465 $validation['errors'][] = 'twitter_enabled must be a boolean value';
466 $validation['valid'] = false;
467 }
468 }
469
470 // Validate Twitter card type
471 if (isset($settings['twitter_card_type'])) {
472 $valid_card_types = $this->supported_platforms['twitter']['card_types'];
473 if (!in_array($settings['twitter_card_type'], $valid_card_types, true)) {
474 $validation['errors'][] = 'Invalid Twitter card type. Must be one of: ' . implode(', ', $valid_card_types);
475 $validation['valid'] = false;
476 }
477 }
478
479 // Validate default image
480 if (isset($settings['default_image']) && !empty($settings['default_image'])) {
481 if (!filter_var($settings['default_image'], FILTER_VALIDATE_URL)) {
482 $validation['errors'][] = 'default_image must be a valid URL';
483 $validation['valid'] = false;
484 } else {
485 // Check image dimensions and format
486 $image_validation = $this->validate_social_image($settings['default_image']);
487 if (!$image_validation['valid']) {
488 $validation['warnings'] = array_merge($validation['warnings'], $image_validation['warnings']);
489 }
490 }
491 }
492
493 // Validate Twitter site handle
494 if (isset($settings['twitter_site']) && !empty($settings['twitter_site'])) {
495 if (!preg_match('/^@[a-zA-Z0-9_]{1,15}$/', $settings['twitter_site'])) {
496 $validation['errors'][] = 'twitter_site must be a valid Twitter handle (e.g., @username)';
497 $validation['valid'] = false;
498 }
499 }
500
501 // Validate custom OG tags
502 if (isset($settings['custom_og_tags']) && !empty($settings['custom_og_tags'])) {
503 if (!is_array($settings['custom_og_tags'])) {
504 $validation['errors'][] = 'custom_og_tags must be an array';
505 $validation['valid'] = false;
506 } else {
507 foreach ($settings['custom_og_tags'] as $property => $content) {
508 if (!is_string($property) || !is_string($content)) {
509 $validation['errors'][] = 'Custom OG tags must have string property names and content';
510 $validation['valid'] = false;
511 break;
512 }
513 }
514 }
515 }
516
517 // Validate platform-specific settings
518
519 // Validate Instagram verification
520 if (isset($settings['instagram_verification']) && !empty($settings['instagram_verification'])) {
521 if (!preg_match('/^[a-zA-Z0-9_-]{20,}$/', $settings['instagram_verification'])) {
522 $validation['errors'][] = 'instagram_verification must be at least 20 characters (letters, numbers, underscore, dash)';
523 $validation['valid'] = false;
524 }
525 }
526
527 // Validate TikTok verification
528 if (isset($settings['tiktok_verification']) && !empty($settings['tiktok_verification'])) {
529 if (!preg_match('/^[a-zA-Z0-9_-]{20,}$/', $settings['tiktok_verification'])) {
530 $validation['errors'][] = 'tiktok_verification must be at least 20 characters (letters, numbers, underscore, dash)';
531 $validation['valid'] = false;
532 }
533 }
534
535 // Validate YouTube Channel ID
536 if (isset($settings['youtube_channel_id']) && !empty($settings['youtube_channel_id'])) {
537 if (!preg_match('/^UC[a-zA-Z0-9_-]{22}$/', $settings['youtube_channel_id'])) {
538 $validation['errors'][] = 'youtube_channel_id must start with "UC" followed by 22 characters';
539 $validation['valid'] = false;
540 }
541 }
542
543 // Validate WhatsApp Business ID
544 if (isset($settings['whatsapp_business_id']) && !empty($settings['whatsapp_business_id'])) {
545 if (!preg_match('/^[0-9]{10,15}$/', $settings['whatsapp_business_id'])) {
546 $validation['errors'][] = 'whatsapp_business_id must be 10-15 digits';
547 $validation['valid'] = false;
548 }
549 }
550
551 return $validation;
552 }
553
554 /**
555 * Get detailed field validation breakdown
556 *
557 * @since 1.0.0
558 *
559 * @param array $settings Settings array to validate
560 * @param string $context Context for specific validation
561 * @return array Detailed field validation results
562 */
563 private function get_detailed_field_validation(array $settings, string $context = 'all'): array {
564 $field_details = [];
565
566 // Return tab-specific validation based on context
567 switch ($context) {
568 case 'open-graph':
569 return $this->validate_open_graph_fields($settings);
570 case 'twitter-cards':
571 return $this->validate_twitter_fields($settings);
572 default:
573 // For 'all' or unknown context, return only Social Media fields (Open Graph + Twitter)
574 $field_details = array_merge($field_details, $this->validate_open_graph_fields($settings));
575 $field_details = array_merge($field_details, $this->validate_twitter_fields($settings));
576 return $field_details;
577 }
578 }
579
580 /**
581 * Validate Open Graph fields
582 *
583 * @since 1.0.0
584 *
585 * @param array $settings Settings array to validate
586 * @return array Open Graph field validation results
587 */
588 private function validate_open_graph_fields(array $settings): array {
589 $field_details = [];
590
591 // Open Graph Enabled
592 if (!empty($settings['enable_open_graph'])) {
593 $field_details[] = [
594 'field' => 'enable_open_graph',
595 'label' => 'Open Graph is enabled for social media sharing.',
596 'status' => 'valid',
597 'icon' => ''
598 ];
599
600 // Site Name validation
601 if (!empty($settings['og_site_name'])) {
602 if (strlen($settings['og_site_name']) <= 60) {
603 $field_details[] = [
604 'field' => 'og_site_name',
605 'label' => 'Site name is properly configured for Open Graph.',
606 'status' => 'valid',
607 'icon' => ''
608 ];
609 } else {
610 $field_details[] = [
611 'field' => 'og_site_name',
612 'label' => 'Site name is longer than 60 characters, may be truncated.',
613 'status' => 'warning',
614 'icon' => ''
615 ];
616 }
617 } else {
618 $field_details[] = [
619 'field' => 'og_site_name',
620 'label' => 'Site name is required for Open Graph.',
621 'status' => 'error',
622 'icon' => ''
623 ];
624 }
625
626 // Description validation
627 if (!empty($settings['og_description'])) {
628 $length = strlen($settings['og_description']);
629 if ($length >= 120 && $length <= 160) {
630 $field_details[] = [
631 'field' => 'og_description',
632 'label' => 'Description is properly configured for social sharing.',
633 'status' => 'valid',
634 'icon' => ''
635 ];
636 } else {
637 $field_details[] = [
638 'field' => 'og_description',
639 'label' => 'Description length could be optimized (120-160 characters recommended).',
640 'status' => 'warning',
641 'icon' => ''
642 ];
643 }
644 } else {
645 $field_details[] = [
646 'field' => 'og_description',
647 'label' => 'Description is recommended for better social sharing.',
648 'status' => 'warning',
649 'icon' => ''
650 ];
651 }
652
653 // Content Type validation
654 if (!empty($settings['og_type'])) {
655 $field_details[] = [
656 'field' => 'og_type',
657 'label' => 'Content type is configured for Open Graph.',
658 'status' => 'valid',
659 'icon' => ''
660 ];
661 } else {
662 $field_details[] = [
663 'field' => 'og_type',
664 'label' => 'Content type should be specified.',
665 'status' => 'suggestion',
666 'icon' => ''
667 ];
668 }
669
670 // Default Image validation
671 if (!empty($settings['default_og_image'])) {
672 if (filter_var($settings['default_og_image'], FILTER_VALIDATE_URL)) {
673 $field_details[] = [
674 'field' => 'default_og_image',
675 'label' => 'Default Open Graph image is configured.',
676 'status' => 'valid',
677 'icon' => ''
678 ];
679 } else {
680 $field_details[] = [
681 'field' => 'default_og_image',
682 'label' => 'Default Open Graph image URL appears invalid.',
683 'status' => 'warning',
684 'icon' => ''
685 ];
686 }
687 } else {
688 $field_details[] = [
689 'field' => 'default_og_image',
690 'label' => 'Default image is recommended for social sharing.',
691 'status' => 'suggestion',
692 'icon' => ''
693 ];
694 }
695
696 } else {
697 $field_details[] = [
698 'field' => 'enable_open_graph',
699 'label' => 'Open Graph is disabled. Enable for better social media sharing.',
700 'status' => 'suggestion',
701 'icon' => ''
702 ];
703 }
704
705 return $field_details;
706 }
707
708 /**
709 * Validate Twitter fields
710 *
711 * @since 1.0.0
712 *
713 * @param array $settings Settings array to validate
714 * @return array Twitter field validation results
715 */
716 private function validate_twitter_fields(array $settings): array {
717 $field_details = [];
718
719 // Twitter Cards Enabled
720 if (!empty($settings['enable_twitter_cards'])) {
721 $field_details[] = [
722 'field' => 'enable_twitter_cards',
723 'label' => 'Twitter Cards are enabled for Twitter sharing.',
724 'status' => 'valid',
725 'icon' => ''
726 ];
727
728 // Twitter Username validation
729 if (!empty($settings['twitter_username'])) {
730 $username = trim($settings['twitter_username']);
731 $clean_username = ltrim($username, '@');
732
733 if (strlen($clean_username) <= 15 && preg_match('/^[A-Za-z0-9_]+$/', $clean_username)) {
734 $field_details[] = [
735 'field' => 'twitter_username',
736 'label' => 'Twitter username is properly formatted.',
737 'status' => 'valid',
738 'icon' => ''
739 ];
740 } else {
741 $field_details[] = [
742 'field' => 'twitter_username',
743 'label' => 'Twitter username format is invalid (max 15 chars, letters/numbers/underscore only).',
744 'status' => 'error',
745 'icon' => ''
746 ];
747 }
748 } else {
749 $field_details[] = [
750 'field' => 'twitter_username',
751 'label' => 'Twitter username recommended for better attribution.',
752 'status' => 'suggestion',
753 'icon' => ''
754 ];
755 }
756
757 // Twitter Creator validation
758 if (!empty($settings['twitter_creator'])) {
759 $creator = trim($settings['twitter_creator']);
760 $clean_creator = ltrim($creator, '@');
761
762 if (strlen($clean_creator) <= 15 && preg_match('/^[A-Za-z0-9_]+$/', $clean_creator)) {
763 $field_details[] = [
764 'field' => 'twitter_creator',
765 'label' => 'Twitter creator is properly formatted.',
766 'status' => 'valid',
767 'icon' => ''
768 ];
769 } else {
770 $field_details[] = [
771 'field' => 'twitter_creator',
772 'label' => 'Twitter creator format is invalid (max 15 chars, letters/numbers/underscore only).',
773 'status' => 'error',
774 'icon' => ''
775 ];
776 }
777 } else {
778 $field_details[] = [
779 'field' => 'twitter_creator',
780 'label' => 'Twitter creator recommended for content attribution.',
781 'status' => 'suggestion',
782 'icon' => ''
783 ];
784 }
785
786 // Card Type validation
787 $valid_card_types = ['summary', 'summary_large_image', 'app', 'player'];
788 if (!empty($settings['twitter_card_type']) && in_array($settings['twitter_card_type'], $valid_card_types)) {
789 $field_details[] = [
790 'field' => 'twitter_card_type',
791 'label' => 'Twitter Card type is properly configured.',
792 'status' => 'valid',
793 'icon' => ''
794 ];
795 } else {
796 $field_details[] = [
797 'field' => 'twitter_card_type',
798 'label' => 'Valid Twitter Card type is required.',
799 'status' => 'error',
800 'icon' => ''
801 ];
802 }
803
804 // Default Twitter Image validation
805 if (!empty($settings['default_twitter_image'])) {
806 if (filter_var($settings['default_twitter_image'], FILTER_VALIDATE_URL)) {
807 $field_details[] = [
808 'field' => 'default_twitter_image',
809 'label' => 'Default Twitter Card image is configured.',
810 'status' => 'valid',
811 'icon' => ''
812 ];
813 } else {
814 $field_details[] = [
815 'field' => 'default_twitter_image',
816 'label' => 'Default Twitter Card image URL appears invalid.',
817 'status' => 'warning',
818 'icon' => ''
819 ];
820 }
821 } else {
822 $field_details[] = [
823 'field' => 'default_twitter_image',
824 'label' => 'Default image recommended for Twitter sharing.',
825 'status' => 'suggestion',
826 'icon' => ''
827 ];
828 }
829
830 } else {
831 $field_details[] = [
832 'field' => 'enable_twitter_cards',
833 'label' => 'Twitter Cards are disabled. Enable for better Twitter sharing.',
834 'status' => 'suggestion',
835 'icon' => ''
836 ];
837 }
838
839 return $field_details;
840 }
841
842
843
844 /**
845 * Get output data for frontend rendering (implements interface)
846 *
847 * @since 1.0.0
848 *
849 * @param string $context_type The context type
850 * @param int|null $context_id Optional. Context ID
851 * @return array Output data ready for frontend rendering
852 */
853 public function get_output_data(string $context_type, ?int $context_id): array {
854 $settings = $this->get_settings($context_type, $context_id);
855 $output = [
856 'og_tags' => [],
857 'twitter_tags' => [],
858 'meta_tags' => [],
859 'enabled' => false
860 ];
861
862 // Check if individual social features are enabled
863 $og_enabled = $settings['enable_open_graph'] ?? $settings['og_enabled'] ?? false;
864 $twitter_enabled = $settings['enable_twitter_cards'] ?? $settings['twitter_enabled'] ?? false;
865
866 if (empty($og_enabled) && empty($twitter_enabled)) {
867 return $output;
868 }
869
870 $output['enabled'] = true;
871
872 // Extract content data
873 $content_data = $this->extract_social_content_data($context_type, $context_id, $settings);
874
875 // Generate Open Graph tags if enabled
876 $og_enabled = $settings['enable_open_graph'] ?? $settings['og_enabled'] ?? false;
877 if (!empty($og_enabled)) {
878 $output['og_tags'] = $this->generate_og_tags($content_data, $context_type);
879
880 // Add custom OG tags
881 if (!empty($settings['custom_og_tags'])) {
882 $output['og_tags'] = array_merge($output['og_tags'], $settings['custom_og_tags']);
883 }
884 }
885
886 // Generate Twitter Card tags if enabled
887 $twitter_enabled = $settings['enable_twitter_cards'] ?? $settings['twitter_enabled'] ?? false;
888 if (!empty($twitter_enabled)) {
889 $output['twitter_tags'] = $this->generate_twitter_tags($content_data, $context_type);
890 }
891
892 // Generate platform-specific meta tags
893 $output['platform_tags'] = $this->generate_platform_meta_tags($settings);
894
895 // Convert to meta tag format for HTML output
896 $output['meta_tags'] = $this->convert_to_meta_tags($output['og_tags'], $output['twitter_tags'], $output['platform_tags']);
897
898 return $output;
899 }
900
901 /**
902 * Get default settings for a context type (implements interface)
903 *
904 * @since 1.0.0
905 *
906 * @param string $context_type The context type to get defaults for
907 * @return array Default settings array
908 */
909 public function get_default_settings(string $context_type): array {
910 // Get WordPress site defaults
911 $site_name = get_bloginfo('name');
912 $site_description = get_bloginfo('description');
913
914 $defaults = [
915 // Open Graph settings
916 'enable_open_graph' => true,
917 'og_site_name' => $site_name,
918 'og_description' => $site_description,
919 'og_type' => 'website',
920 'og_locale' => 'en_US',
921 'default_og_image' => '',
922 'og_image_width' => 1200,
923 'og_image_height' => 630,
924
925 // Twitter Cards settings
926 'enable_twitter_cards' => true,
927 'twitter_username' => '',
928 'twitter_creator' => '',
929 'twitter_card_type' => 'summary_large_image',
930 'default_twitter_image' => '',
931
932 // Facebook settings
933 'facebook_app_id' => '',
934 'facebook_admins' => '',
935
936 // LinkedIn settings
937 'enable_linkedin' => false,
938
939 // Pinterest settings
940 'enable_pinterest' => false,
941 'pinterest_site_verification' => '',
942
943 // Instagram settings
944 'enable_instagram' => false,
945 'instagram_verification' => '',
946
947 // TikTok settings
948 'enable_tiktok' => false,
949 'tiktok_verification' => '',
950
951 // YouTube settings
952 'enable_youtube' => false,
953 'youtube_channel_id' => '',
954
955 // WhatsApp Business settings
956 'enable_whatsapp' => false,
957 'whatsapp_business_id' => '',
958
959 // Advanced settings
960 'auto_generate_descriptions' => true,
961 'fallback_to_excerpt' => true,
962 'strip_html_tags' => true,
963 'max_description_length' => 160,
964
965 // Legacy format for backward compatibility (only when needed)
966 'custom_og_tags' => [],
967 'image_optimization' => true,
968 'auto_generate' => true
969 ];
970
971 // Context-specific defaults
972 switch ($context_type) {
973 case 'site':
974 $defaults['og_type'] = 'website';
975 break;
976 case 'post':
977 $defaults['og_type'] = 'article';
978 break;
979 case 'page':
980 $defaults['og_type'] = 'website';
981 break;
982 case 'product':
983 $defaults['og_type'] = 'product';
984 break;
985 }
986
987 return $defaults;
988 }
989
990 /**
991 * Get settings schema definition (implements interface)
992 *
993 * @since 1.0.0
994 *
995 * @param string $context_type The context type to get schema for
996 * @return array Settings schema definition
997 */
998 public function get_settings_schema(string $context_type): array {
999 return [
1000 'enable_open_graph' => [
1001 'type' => 'boolean',
1002 'title' => 'Enable Open Graph',
1003 'description' => 'Generate Open Graph meta tags for social media sharing',
1004 'default' => true
1005 ],
1006 'og_site_name' => [
1007 'type' => 'string',
1008 'title' => 'Site Name',
1009 'description' => 'The name of your website for Open Graph',
1010 'maxLength' => 60,
1011 'default' => get_bloginfo('name')
1012 ],
1013 'og_description' => [
1014 'type' => 'string',
1015 'title' => 'Site Description',
1016 'description' => 'Default description for Open Graph tags',
1017 'maxLength' => 160,
1018 'default' => get_bloginfo('description')
1019 ],
1020 'og_type' => [
1021 'type' => 'string',
1022 'title' => 'Open Graph Type',
1023 'description' => 'The type of content for Open Graph',
1024 'enum' => ['website', 'article', 'book', 'profile'],
1025 'default' => 'website'
1026 ],
1027 'og_locale' => [
1028 'type' => 'string',
1029 'title' => 'Locale',
1030 'description' => 'The locale for Open Graph tags',
1031 'default' => 'en_US'
1032 ],
1033 'default_og_image' => [
1034 'type' => 'string',
1035 'title' => 'Default Open Graph Image',
1036 'description' => 'Default image URL for Open Graph tags',
1037 'format' => 'uri',
1038 'default' => ''
1039 ],
1040 'og_image_width' => [
1041 'type' => 'integer',
1042 'title' => 'Image Width',
1043 'description' => 'Default width for Open Graph images',
1044 'minimum' => 200,
1045 'default' => 1200
1046 ],
1047 'og_image_height' => [
1048 'type' => 'integer',
1049 'title' => 'Image Height',
1050 'description' => 'Default height for Open Graph images',
1051 'minimum' => 200,
1052 'default' => 630
1053 ],
1054 'enable_twitter_cards' => [
1055 'type' => 'boolean',
1056 'title' => 'Enable Twitter Cards',
1057 'description' => 'Generate Twitter Card meta tags for Twitter sharing',
1058 'default' => true
1059 ],
1060 'twitter_username' => [
1061 'type' => 'string',
1062 'title' => 'Twitter Username',
1063 'description' => 'Twitter username for the site (without @, e.g., username)',
1064 'pattern' => '^[a-zA-Z0-9_]{1,15}$',
1065 'default' => ''
1066 ],
1067 'twitter_creator' => [
1068 'type' => 'string',
1069 'title' => 'Twitter Creator',
1070 'description' => 'Content creator Twitter username (without @, e.g., creator_username)',
1071 'pattern' => '^[a-zA-Z0-9_]{1,15}$',
1072 'default' => ''
1073 ],
1074 'twitter_card_type' => [
1075 'type' => 'string',
1076 'title' => 'Twitter Card Type',
1077 'description' => 'The type of Twitter Card to generate',
1078 'enum' => $this->supported_platforms['twitter']['card_types'],
1079 'default' => 'summary_large_image'
1080 ],
1081 'og_type' => [
1082 'type' => 'string',
1083 'title' => 'Open Graph Type',
1084 'description' => 'The Open Graph type for this content',
1085 'enum' => array_keys($this->og_types),
1086 'default' => $this->get_default_settings($context_type)['og_type']
1087 ],
1088 'default_image' => [
1089 'type' => 'string',
1090 'title' => 'Default Social Image',
1091 'description' => 'Default image URL for social media sharing',
1092 'format' => 'uri',
1093 'default' => ''
1094 ],
1095 'twitter_site' => [
1096 'type' => 'string',
1097 'title' => 'Twitter Site Handle',
1098 'description' => 'Twitter handle for the site (e.g., @username)',
1099 'pattern' => '^@[a-zA-Z0-9_]{1,15}$',
1100 'default' => ''
1101 ],
1102 'default_twitter_image' => [
1103 'type' => 'string',
1104 'title' => 'Default Twitter Image',
1105 'description' => 'Default image URL for Twitter Cards',
1106 'format' => 'uri',
1107 'default' => ''
1108 ],
1109 'facebook_app_id' => [
1110 'type' => 'string',
1111 'title' => 'Facebook App ID',
1112 'description' => 'Facebook App ID for analytics and insights',
1113 'pattern' => '^[0-9]+$',
1114 'default' => ''
1115 ],
1116 'facebook_admins' => [
1117 'type' => 'string',
1118 'title' => 'Facebook Admins',
1119 'description' => 'Comma-separated list of Facebook admin user IDs',
1120 'default' => ''
1121 ],
1122 'enable_linkedin' => [
1123 'type' => 'boolean',
1124 'title' => 'Enable LinkedIn',
1125 'description' => 'Enable LinkedIn-specific optimizations',
1126 'default' => false
1127 ],
1128 'enable_pinterest' => [
1129 'type' => 'boolean',
1130 'title' => 'Enable Pinterest',
1131 'description' => 'Enable Pinterest-specific optimizations',
1132 'default' => false
1133 ],
1134 'pinterest_site_verification' => [
1135 'type' => 'string',
1136 'title' => 'Pinterest Site Verification',
1137 'description' => 'Pinterest site verification meta tag content',
1138 'pattern' => '^[a-f0-9]{32}$',
1139 'default' => ''
1140 ],
1141 'enable_instagram' => [
1142 'type' => 'boolean',
1143 'title' => 'Enable Instagram',
1144 'description' => 'Enable Instagram-specific optimizations',
1145 'default' => false
1146 ],
1147 'instagram_verification' => [
1148 'type' => 'string',
1149 'title' => 'Instagram Site Verification',
1150 'description' => 'Instagram Business account verification code',
1151 'pattern' => '^[a-zA-Z0-9_-]{20,}$',
1152 'default' => ''
1153 ],
1154 'enable_tiktok' => [
1155 'type' => 'boolean',
1156 'title' => 'Enable TikTok',
1157 'description' => 'Enable TikTok-specific optimizations',
1158 'default' => false
1159 ],
1160 'tiktok_verification' => [
1161 'type' => 'string',
1162 'title' => 'TikTok Site Verification',
1163 'description' => 'TikTok for Business verification code',
1164 'pattern' => '^[a-zA-Z0-9_-]{20,}$',
1165 'default' => ''
1166 ],
1167 'enable_youtube' => [
1168 'type' => 'boolean',
1169 'title' => 'Enable YouTube',
1170 'description' => 'Enable YouTube-specific optimizations',
1171 'default' => false
1172 ],
1173 'youtube_channel_id' => [
1174 'type' => 'string',
1175 'title' => 'YouTube Channel ID',
1176 'description' => 'YouTube channel ID for content attribution',
1177 'pattern' => '^UC[a-zA-Z0-9_-]{22}$',
1178 'default' => ''
1179 ],
1180 'enable_whatsapp' => [
1181 'type' => 'boolean',
1182 'title' => 'Enable WhatsApp Business',
1183 'description' => 'Enable WhatsApp Business optimizations',
1184 'default' => false
1185 ],
1186 'whatsapp_business_id' => [
1187 'type' => 'string',
1188 'title' => 'WhatsApp Business ID',
1189 'description' => 'WhatsApp Business account ID',
1190 'pattern' => '^[0-9]{10,15}$',
1191 'default' => ''
1192 ],
1193 'auto_generate_descriptions' => [
1194 'type' => 'boolean',
1195 'title' => 'Auto-generate Descriptions',
1196 'description' => 'Automatically generate descriptions from content',
1197 'default' => true
1198 ],
1199 'fallback_to_excerpt' => [
1200 'type' => 'boolean',
1201 'title' => 'Fallback to Excerpt',
1202 'description' => 'Use post excerpt as fallback for descriptions',
1203 'default' => true
1204 ],
1205 'strip_html_tags' => [
1206 'type' => 'boolean',
1207 'title' => 'Strip HTML Tags',
1208 'description' => 'Remove HTML tags from generated descriptions',
1209 'default' => true
1210 ],
1211 'max_description_length' => [
1212 'type' => 'integer',
1213 'title' => 'Max Description Length',
1214 'description' => 'Maximum length for generated descriptions',
1215 'minimum' => 50,
1216 'maximum' => 300,
1217 'default' => 160
1218 ],
1219 'custom_og_tags' => [
1220 'type' => 'object',
1221 'title' => 'Custom Open Graph Tags',
1222 'description' => 'Additional custom Open Graph meta tags',
1223 'default' => []
1224 ],
1225 'image_optimization' => [
1226 'type' => 'boolean',
1227 'title' => 'Enable Image Optimization',
1228 'description' => 'Optimize images for social media platforms',
1229 'default' => true
1230 ],
1231 'auto_generate' => [
1232 'type' => 'boolean',
1233 'title' => 'Auto-generate Tags',
1234 'description' => 'Automatically generate social meta tags from content',
1235 'default' => true
1236 ]
1237 ];
1238 }
1239
1240 /**
1241 * Extract content data for social meta generation
1242 *
1243 * @since 1.0.0
1244 *
1245 * @param string $context_type The context type
1246 * @param int|null $context_id Optional. Context ID
1247 * @param array $settings Social meta settings
1248 * @return array Extracted content data
1249 */
1250 private function extract_social_content_data(string $context_type, ?int $context_id, array $settings): array {
1251 $data = [
1252 'title' => '',
1253 'description' => '',
1254 'url' => '',
1255 'image' => '',
1256 'twitter_image' => '', // Separate field for Twitter-specific images
1257 'type' => 'website',
1258 'author' => [],
1259 'published_time' => '',
1260 'modified_time' => '',
1261 'site_name' => $settings['og_site_name'] ?? get_bloginfo('name')
1262 ];
1263
1264 if ($context_type === 'site') {
1265 // Site-wide data with Social Media tab settings priority
1266 $data['title'] = $settings['og_site_name'] ?? get_bloginfo('name');
1267 $data['description'] = $settings['og_description'] ?? get_bloginfo('description');
1268 $data['url'] = home_url();
1269 $data['type'] = $settings['og_type'] ?? 'website';
1270 $data['locale'] = $settings['og_locale'] ?? null;
1271
1272 // Set Open Graph image with proper fallback
1273 $data['image'] = $this->get_og_image_for_context($settings, null);
1274
1275 // Set Twitter image with proper fallback
1276 $data['twitter_image'] = $this->get_twitter_image_for_context($settings, null);
1277 } elseif ($context_id && in_array($context_type, ['post', 'page', 'product'], true)) {
1278 // Post-specific data
1279 $post = get_post($context_id);
1280 if ($post) {
1281 $data['title'] = get_the_title($post);
1282 $data['description'] = $this->get_social_description($post);
1283 $data['url'] = get_permalink($post);
1284 $data['type'] = $settings['og_type'] ?? $this->determine_og_type($context_type, []);
1285 $data['published_time'] = get_the_date('c', $post);
1286 $data['modified_time'] = get_the_modified_date('c', $post);
1287
1288 // Author data
1289 $author_id = $post->post_author;
1290 $data['author'] = [
1291 'name' => get_the_author_meta('display_name', $author_id),
1292 'url' => get_author_posts_url($author_id),
1293 'twitter' => get_the_author_meta('twitter', $author_id)
1294 ];
1295
1296 // Set Open Graph image with proper fallback
1297 $data['image'] = $this->get_og_image_for_context($settings, $post);
1298
1299 // Set Twitter image with proper fallback
1300 $data['twitter_image'] = $this->get_twitter_image_for_context($settings, $post);
1301 }
1302 }
1303
1304 return $data;
1305 }
1306
1307 /**
1308 * Get Open Graph image for context with proper fallback
1309 *
1310 * @since 1.0.0
1311 *
1312 * @param array $settings Social meta settings
1313 * @param \WP_Post|null $post Optional. Post object for post-specific images
1314 * @return string Image URL or empty string
1315 */
1316 private function get_og_image_for_context(array $settings, ?\WP_Post $post = null): string {
1317 // For posts, check featured image first
1318 if ($post) {
1319 $image_id = get_post_thumbnail_id($post);
1320 if ($image_id) {
1321 $image_data = wp_get_attachment_image_src($image_id, 'large');
1322 if (!empty($image_data[0])) {
1323 return $image_data[0];
1324 }
1325 }
1326 }
1327
1328 // Fallback to configured Open Graph image
1329 if (!empty($settings['default_og_image'])) {
1330 return $settings['default_og_image'];
1331 }
1332
1333 // Final fallback to generic default image
1334 if (!empty($settings['default_image'])) {
1335 return $settings['default_image'];
1336 }
1337
1338 return '';
1339 }
1340
1341 /**
1342 * Get Twitter image for context with proper fallback
1343 *
1344 * @since 1.0.0
1345 *
1346 * @param array $settings Social meta settings
1347 * @param \WP_Post|null $post Optional. Post object for post-specific images
1348 * @return string Image URL or empty string
1349 */
1350 private function get_twitter_image_for_context(array $settings, ?\WP_Post $post = null): string {
1351 // For posts, check for post-specific Twitter image meta first (if implemented)
1352 if ($post) {
1353 // Check for post-specific Twitter image meta (future enhancement)
1354 $post_twitter_image = get_post_meta($post->ID, '_thinkrank_twitter_image', true);
1355 if (!empty($post_twitter_image)) {
1356 return $post_twitter_image;
1357 }
1358
1359 // Check featured image as fallback for posts
1360 $image_id = get_post_thumbnail_id($post);
1361 if ($image_id) {
1362 $image_data = wp_get_attachment_image_src($image_id, 'large');
1363 if (!empty($image_data[0])) {
1364 return $image_data[0];
1365 }
1366 }
1367 }
1368
1369 // Prioritize Twitter-specific default image
1370 if (!empty($settings['default_twitter_image'])) {
1371 return $settings['default_twitter_image'];
1372 }
1373
1374 // Fallback to Open Graph default image
1375 if (!empty($settings['default_og_image'])) {
1376 return $settings['default_og_image'];
1377 }
1378
1379 // Final fallback to generic default image
1380 if (!empty($settings['default_image'])) {
1381 return $settings['default_image'];
1382 }
1383
1384 return '';
1385 }
1386
1387 /**
1388 * Get social media description for post
1389 *
1390 * @since 1.0.0
1391 *
1392 * @param \WP_Post $post Post object
1393 * @return string Social media description
1394 */
1395 private function get_social_description(\WP_Post $post): string {
1396 // Try excerpt first
1397 $description = get_the_excerpt($post);
1398
1399 // If no excerpt, generate from content
1400 if (empty($description)) {
1401 $content = wp_strip_all_tags($post->post_content);
1402 $description = wp_trim_words($content, 30, '...');
1403 }
1404
1405 // If still empty, use site description
1406 if (empty($description)) {
1407 $description = get_bloginfo('description');
1408 }
1409
1410 return $description;
1411 }
1412
1413 /**
1414 * Determine Open Graph type based on context
1415 *
1416 * @since 1.0.0
1417 *
1418 * @param string $context Context type
1419 * @param array $data Content data
1420 * @return string OG type
1421 */
1422 private function determine_og_type(string $context, array $data): string {
1423 switch ($context) {
1424 case 'post':
1425 return 'article';
1426 case 'product':
1427 return 'product';
1428 case 'page':
1429 // Check if it's a video or music page
1430 if (!empty($data['content'])) {
1431 if (preg_match('/\b(video|youtube|vimeo)\b/i', $data['content'])) {
1432 return 'video';
1433 }
1434 if (preg_match('/\b(music|song|album|artist)\b/i', $data['content'])) {
1435 return 'music';
1436 }
1437 }
1438 return 'website';
1439 case 'site':
1440 default:
1441 return 'website';
1442 }
1443 }
1444
1445 /**
1446 * Determine Twitter Card type based on content
1447 *
1448 * @since 1.0.0
1449 *
1450 * @param array $data Content data
1451 * @param string $context Context type
1452 * @return string Twitter Card type
1453 */
1454 private function determine_twitter_card_type(array $data, string $context): string {
1455 // Check for video content
1456 if (!empty($data['content']) && preg_match('/\b(video|youtube|vimeo)\b/i', $data['content'])) {
1457 return 'player';
1458 }
1459
1460 // Check for app content
1461 if (!empty($data['content']) && preg_match('/\b(app|download|install)\b/i', $data['content'])) {
1462 return 'app';
1463 }
1464
1465 // Use large image if image is available, otherwise summary
1466 return !empty($data['image']) ? 'summary_large_image' : 'summary';
1467 }
1468
1469 /**
1470 * Optimize title for platform requirements
1471 *
1472 * @since 1.0.0
1473 *
1474 * @param string $title Original title
1475 * @param string $platform Target platform
1476 * @return string Optimized title
1477 */
1478 private function optimize_title_for_platform(string $title, string $platform): string {
1479 if (empty($title)) {
1480 return get_bloginfo('name');
1481 }
1482
1483 $max_length = $this->supported_platforms[$platform]['title_max_length'] ?? 60;
1484
1485 if (strlen($title) <= $max_length) {
1486 return $title;
1487 }
1488
1489 // Truncate at word boundary
1490 $truncated = wp_trim_words($title, 10, '');
1491 if (strlen($truncated) <= $max_length) {
1492 return $truncated;
1493 }
1494
1495 // Hard truncate if necessary
1496 return substr($title, 0, $max_length - 3) . '...';
1497 }
1498
1499 /**
1500 * Optimize description for platform requirements
1501 *
1502 * @since 1.0.0
1503 *
1504 * @param string $description Original description
1505 * @param string $platform Target platform
1506 * @return string Optimized description
1507 */
1508 private function optimize_description_for_platform(string $description, string $platform): string {
1509 if (empty($description)) {
1510 return get_bloginfo('description');
1511 }
1512
1513 $max_length = $this->supported_platforms[$platform]['description_max_length'] ?? 160;
1514
1515 if (strlen($description) <= $max_length) {
1516 return $description;
1517 }
1518
1519 // Truncate at word boundary
1520 $truncated = wp_trim_words($description, 25, '');
1521 if (strlen($truncated) <= $max_length) {
1522 return $truncated;
1523 }
1524
1525 // Hard truncate if necessary
1526 return substr($description, 0, $max_length - 3) . '...';
1527 }
1528
1529 /**
1530 * Optimize image for platform requirements
1531 *
1532 * @since 1.0.0
1533 *
1534 * @param string $image_url Image URL
1535 * @param string $platform Target platform
1536 * @return array Optimized image data
1537 */
1538 private function optimize_image_for_platform(string $image_url, string $platform): array {
1539 $image_data = [
1540 'url' => $image_url,
1541 'width' => '',
1542 'height' => '',
1543 'alt' => '',
1544 'type' => '',
1545 'valid' => false,
1546 'warnings' => []
1547 ];
1548
1549 if (empty($image_url)) {
1550 return $image_data;
1551 }
1552
1553 // Get image metadata
1554 $attachment_id = attachment_url_to_postid($image_url);
1555 if ($attachment_id) {
1556 $image_meta = wp_get_attachment_metadata($attachment_id);
1557 $image_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
1558 $mime_type = get_post_mime_type($attachment_id);
1559
1560 if ($image_meta && isset($image_meta['width'], $image_meta['height'])) {
1561 $image_data['width'] = $image_meta['width'];
1562 $image_data['height'] = $image_meta['height'];
1563 $image_data['alt'] = $image_alt ?: '';
1564 $image_data['type'] = $mime_type ?: '';
1565
1566 // Validate against platform requirements
1567 $platform_spec = $this->supported_platforms[$platform] ?? [];
1568 $min_width = $platform_spec['image_min_width'] ?? 300;
1569 $min_height = $platform_spec['image_min_height'] ?? 200;
1570
1571 if ($image_meta['width'] >= $min_width && $image_meta['height'] >= $min_height) {
1572 $image_data['valid'] = true;
1573 } else {
1574 $image_data['warnings'][] = "Image dimensions ({$image_meta['width']}x{$image_meta['height']}) are below recommended minimum ({$min_width}x{$min_height}) for {$platform}";
1575 }
1576
1577 // Check aspect ratio if specified
1578 if (isset($platform_spec['image_recommended_ratio'])) {
1579 $actual_ratio = $image_meta['width'] / $image_meta['height'];
1580 $recommended_ratio = $platform_spec['image_recommended_ratio'];
1581 $ratio_tolerance = 0.1;
1582
1583 if (abs($actual_ratio - $recommended_ratio) > $ratio_tolerance) {
1584 $image_data['warnings'][] = sprintf(
1585 "Image aspect ratio (%.2f) differs from recommended ratio (%.2f) for %s",
1586 $actual_ratio,
1587 $recommended_ratio,
1588 $platform
1589 );
1590 }
1591 }
1592 }
1593 }
1594
1595 return $image_data;
1596 }
1597
1598 /**
1599 * Get default social image
1600 *
1601 * @since 1.0.0
1602 *
1603 * @return string Default social image URL
1604 */
1605 private function get_default_social_image(): string {
1606 // Try custom logo first
1607 $custom_logo_id = get_theme_mod('custom_logo');
1608 if ($custom_logo_id) {
1609 $logo_data = wp_get_attachment_image_src($custom_logo_id, 'large');
1610 if ($logo_data) {
1611 return $logo_data[0];
1612 }
1613 }
1614
1615 // Try site icon
1616 $site_icon_id = get_option('site_icon');
1617 if ($site_icon_id) {
1618 $icon_data = wp_get_attachment_image_src($site_icon_id, 'large');
1619 if ($icon_data) {
1620 return $icon_data[0];
1621 }
1622 }
1623
1624 return '';
1625 }
1626
1627 /**
1628 * Get Open Graph locale
1629 *
1630 * @since 1.0.0
1631 *
1632 * @return string OG locale
1633 */
1634 private function get_og_locale(): string {
1635 $locale = get_locale();
1636
1637 // Convert WordPress locale to OG locale format
1638 $og_locale_map = [
1639 'en_US' => 'en_US',
1640 'en_GB' => 'en_GB',
1641 'es_ES' => 'es_ES',
1642 'fr_FR' => 'fr_FR',
1643 'de_DE' => 'de_DE',
1644 'it_IT' => 'it_IT',
1645 'pt_BR' => 'pt_BR',
1646 'ru_RU' => 'ru_RU',
1647 'ja' => 'ja_JP',
1648 'zh_CN' => 'zh_CN',
1649 'ko_KR' => 'ko_KR'
1650 ];
1651
1652 return $og_locale_map[$locale] ?? 'en_US';
1653 }
1654
1655 /**
1656 * Get current URL
1657 *
1658 * @since 1.0.0
1659 *
1660 * @return string Current URL
1661 */
1662 private function get_current_url(): string {
1663 if (is_admin()) {
1664 return home_url();
1665 }
1666
1667 global $wp;
1668 return home_url(add_query_arg([], $wp->request));
1669 }
1670
1671 /**
1672 * Get Twitter site handle
1673 *
1674 * @since 1.0.0
1675 *
1676 * @return string Twitter site handle
1677 */
1678 private function get_twitter_site_handle(): string {
1679 $settings = $this->get_settings('site');
1680 $username = $settings['twitter_username'] ?? '';
1681
1682 if (empty($username)) {
1683 return '';
1684 }
1685
1686 // Ensure username starts with @ for meta tag output
1687 return '@' . ltrim($username, '@');
1688 }
1689
1690 /**
1691 * Get Twitter creator handle
1692 *
1693 * @since 1.0.0
1694 *
1695 * @return string Twitter creator handle
1696 */
1697 private function get_twitter_creator_handle(): string {
1698 $settings = $this->get_settings('site');
1699 $creator = $settings['twitter_creator'] ?? '';
1700
1701 if (empty($creator)) {
1702 return '';
1703 }
1704
1705 // Ensure creator starts with @ for meta tag output
1706 return '@' . ltrim($creator, '@');
1707 }
1708
1709 /**
1710 * Add context-specific Open Graph tags
1711 *
1712 * @since 1.0.0
1713 *
1714 * @param array $og_tags OG tags array
1715 * @param string $context Context type
1716 * @param array $data Content data
1717 * @param string $og_type OG type
1718 * @return array Updated OG tags
1719 */
1720 private function add_context_specific_og_tags(array $og_tags, string $context, array $data, string $og_type): array {
1721 switch ($og_type) {
1722 case 'article':
1723 if (!empty($data['author']['name'])) {
1724 $og_tags['article:author'] = $data['author']['name'];
1725 }
1726 if (!empty($data['published_time'])) {
1727 $og_tags['article:published_time'] = $data['published_time'];
1728 }
1729 if (!empty($data['modified_time'])) {
1730 $og_tags['article:modified_time'] = $data['modified_time'];
1731 }
1732 break;
1733 case 'product':
1734 // Product-specific tags would be added here
1735 // This could be extended with price, availability, etc.
1736 break;
1737 }
1738
1739 // Add local business Open Graph tags if business data is available
1740 $og_tags = $this->add_local_business_og_tags($og_tags, $data);
1741
1742 return $og_tags;
1743 }
1744
1745 /**
1746 * Add local business Open Graph tags
1747 *
1748 * @since 1.0.0
1749 *
1750 * @param array $og_tags OG tags array
1751 * @param array $data Content data
1752 * @return array Updated OG tags with local business information
1753 */
1754 private function add_local_business_og_tags(array $og_tags, array $data): array {
1755 // Get business data from Site Identity settings
1756 $business_data = $this->get_local_business_data();
1757
1758 if (empty($business_data) || empty($business_data['business_name'])) {
1759 return $og_tags;
1760 }
1761
1762 // Add business contact data Open Graph tags
1763 if (!empty($business_data['business_address'])) {
1764 $og_tags['business:contact_data:street_address'] = $business_data['business_address'];
1765 }
1766
1767 if (!empty($business_data['business_city'])) {
1768 $og_tags['business:contact_data:locality'] = $business_data['business_city'];
1769 }
1770
1771 if (!empty($business_data['business_state'])) {
1772 $og_tags['business:contact_data:region'] = $business_data['business_state'];
1773 }
1774
1775 if (!empty($business_data['business_postal_code'])) {
1776 $og_tags['business:contact_data:postal_code'] = $business_data['business_postal_code'];
1777 }
1778
1779 if (!empty($business_data['business_country'])) {
1780 $og_tags['business:contact_data:country_name'] = $business_data['business_country'];
1781 }
1782
1783 if (!empty($business_data['business_phone'])) {
1784 $og_tags['business:contact_data:phone_number'] = $business_data['business_phone'];
1785 }
1786
1787 if (!empty($business_data['business_email'])) {
1788 $og_tags['business:contact_data:email'] = $business_data['business_email'];
1789 }
1790
1791 // Add business hours if available
1792 if (!empty($business_data['business_hours']) && is_array($business_data['business_hours'])) {
1793 $formatted_hours = $this->format_business_hours_for_og($business_data['business_hours']);
1794 if (!empty($formatted_hours)) {
1795 $og_tags['business:hours'] = $formatted_hours;
1796 }
1797 }
1798
1799 // Add business website
1800 if (!empty($business_data['business_website'])) {
1801 $og_tags['business:contact_data:website'] = $business_data['business_website'];
1802 }
1803
1804 // Add coordinates if available
1805 if (!empty($business_data['business_latitude']) && !empty($business_data['business_longitude'])) {
1806 $og_tags['place:location:latitude'] = $business_data['business_latitude'];
1807 $og_tags['place:location:longitude'] = $business_data['business_longitude'];
1808 }
1809
1810 return $og_tags;
1811 }
1812
1813 /**
1814 * Get local business data from Site Identity settings
1815 *
1816 * @since 1.0.0
1817 *
1818 * @return array Business data array
1819 */
1820 private function get_local_business_data(): array {
1821 // Try to get Site Identity Manager
1822 if (!class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) {
1823 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-site-identity-manager.php';
1824 }
1825
1826 $site_identity_manager = new \ThinkRank\SEO\Site_Identity_Manager();
1827 $settings = $site_identity_manager->get_settings('site');
1828
1829 // Only return data if local SEO is enabled
1830 if (empty($settings['local_seo_enabled'])) {
1831 return [];
1832 }
1833
1834 return [
1835 'business_name' => $settings['business_name'] ?? '',
1836 'business_address' => $settings['business_address'] ?? '',
1837 'business_city' => $settings['business_city'] ?? '',
1838 'business_state' => $settings['business_state'] ?? '',
1839 'business_postal_code' => $settings['business_postal_code'] ?? '',
1840 'business_country' => $settings['business_country'] ?? '',
1841 'business_phone' => $settings['business_phone'] ?? '',
1842 'business_email' => $settings['business_email'] ?? '',
1843 'business_hours' => $settings['business_hours'] ?? [],
1844 'business_website' => $settings['business_website'] ?? home_url(),
1845 'business_latitude' => $settings['business_latitude'] ?? '',
1846 'business_longitude' => $settings['business_longitude'] ?? ''
1847 ];
1848 }
1849
1850 /**
1851 * Format business hours for Open Graph tags
1852 *
1853 * @since 1.0.0
1854 *
1855 * @param array $business_hours Business hours array
1856 * @return string Formatted hours string
1857 */
1858 private function format_business_hours_for_og(array $business_hours): string {
1859 $formatted_days = [];
1860
1861 $day_names = [
1862 'monday' => 'Monday',
1863 'tuesday' => 'Tuesday',
1864 'wednesday' => 'Wednesday',
1865 'thursday' => 'Thursday',
1866 'friday' => 'Friday',
1867 'saturday' => 'Saturday',
1868 'sunday' => 'Sunday'
1869 ];
1870
1871 foreach ($day_names as $day => $display_name) {
1872 if (isset($business_hours[$day]) && !empty($business_hours[$day])) {
1873 $day_data = $business_hours[$day];
1874
1875 if (!empty($day_data['closed']) || empty($day_data['open']) || empty($day_data['close'])) {
1876 $formatted_days[] = $display_name . ': Closed';
1877 } else {
1878 $formatted_days[] = $display_name . ': ' . $day_data['open'] . ' - ' . $day_data['close'];
1879 }
1880 }
1881 }
1882
1883 return implode('; ', $formatted_days);
1884 }
1885
1886 /**
1887 * Apply platform-specific optimizations
1888 *
1889 * @since 1.0.0
1890 *
1891 * @param array $og_tags OG tags array
1892 * @param string $platform Target platform
1893 * @param array $data Content data
1894 * @return array Optimized OG tags
1895 */
1896 private function apply_platform_specific_optimizations(array $og_tags, string $platform, array $data): array {
1897 $settings = $this->get_settings('site');
1898
1899 switch ($platform) {
1900 case 'linkedin':
1901 // LinkedIn prefers professional content
1902 if ($settings['enable_linkedin'] ?? false) {
1903 if (isset($og_tags['og:description'])) {
1904 $og_tags['og:description'] = $this->make_description_professional($og_tags['og:description']);
1905 }
1906 // Add LinkedIn-specific type if appropriate
1907 if ($og_tags['og:type'] === 'article') {
1908 $og_tags['article:author'] = $data['author'] ?? '';
1909 }
1910 }
1911 break;
1912
1913 case 'pinterest':
1914 // Pinterest prefers descriptive content
1915 if ($settings['enable_pinterest'] ?? false) {
1916 if (isset($og_tags['og:description'])) {
1917 $og_tags['og:description'] = $this->make_description_descriptive($og_tags['og:description']);
1918 }
1919 // Pinterest prefers article type for rich pins
1920 if (in_array($og_tags['og:type'], ['website', 'blog'])) {
1921 $og_tags['og:type'] = 'article';
1922 }
1923 }
1924 break;
1925
1926 case 'instagram':
1927 // Instagram optimizations
1928 if ($settings['enable_instagram'] ?? false) {
1929 // Instagram prefers square or vertical images
1930 if (isset($og_tags['og:description'])) {
1931 $og_tags['og:description'] = $this->make_description_engaging($og_tags['og:description']);
1932 }
1933 }
1934 break;
1935
1936 case 'tiktok':
1937 // TikTok optimizations
1938 if ($settings['enable_tiktok'] ?? false) {
1939 // TikTok prefers short, catchy descriptions
1940 if (isset($og_tags['og:description'])) {
1941 $og_tags['og:description'] = $this->make_description_catchy($og_tags['og:description']);
1942 }
1943 }
1944 break;
1945 }
1946
1947 return $og_tags;
1948 }
1949
1950 /**
1951 * Make description more professional for LinkedIn
1952 *
1953 * @since 1.0.0
1954 *
1955 * @param string $description Original description
1956 * @return string Professional description
1957 */
1958 private function make_description_professional(string $description): string {
1959 // Remove casual language and emojis, add professional tone
1960 $description = preg_replace('/[^\w\s\.,!?-]/', '', $description);
1961
1962 // Add professional keywords if not present
1963 $professional_words = ['insights', 'expertise', 'professional', 'industry', 'business'];
1964 if (!preg_match('/\b(' . implode('|', $professional_words) . ')\b/i', $description)) {
1965 $description = 'Professional insights: ' . $description;
1966 }
1967
1968 return $description;
1969 }
1970
1971 /**
1972 * Make description more descriptive for Pinterest
1973 *
1974 * @since 1.0.0
1975 *
1976 * @param string $description Original description
1977 * @return string Descriptive description
1978 */
1979 private function make_description_descriptive(string $description): string {
1980 // Pinterest users love detailed, searchable descriptions
1981 $descriptive_words = ['discover', 'explore', 'learn', 'find', 'ideas'];
1982
1983 if (!preg_match('/\b(' . implode('|', $descriptive_words) . ')\b/i', $description)) {
1984 $description = 'Discover ' . lcfirst($description);
1985 }
1986
1987 return $description;
1988 }
1989
1990 /**
1991 * Make description engaging for Instagram
1992 *
1993 * @since 1.0.0
1994 *
1995 * @param string $description Original description
1996 * @return string Engaging description
1997 */
1998 private function make_description_engaging(string $description): string {
1999 // Instagram prefers engaging, visual language
2000 $engaging_words = ['amazing', 'stunning', 'beautiful', 'incredible', 'inspiring'];
2001
2002 if (!preg_match('/\b(' . implode('|', $engaging_words) . ')\b/i', $description)) {
2003 $description = '' . $description;
2004 }
2005
2006 return $description;
2007 }
2008
2009 /**
2010 * Make description catchy for TikTok
2011 *
2012 * @since 1.0.0
2013 *
2014 * @param string $description Original description
2015 * @return string Catchy description
2016 */
2017 private function make_description_catchy(string $description): string {
2018 // TikTok prefers short, catchy descriptions
2019 $catchy_words = ['viral', 'trending', 'must-see', 'epic', 'mind-blowing'];
2020
2021 // Limit to 100 characters for TikTok
2022 if (strlen($description) > 100) {
2023 $description = substr($description, 0, 97) . '...';
2024 }
2025
2026 if (!preg_match('/\b(' . implode('|', $catchy_words) . ')\b/i', $description)) {
2027 $description = '🔥 ' . $description;
2028 }
2029
2030 return $description;
2031 }
2032
2033 /**
2034 * Add Twitter Card specific tags
2035 *
2036 * @since 1.0.0
2037 *
2038 * @param array $twitter_tags Twitter tags array
2039 * @param string $card_type Card type
2040 * @param array $data Content data
2041 * @param string $context Context type
2042 * @return array Updated Twitter tags
2043 */
2044 private function add_twitter_card_specific_tags(array $twitter_tags, string $card_type, array $data, string $context): array {
2045 switch ($card_type) {
2046 case 'player':
2047 if (!empty($data['video_url'])) {
2048 $twitter_tags['twitter:player'] = $data['video_url'];
2049 $twitter_tags['twitter:player:width'] = $data['video_width'] ?? '1280';
2050 $twitter_tags['twitter:player:height'] = $data['video_height'] ?? '720';
2051 }
2052 break;
2053 case 'app':
2054 if (!empty($data['app_name'])) {
2055 $twitter_tags['twitter:app:name:iphone'] = $data['app_name'];
2056 $twitter_tags['twitter:app:name:android'] = $data['app_name'];
2057 }
2058 if (!empty($data['app_id'])) {
2059 $twitter_tags['twitter:app:id:iphone'] = $data['app_id'];
2060 $twitter_tags['twitter:app:id:android'] = $data['app_id'];
2061 }
2062 break;
2063 }
2064
2065 return $twitter_tags;
2066 }
2067
2068 /**
2069 * Generate Open Graph preview
2070 *
2071 * @since 1.0.0
2072 *
2073 * @param array $og_tags OG tags
2074 * @param string $platform Platform name
2075 * @param array $preview Preview array
2076 * @return array Updated preview
2077 */
2078 private function generate_og_preview(array $og_tags, string $platform, array $preview): array {
2079 $preview['title'] = $og_tags['og:title'] ?? '';
2080 $preview['description'] = $og_tags['og:description'] ?? '';
2081 $preview['image'] = $og_tags['og:image'] ?? '';
2082 $preview['preview_url'] = $og_tags['og:url'] ?? '';
2083
2084 // Validate required fields
2085 $required_fields = ['og:title', 'og:type', 'og:image', 'og:url'];
2086 $missing_fields = [];
2087
2088 foreach ($required_fields as $field) {
2089 if (empty($og_tags[$field])) {
2090 $missing_fields[] = $field;
2091 }
2092 }
2093
2094 if (empty($missing_fields)) {
2095 $preview['valid'] = true;
2096 } else {
2097 $preview['warnings'][] = 'Missing required fields: ' . implode(', ', $missing_fields);
2098 }
2099
2100 // Platform-specific validation
2101 $platform_spec = $this->supported_platforms[$platform] ?? [];
2102 if (isset($platform_spec['title_max_length']) && strlen($preview['title']) > $platform_spec['title_max_length']) {
2103 $preview['warnings'][] = "Title exceeds {$platform} maximum length of {$platform_spec['title_max_length']} characters";
2104 }
2105
2106 return $preview;
2107 }
2108
2109 /**
2110 * Generate Twitter preview
2111 *
2112 * @since 1.0.0
2113 *
2114 * @param array $twitter_tags Twitter tags
2115 * @param array $preview Preview array
2116 * @return array Updated preview
2117 */
2118 private function generate_twitter_preview(array $twitter_tags, array $preview): array {
2119 $preview['title'] = $twitter_tags['twitter:title'] ?? '';
2120 $preview['description'] = $twitter_tags['twitter:description'] ?? '';
2121 $preview['image'] = $twitter_tags['twitter:image'] ?? '';
2122
2123 // Validate required fields
2124 $required_fields = ['twitter:card', 'twitter:title'];
2125 $missing_fields = [];
2126
2127 foreach ($required_fields as $field) {
2128 if (empty($twitter_tags[$field])) {
2129 $missing_fields[] = $field;
2130 }
2131 }
2132
2133 if (empty($missing_fields)) {
2134 $preview['valid'] = true;
2135 } else {
2136 $preview['warnings'][] = 'Missing required fields: ' . implode(', ', $missing_fields);
2137 }
2138
2139 // Twitter-specific validation
2140 $twitter_spec = $this->supported_platforms['twitter'];
2141 if (strlen($preview['title']) > $twitter_spec['title_max_length']) {
2142 $preview['warnings'][] = "Title exceeds Twitter maximum length of {$twitter_spec['title_max_length']} characters";
2143 }
2144
2145 return $preview;
2146 }
2147
2148 /**
2149 * Generate Pinterest preview
2150 *
2151 * @since 1.0.0
2152 *
2153 * @param array $og_tags OG tags
2154 * @param array $preview Preview array
2155 * @return array Updated preview
2156 */
2157 private function generate_pinterest_preview(array $og_tags, array $preview): array {
2158 $preview['title'] = $og_tags['og:title'] ?? '';
2159 $preview['description'] = $og_tags['og:description'] ?? '';
2160 $preview['image'] = $og_tags['og:image'] ?? '';
2161 $preview['preview_url'] = $og_tags['og:url'] ?? '';
2162
2163 // Validate required fields for Pinterest
2164 $required_fields = ['og:title', 'og:image', 'og:url'];
2165 $missing_fields = [];
2166
2167 foreach ($required_fields as $field) {
2168 if (empty($og_tags[$field])) {
2169 $missing_fields[] = $field;
2170 }
2171 }
2172
2173 if (empty($missing_fields)) {
2174 $preview['valid'] = true;
2175 } else {
2176 $preview['warnings'][] = 'Missing required fields: ' . implode(', ', $missing_fields);
2177 }
2178
2179 // Pinterest-specific validation and suggestions
2180 $pinterest_spec = $this->supported_platforms['pinterest'] ?? [];
2181
2182 // Title length validation
2183 if (isset($pinterest_spec['title_max_length']) && strlen($preview['title']) > $pinterest_spec['title_max_length']) {
2184 $preview['warnings'][] = "Title exceeds Pinterest maximum length of {$pinterest_spec['title_max_length']} characters";
2185 }
2186
2187 // Description length validation
2188 if (isset($pinterest_spec['description_max_length']) && strlen($preview['description']) > $pinterest_spec['description_max_length']) {
2189 $preview['warnings'][] = "Description exceeds Pinterest maximum length of {$pinterest_spec['description_max_length']} characters";
2190 }
2191
2192 // Image dimension suggestions for Pinterest
2193 if (!empty($preview['image'])) {
2194 $image_data = $this->optimize_image_for_platform($preview['image'], 'pinterest');
2195 if (!empty($image_data['width']) && !empty($image_data['height'])) {
2196 $aspect_ratio = $image_data['width'] / $image_data['height'];
2197
2198 // Pinterest prefers vertical images (2:3 ratio is optimal)
2199 if ($aspect_ratio > 1) {
2200 $preview['suggestions'][] = 'Pinterest performs better with vertical images (2:3 aspect ratio recommended)';
2201 } elseif ($aspect_ratio < 0.6) {
2202 $preview['suggestions'][] = 'Image is very tall - consider a 2:3 aspect ratio for optimal Pinterest performance';
2203 }
2204
2205 // Minimum size recommendations
2206 if ($image_data['width'] < 600) {
2207 $preview['suggestions'][] = 'Pinterest recommends images at least 600px wide for better quality';
2208 }
2209 }
2210 }
2211
2212 return $preview;
2213 }
2214
2215 /**
2216 * Generate platform-specific meta tags
2217 *
2218 * @since 1.0.0
2219 *
2220 * @param array $settings Settings array
2221 * @return array Platform-specific meta tags
2222 */
2223 private function generate_platform_meta_tags(array $settings): array {
2224 $platform_tags = [];
2225
2226 // Facebook meta tags
2227 if (!empty($settings['facebook_app_id'])) {
2228 $platform_tags['fb:app_id'] = $settings['facebook_app_id'];
2229 }
2230
2231 if (!empty($settings['facebook_admins'])) {
2232 $platform_tags['fb:admins'] = $settings['facebook_admins'];
2233 }
2234
2235 // Pinterest site verification
2236 if (!empty($settings['pinterest_site_verification'])) {
2237 $platform_tags['pinterest-site-verification'] = $settings['pinterest_site_verification'];
2238 }
2239
2240 // Instagram verification (if enabled)
2241 if (!empty($settings['instagram_verification'])) {
2242 $platform_tags['instagram-site-verification'] = $settings['instagram_verification'];
2243 }
2244
2245 // TikTok verification (if enabled)
2246 if (!empty($settings['tiktok_verification'])) {
2247 $platform_tags['tiktok-site-verification'] = $settings['tiktok_verification'];
2248 }
2249
2250 // YouTube channel verification (if enabled)
2251 if (!empty($settings['youtube_channel_id'])) {
2252 $platform_tags['youtube-channel-id'] = $settings['youtube_channel_id'];
2253 }
2254
2255 // WhatsApp Business verification (if enabled)
2256 if (!empty($settings['whatsapp_business_id'])) {
2257 $platform_tags['whatsapp-business-id'] = $settings['whatsapp_business_id'];
2258 }
2259
2260 return $platform_tags;
2261 }
2262
2263 /**
2264 * Convert OG, Twitter, and Platform tags to HTML meta tags
2265 *
2266 * @since 1.0.0
2267 *
2268 * @param array $og_tags Open Graph tags
2269 * @param array $twitter_tags Twitter tags
2270 * @param array $platform_tags Platform-specific tags
2271 * @return array HTML meta tags
2272 */
2273 private function convert_to_meta_tags(array $og_tags, array $twitter_tags, array $platform_tags = []): array {
2274 $meta_tags = [];
2275
2276 // Convert OG tags
2277 foreach ($og_tags as $property => $content) {
2278 if (!empty($content)) {
2279 $meta_tags[] = [
2280 'property' => esc_attr($property),
2281 'content' => esc_attr($content)
2282 ];
2283 }
2284 }
2285
2286 // Convert Twitter tags
2287 foreach ($twitter_tags as $name => $content) {
2288 if (!empty($content)) {
2289 $meta_tags[] = [
2290 'name' => esc_attr($name),
2291 'content' => esc_attr($content)
2292 ];
2293 }
2294 }
2295
2296 // Convert Platform tags
2297 foreach ($platform_tags as $name => $content) {
2298 if (!empty($content)) {
2299 // Determine if it should be property or name attribute
2300 if (strpos($name, 'fb:') === 0) {
2301 // Facebook tags use property attribute
2302 $meta_tags[] = [
2303 'property' => esc_attr($name),
2304 'content' => esc_attr($content)
2305 ];
2306 } else {
2307 // Other platform tags use name attribute
2308 $meta_tags[] = [
2309 'name' => esc_attr($name),
2310 'content' => esc_attr($content)
2311 ];
2312 }
2313 }
2314 }
2315
2316 return $meta_tags;
2317 }
2318
2319 /**
2320 * Validate social image
2321 *
2322 * @since 1.0.0
2323 *
2324 * @param string $image_url Image URL to validate
2325 * @return array Validation results
2326 */
2327 private function validate_social_image(string $image_url): array {
2328 $validation = [
2329 'valid' => false,
2330 'warnings' => [],
2331 'suggestions' => []
2332 ];
2333
2334 if (empty($image_url)) {
2335 $validation['warnings'][] = 'No image provided';
2336 return $validation;
2337 }
2338
2339 // Check if URL is valid
2340 if (!filter_var($image_url, FILTER_VALIDATE_URL)) {
2341 $validation['warnings'][] = 'Invalid image URL';
2342 return $validation;
2343 }
2344
2345 // Get image metadata if it's a local attachment
2346 $attachment_id = attachment_url_to_postid($image_url);
2347 if ($attachment_id) {
2348 $image_meta = wp_get_attachment_metadata($attachment_id);
2349 if ($image_meta && isset($image_meta['width'], $image_meta['height'])) {
2350 // Check minimum dimensions for major platforms
2351 $min_width = 600; // Facebook minimum
2352 $min_height = 315; // Facebook minimum
2353
2354 if ($image_meta['width'] >= $min_width && $image_meta['height'] >= $min_height) {
2355 $validation['valid'] = true;
2356 } else {
2357 $validation['warnings'][] = "Image dimensions ({$image_meta['width']}x{$image_meta['height']}) are below recommended minimum ({$min_width}x{$min_height})";
2358 }
2359
2360 // Check file size
2361 $file_path = get_attached_file($attachment_id);
2362 if ($file_path && file_exists($file_path)) {
2363 $file_size = filesize($file_path);
2364 $max_size = 5 * 1024 * 1024; // 5MB
2365
2366 if ($file_size > $max_size) {
2367 $validation['warnings'][] = 'Image file size exceeds 5MB, may not display on some platforms';
2368 }
2369 }
2370
2371 // Check aspect ratio
2372 $aspect_ratio = $image_meta['width'] / $image_meta['height'];
2373 if ($aspect_ratio < 1.5 || $aspect_ratio > 2.5) {
2374 $validation['suggestions'][] = 'Consider using an image with 1.91:1 aspect ratio for optimal display';
2375 }
2376 }
2377 } else {
2378 $validation['suggestions'][] = 'External images may not be optimized for social media platforms';
2379 }
2380
2381 return $validation;
2382 }
2383 }
2384