PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 All 49 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.0, at includes/seo/class-social-meta-manager.php

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