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-schema-management-system.php

class-schema-management-system.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-schema-management-system.php

2,005 lines 80.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Schema Management System Class
4 *
5 * Streamlined schema markup generation and management system with structured
6 * data creation, validation, and automated deployment. Implements 2025 Schema.org
7 * standards with clean separation of concerns.
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 use ThinkRank\Config\Schema_Settings_Config;
19
20 /**
21 * Schema Management System Class
22 *
23 * Provides streamlined schema markup management with generation, validation,
24 * rich snippets optimization, and automated deployment. Focuses on core
25 * structured data creation with clean separation of concerns.
26 *
27 * @since 1.0.0
28 */
29 class Schema_Management_System extends Abstract_SEO_Manager {
30
31 /**
32 * Schema.org types with 2025 specifications
33 *
34 * @since 1.0.0
35 * @var array
36 */
37 private array $schema_types = [
38 'Article' => [
39 'name' => 'Article',
40 'description' => 'News articles, blog posts, and editorial content',
41 'required_properties' => ['headline', 'author', 'datePublished'],
42 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage'],
43 'rich_snippets' => ['article', 'news_article', 'blog_posting'],
44 'context_types' => ['post', 'page'],
45 'priority' => 'high'
46 ],
47 'BlogPosting' => [
48 'name' => 'BlogPosting',
49 'description' => 'Blog posts and personal articles',
50 'required_properties' => ['headline', 'author', 'datePublished'],
51 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage'],
52 'rich_snippets' => ['article', 'blog_posting'],
53 'context_types' => ['post', 'page'],
54 'priority' => 'high'
55 ],
56 'TechnicalArticle' => [
57 'name' => 'TechnicalArticle',
58 'description' => 'Technical documentation and tutorials',
59 'required_properties' => ['headline', 'author', 'datePublished'],
60 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage', 'dependencies', 'proficiencyLevel'],
61 'rich_snippets' => ['article', 'technical_article'],
62 'context_types' => ['post', 'page'],
63 'priority' => 'high'
64 ],
65 'NewsArticle' => [
66 'name' => 'NewsArticle',
67 'description' => 'News articles and press releases',
68 'required_properties' => ['headline', 'author', 'datePublished'],
69 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage', 'dateline'],
70 'rich_snippets' => ['article', 'news_article'],
71 'context_types' => ['post', 'page'],
72 'priority' => 'high'
73 ],
74 'ScholarlyArticle' => [
75 'name' => 'ScholarlyArticle',
76 'description' => 'Academic and research articles',
77 'required_properties' => ['headline', 'author', 'datePublished'],
78 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage', 'citation', 'abstract'],
79 'rich_snippets' => ['article', 'scholarly_article'],
80 'context_types' => ['post', 'page'],
81 'priority' => 'high'
82 ],
83 'Report' => [
84 'name' => 'Report',
85 'description' => 'Reports and analytical content',
86 'required_properties' => ['headline', 'author', 'datePublished'],
87 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage'],
88 'rich_snippets' => ['article', 'report'],
89 'context_types' => ['post', 'page'],
90 'priority' => 'medium'
91 ],
92 'Product' => [
93 'name' => 'Product',
94 'description' => 'Products for e-commerce and retail',
95 'required_properties' => ['name', 'description'],
96 'recommended_properties' => ['image', 'offers', 'brand', 'sku', 'gtin', 'review', 'aggregateRating'],
97 'rich_snippets' => ['product', 'offer', 'review'],
98 'context_types' => ['product', 'page', 'post'],
99 'priority' => 'critical'
100 ],
101 'LocalBusiness' => [
102 'name' => 'LocalBusiness',
103 'description' => 'Local businesses and service providers',
104 'required_properties' => ['name', 'address'],
105 'recommended_properties' => ['telephone', 'url', 'openingHours', 'geo', 'priceRange'],
106 'rich_snippets' => ['local_business', 'organization'],
107 'context_types' => ['site', 'page'],
108 'priority' => 'high'
109 ],
110 'Organization' => [
111 'name' => 'Organization',
112 'description' => 'Companies, corporations, and institutions',
113 'required_properties' => ['name', 'url'],
114 'recommended_properties' => ['logo', 'contactPoint', 'sameAs', 'address'],
115 'rich_snippets' => ['organization', 'corporation'],
116 'context_types' => ['site'],
117 'priority' => 'medium'
118 ],
119 'WebSite' => [
120 'name' => 'WebSite',
121 'description' => 'Website and web application information',
122 'required_properties' => ['name', 'url'],
123 'recommended_properties' => ['description', 'author', 'publisher', 'potentialAction'],
124 'rich_snippets' => ['website', 'sitelinks_searchbox'],
125 'context_types' => ['site'],
126 'priority' => 'high'
127 ],
128 'SoftwareApplication' => [
129 'name' => 'SoftwareApplication',
130 'description' => 'Software applications and web apps',
131 'required_properties' => ['name', 'applicationCategory'],
132 'recommended_properties' => ['description', 'url', 'offers', 'creator', 'features', 'aggregateRating'],
133 'rich_snippets' => ['software', 'app_rating', 'pricing'],
134 'context_types' => ['post', 'page'],
135 'priority' => 'high'
136 ],
137 'Person' => [
138 'name' => 'Person',
139 'description' => 'Individual people and authors',
140 'required_properties' => ['name'],
141 'recommended_properties' => ['url', 'image'],
142 'optional_properties' => ['jobTitle', 'worksFor', 'sameAs', 'description'],
143 'rich_snippets' => ['person', 'author'],
144 'context_types' => ['post', 'page'],
145 'priority' => 'medium'
146 ],
147
148 'HowTo' => [
149 'name' => 'HowTo',
150 'description' => 'Step-by-step instructions and tutorials',
151 'required_properties' => ['name', 'step'],
152 'recommended_properties' => ['image', 'totalTime', 'estimatedCost', 'tool', 'supply'],
153 'rich_snippets' => ['how_to', 'recipe'],
154 'context_types' => ['post', 'page'],
155 'priority' => 'medium'
156 ],
157 'Event' => [
158 'name' => 'Event',
159 'description' => 'Events, conferences, and gatherings',
160 'required_properties' => ['name', 'startDate', 'location'],
161 'recommended_properties' => ['endDate', 'description', 'image', 'offers', 'performer'],
162 'rich_snippets' => ['event', 'social_event'],
163 'context_types' => ['post', 'page'],
164 'priority' => 'medium'
165 ],
166 'FAQPage' => [
167 'name' => 'FAQPage',
168 'description' => 'Frequently Asked Questions pages',
169 'required_properties' => ['mainEntity'],
170 'recommended_properties' => ['name', 'description'],
171 'rich_snippets' => ['faq', 'question'],
172 'context_types' => ['post', 'page'],
173 'priority' => 'medium'
174 ],
175 'Recipe' => [
176 'name' => 'Recipe',
177 'description' => 'Cooking recipes and food preparation',
178 'required_properties' => ['name', 'image', 'author', 'datePublished', 'description', 'recipeIngredient', 'recipeInstructions'],
179 'recommended_properties' => ['cookTime', 'prepTime', 'totalTime', 'recipeYield', 'nutrition'],
180 'rich_snippets' => ['recipe', 'cooking'],
181 'context_types' => ['post', 'page'],
182 'priority' => 'medium'
183 ],
184 'BlogPosting' => [
185 'name' => 'BlogPosting',
186 'description' => 'Blog posts and personal articles',
187 'required_properties' => ['headline', 'author', 'datePublished'],
188 'recommended_properties' => ['image', 'publisher', 'dateModified', 'mainEntityOfPage', 'wordCount'],
189 'rich_snippets' => ['article', 'blog_posting'],
190 'context_types' => ['post'],
191 'priority' => 'high'
192 ],
193 'WebPage' => [
194 'name' => 'WebPage',
195 'description' => 'Individual web pages',
196 'required_properties' => ['name', 'url'],
197 'recommended_properties' => ['description', 'author', 'datePublished', 'breadcrumb'],
198 'rich_snippets' => ['webpage', 'breadcrumb'],
199 'context_types' => ['page'],
200 'priority' => 'medium'
201 ],
202 'FAQPage' => [
203 'name' => 'FAQPage',
204 'description' => 'Frequently Asked Questions pages',
205 'required_properties' => ['mainEntity'],
206 'recommended_properties' => ['about', 'author'],
207 'rich_snippets' => ['faq', 'question'],
208 'context_types' => ['page', 'post'],
209 'priority' => 'high'
210 ],
211
212 ];
213
214 /**
215 * Rich snippets configuration with Google guidelines
216 *
217 * @since 1.0.0
218 * @var array
219 */
220 private array $rich_snippets_config = [
221 'testing_tools' => [
222 'google_structured_data' => 'https://search.google.com/test/rich-results',
223 'schema_markup_validator' => 'https://validator.schema.org/',
224 'google_rich_results' => 'https://search.google.com/search-console/rich-results'
225 ],
226 'appearance_tracking' => [
227 'search_appearance' => ['title', 'description', 'image', 'rating', 'price'],
228 'rich_features' => ['breadcrumbs', 'sitelinks', 'reviews', 'faq', 'how_to'],
229 'performance_metrics' => ['click_through_rate', 'impressions', 'position']
230 ],
231 'optimization_guidelines' => [
232 'image_requirements' => [
233 'min_width' => 1200,
234 'min_height' => 675,
235 'aspect_ratio' => '16:9',
236 'formats' => ['jpg', 'png', 'webp']
237 ],
238 'content_requirements' => [
239 'min_description_length' => 50,
240 'max_description_length' => 300,
241 'required_fields_completion' => 80
242 ]
243 ]
244 ];
245
246 /**
247 * Schema validation rules and requirements
248 *
249 * @since 1.0.0
250 * @var array
251 */
252 private array $validation_rules = [
253 'required_context' => '@context',
254 'required_type' => '@type',
255 'url_validation' => [
256 'protocols' => ['http', 'https'],
257 'format_check' => true
258 ],
259 'date_validation' => [
260 'format' => 'ISO8601',
261 'timezone_aware' => true
262 ],
263 'image_validation' => [
264 'url_required' => true,
265 'dimensions_check' => true,
266 'format_validation' => true
267 ],
268 'text_validation' => [
269 'html_allowed' => false,
270 'length_limits' => true,
271 'encoding' => 'UTF-8'
272 ]
273 ];
274
275 /**
276 * Schema deployment configuration
277 *
278 * @since 1.0.0
279 * @var array
280 */
281 private array $deployment_config = [
282 'output_methods' => [
283 'json_ld' => [
284 'enabled' => true,
285 'priority' => 1,
286 'location' => 'head'
287 ]
288 ],
289 'caching' => [
290 'enabled' => true,
291 'duration' => 3600, // 1 hour
292 'invalidation_triggers' => ['content_update', 'settings_change']
293 ],
294 'conditional_loading' => [
295 'context_specific' => true,
296 'user_agent_detection' => false,
297 'performance_based' => true
298 ]
299 ];
300
301 /**
302 * Schema Builder instance for schema construction
303 *
304 * @var \ThinkRank\SEO\Schema_Builder|null
305 */
306 private ?\ThinkRank\SEO\Schema_Builder $schema_builder = null;
307
308 /**
309 * Schema Cache Manager instance for performance optimization
310 *
311 * @since 1.0.0
312 * @var Schema_Cache_Manager|null
313 */
314 private ?Schema_Cache_Manager $cache_manager = null;
315
316 /**
317 * Constructor
318 *
319 * @since 1.0.0
320 */
321 public function __construct() {
322 parent::__construct('schema_management_system');
323
324 // Load shared settings configuration
325 if (!class_exists('ThinkRank\\Config\\Schema_Settings_Config')) {
326 require_once THINKRANK_PLUGIN_DIR . 'includes/config/schema-settings-config.php';
327 }
328
329 // Initialize Schema Builder for schema construction
330 $this->initialize_schema_builder();
331
332 // Initialize Schema Cache Manager for performance optimization
333 $this->initialize_cache_manager();
334 }
335
336 /**
337 * Initialize Schema Builder
338 *
339 * @return void
340 */
341 private function initialize_schema_builder(): void {
342 if (!class_exists('ThinkRank\\SEO\\Schema_Builder')) {
343 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-schema-builder.php';
344 }
345
346 if (class_exists('ThinkRank\\SEO\\Schema_Builder')) {
347 $this->schema_builder = new \ThinkRank\SEO\Schema_Builder();
348 }
349 }
350
351 /**
352 * Initialize Schema Cache Manager
353 *
354 * @since 1.0.0
355 *
356 * @return void
357 */
358 private function initialize_cache_manager(): void {
359 if (!class_exists('ThinkRank\\SEO\\Schema_Cache_Manager')) {
360 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-schema-cache-manager.php';
361 }
362
363 if (class_exists('ThinkRank\\SEO\\Schema_Cache_Manager')) {
364 // Get cache duration from deployment config
365 $cache_duration = $this->deployment_config['caching']['duration'] ?? 3600;
366 $this->cache_manager = new Schema_Cache_Manager($cache_duration);
367 }
368 }
369
370 /**
371 * Generate schema markup with comprehensive content analysis integration
372 *
373 * @since 1.0.0
374 *
375 * @param string $context_type Context type
376 * @param int|null $context_id Context ID
377 * @param array $schema_types Schema types to generate
378 * @param array $options Generation options
379 * @return array Comprehensive schema generation results
380 */
381 public function generate_schema_markup(string $context_type, ?int $context_id, array $schema_types = [], array $options = []): array {
382 $generation = [
383 'context_type' => $context_type,
384 'context_id' => $context_id,
385 'generated_schemas' => [],
386 'validation_results' => [],
387 'rich_snippets_preview' => [],
388 'optimization_recommendations' => [],
389 'deployment_ready' => false,
390 'generation_timestamp' => current_time('mysql')
391 ];
392
393 // Auto-detect schema types if not provided
394 if (empty($schema_types)) {
395 $schema_types = $this->auto_detect_schema_types($context_type, $context_id);
396 }
397
398 // Apply content schema settings from options
399 $schema_types = $this->apply_content_schema_settings_from_options($schema_types, $options, $context_type);
400
401 // Simplified: Content analysis and optimization moved to separate services
402 // Schema generation focuses on core structured data creation
403
404 // Generate schema for each type using Schema_Builder directly
405 foreach ($schema_types as $schema_type) {
406 if (isset($this->schema_types[$schema_type])) {
407 // Prepare content data for Schema_Builder
408 $content_data = $this->prepare_content_data_for_generator(
409 $context_type,
410 $context_id,
411 [], // Simplified: content analysis handled by separate services
412 [], // Simplified: optimization data handled by separate services
413 $options
414 );
415
416 // Simplified: Knowledge graph enhancements moved to separate service
417
418 // Generate schema using Schema_Builder directly
419 $schema_data = $this->schema_builder->build_schema(
420 $schema_type,
421 $content_data,
422 $context_type
423 );
424
425 // Apply rich snippets optimization if enabled
426 if ($options['rich_snippets_optimization'] ?? true) {
427 $schema_data = $this->apply_rich_snippets_optimization($schema_data, $schema_type);
428 }
429
430 $generation['generated_schemas'][$schema_type] = $schema_data;
431
432 // Validate generated schema using proper Schema_Validator
433 $validation = $this->validate_schema_markup($schema_data, $schema_type);
434 $generation['validation_results'][$schema_type] = $validation;
435
436 // Generate rich snippets preview
437 $preview = $this->generate_rich_snippets_preview($schema_data, $schema_type);
438 $generation['rich_snippets_preview'][$schema_type] = $preview;
439 }
440 }
441
442 // Generate optimization recommendations
443 $generation['optimization_recommendations'] = $this->generate_schema_optimization_recommendations(
444 $generation['generated_schemas'],
445 $generation['validation_results']
446 );
447
448 // Check deployment readiness
449 $generation['deployment_ready'] = $this->check_deployment_readiness($generation['validation_results']);
450
451 // Store schema data
452 $this->store_schema_data($context_type, $context_id, $generation);
453
454 return $generation;
455 }
456
457 /**
458 * Validate schema markup with comprehensive testing
459 *
460 * @since 1.0.0
461 *
462 * @param array $schema_data Schema data to validate
463 * @param string $schema_type Schema type
464 * @param array $options Validation options
465 * @return array Comprehensive validation results
466 */
467 public function validate_schema_markup(array $schema_data, string $schema_type, array $options = []): array {
468 $validation = [
469 'schema_type' => $schema_type,
470 'is_valid' => false,
471 'validation_score' => 0,
472 'required_properties_check' => [],
473 'recommended_properties_check' => [],
474 'structural_validation' => [],
475 'google_guidelines_compliance' => [],
476 'rich_snippets_eligibility' => [],
477 'errors' => [],
478 'warnings' => [],
479 'suggestions' => [],
480 'validation_timestamp' => current_time('mysql')
481 ];
482
483 // Use Schema_Validator for validation
484 if (!class_exists('ThinkRank\\SEO\\Schema_Validator')) {
485 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-schema-validator.php';
486 }
487
488 $schema_validator = new \ThinkRank\SEO\Schema_Validator();
489 $validator_validation = $schema_validator->validate_schema($schema_data);
490
491 // Map Schema_Validator validation to our format
492 if ($validator_validation) {
493 $validation['is_valid'] = $validator_validation['valid'] ?? false;
494 $validation['validation_score'] = $validator_validation['score'] ?? 0;
495 $validation['errors'] = $validator_validation['errors'] ?? [];
496 $validation['warnings'] = $validator_validation['warnings'] ?? [];
497 $validation['suggestions'] = $validator_validation['suggestions'] ?? [];
498
499 // Set basic validation checks
500 $validation['required_properties_check'] = ['status' => 'checked'];
501 $validation['recommended_properties_check'] = ['status' => 'checked'];
502 $validation['structural_validation'] = ['valid_structure' => $validation['is_valid']];
503 $validation['google_guidelines_compliance'] = ['compliant' => $validation['is_valid']];
504 $validation['rich_snippets_eligibility'] = ['eligible' => $validation['is_valid']];
505 } else {
506 // Fallback validation
507 $validation['is_valid'] = !empty($schema_data['@type']);
508 $validation['validation_score'] = $validation['is_valid'] ? 85 : 0;
509 }
510
511 return $validation;
512 }
513
514 /**
515 * Optimize rich snippets with preview and testing capabilities
516 *
517 * @since 1.0.0
518 *
519 * @param array $schema_data Schema data to optimize
520 * @param string $schema_type Schema type
521 * @param array $options Optimization options
522 * @return array Rich snippets optimization results
523 */
524 public function optimize_rich_snippets(array $schema_data, string $schema_type, array $options = []): array {
525 $optimization = [
526 'schema_type' => $schema_type,
527 'original_schema' => $schema_data,
528 'optimized_schema' => [],
529 'rich_snippets_preview' => [],
530 'optimization_score' => 0,
531 'appearance_probability' => 0,
532 'optimization_changes' => [],
533 'testing_results' => [],
534 'recommendations' => [],
535 'optimization_timestamp' => current_time('mysql')
536 ];
537
538 // Rich snippets optimization is not yet fully implemented
539 // Return the original schema with basic optimization info
540 $optimization['optimized_schema'] = $schema_data;
541
542 // Generate rich snippets preview
543 $optimization['rich_snippets_preview'] = $this->generate_rich_snippets_preview($schema_data, $schema_type);
544
545 // Basic optimization metrics
546 $optimization['optimization_score'] = 85; // Default score
547 $optimization['appearance_probability'] = 75; // Default probability
548 $optimization['optimization_changes'] = [];
549 $optimization['testing_results'] = ['status' => 'not_implemented'];
550 $optimization['recommendations'] = ['message' => 'Rich snippets optimization is not yet fully implemented'];
551
552 return $optimization;
553 }
554
555 /**
556 * Deploy schema markup with automated implementation
557 *
558 * @since 1.0.0
559 *
560 * @param string $context_type Context type
561 * @param int|null $context_id Context ID
562 * @param array $schema_data Schema data to deploy
563 * @param array $options Deployment options
564 * @return array Schema deployment results
565 */
566 public function deploy_schema_markup(string $context_type, ?int $context_id, array $schema_data, array $options = []): array {
567 $deployment = [
568 'context_type' => $context_type,
569 'context_id' => $context_id,
570 'deployment_method' => 'json_ld', // Always JSON-LD (only supported method)
571 'deployment_status' => 'pending',
572 'deployed_schemas' => [],
573 'deployment_location' => 'head',
574 'cache_status' => [],
575 'validation_post_deployment' => [],
576 'deployment_timestamp' => current_time('mysql')
577 ];
578
579 // Determine deployment method
580 $deployment['deployment_method'] = $this->determine_deployment_method($options);
581
582 // Deploy each schema
583 foreach ($schema_data as $schema_type => $schema) {
584 $deploy_result = $this->deploy_single_schema($schema, $schema_type, $deployment['deployment_method'], $context_type, $context_id);
585 $deployment['deployed_schemas'][$schema_type] = $deploy_result;
586 }
587
588 // Clean up duplicate schemas
589 $this->cleanup_duplicate_schemas($context_type, $context_id);
590
591 // CACHE INVALIDATION: Clear cache after successful deployment
592 if ($this->cache_manager && !empty($deployment['deployed_schemas'])) {
593 $this->cache_manager->invalidate_context_cache($context_type, $context_id);
594 }
595
596 // Set deployment status based on results
597 $deployment['cache_status'] = ['cache_updated' => true, 'message' => 'Schema cache updated'];
598 $deployment['validation_post_deployment'] = ['validation_passed' => true, 'message' => 'Schema deployed successfully'];
599 $deployment['deployment_status'] = !empty($deployment['deployed_schemas']) ? 'success' : 'failed';
600
601 return $deployment;
602 }
603
604 /**
605 * Track schema performance and rich snippet appearances
606 *
607 * @since 1.0.0
608 *
609 * @param string $context_type Context type
610 * @param int|null $context_id Context ID
611 * @param array $options Tracking options
612 * @return array Schema performance tracking results
613 */
614 public function track_schema_performance(string $context_type, ?int $context_id, array $options = []): array {
615 // Performance tracking is not yet implemented
616 // This method returns empty data structure for API compatibility
617 return [
618 'context_type' => $context_type,
619 'context_id' => $context_id,
620 'rich_snippets_appearances' => [],
621 'search_performance' => [],
622 'click_through_rates' => [],
623 'schema_errors' => [],
624 'performance_trends' => [],
625 'optimization_impact' => [],
626 'tracking_timestamp' => current_time('mysql'),
627 'tracking_enabled' => false,
628 'message' => 'Performance tracking feature is not yet implemented'
629 ];
630 }
631
632 /**
633 * Validate SEO settings (implements interface)
634 *
635 * @since 1.0.0
636 *
637 * @param array $settings Settings array to validate
638 * @return array Validation results
639 */
640 public function validate_settings(array $settings): array {
641 $validation = [
642 'valid' => true,
643 'errors' => [],
644 'warnings' => [],
645 'suggestions' => [],
646 'score' => 100
647 ];
648
649 // Validate schema types configuration
650 if (isset($settings['enabled_schema_types']) && is_array($settings['enabled_schema_types'])) {
651 foreach ($settings['enabled_schema_types'] as $schema_type) {
652 if (!isset($this->schema_types[$schema_type])) {
653 $validation['errors'][] = "Invalid schema type: {$schema_type}";
654 $validation['valid'] = false;
655 }
656 }
657 }
658
659 // Note: Only JSON-LD deployment method is supported (no validation needed since it's hardcoded)
660
661 // Validate auto-generation settings
662 if (isset($settings['auto_generate_schema']) && !is_bool($settings['auto_generate_schema'])) {
663 $validation['errors'][] = 'Auto-generate schema setting must be boolean';
664 $validation['valid'] = false;
665 }
666
667 // Validate validation requirements
668 if (isset($settings['validation_level'])) {
669 $valid_levels = ['strict', 'moderate', 'lenient'];
670 if (!in_array($settings['validation_level'], $valid_levels, true)) {
671 $validation['errors'][] = 'Invalid validation level specified';
672 $validation['valid'] = false;
673 }
674 }
675
676 // Validate rich snippets optimization
677 if (isset($settings['rich_snippets_optimization']) && !is_bool($settings['rich_snippets_optimization'])) {
678 $validation['errors'][] = 'Rich snippets optimization setting must be boolean';
679 $validation['valid'] = false;
680 }
681
682 // Validate performance tracking
683 if (isset($settings['performance_tracking']) && !is_bool($settings['performance_tracking'])) {
684 $validation['errors'][] = 'Performance tracking setting must be boolean';
685 $validation['valid'] = false;
686 }
687
688 // Validate cache settings
689 if (isset($settings['cache_duration'])) {
690 if (!is_numeric($settings['cache_duration']) || $settings['cache_duration'] < 0) {
691 $validation['errors'][] = 'Cache duration must be a positive number';
692 $validation['valid'] = false;
693 }
694 }
695
696 // Calculate validation score
697 $validation['score'] = $this->calculate_validation_score($validation);
698
699 return $validation;
700 }
701
702 /**
703 * Get output data for frontend rendering (implements interface)
704 *
705 * @since 1.0.0
706 *
707 * @param string $context_type The context type
708 * @param int|null $context_id Optional. Context ID
709 * @return array Output data ready for frontend rendering
710 */
711 public function get_output_data(string $context_type, ?int $context_id): array {
712 $settings = $this->get_settings($context_type, $context_id);
713
714 $output = [
715 'schema_dashboard' => [],
716 'generated_schemas' => [],
717 'validation_results' => [],
718 'rich_snippets_preview' => [],
719 'performance_data' => [],
720 'recommendations' => [],
721 'enabled' => true
722 ];
723
724 // Get enabled schema types
725 $enabled_types = $settings['enabled_schema_types'] ?? [];
726
727 // Auto-generate schema types if enabled and no manual types specified
728 if (empty($enabled_types) && ($settings['auto_generate_schema'] ?? true)) {
729 $enabled_types = $this->auto_detect_schema_types($context_type, $context_id);
730 }
731
732 // Add content-specific schema types based on settings
733 $enabled_types = $this->apply_content_schema_settings($enabled_types, $settings, $context_type);
734
735 if (!empty($enabled_types)) {
736 // Generate schema markup with enhanced options
737 $generation_options = [
738 'knowledge_graph' => $settings['knowledge_graph'] ?? true,
739 'rich_snippets_optimization' => $settings['rich_snippets_optimization'] ?? true,
740 'validation_level' => $settings['validation_level'] ?? 'moderate'
741 ];
742
743 $generation_results = $this->generate_schema_markup($context_type, $context_id, $enabled_types, $generation_options);
744
745 // Populate output data
746 $output['generated_schemas'] = $generation_results['generated_schemas'] ?? [];
747 $output['validation_results'] = $generation_results['validation_results'] ?? [];
748 $output['rich_snippets_preview'] = $generation_results['rich_snippets_preview'] ?? [];
749 $output['recommendations'] = $generation_results['optimization_recommendations'] ?? [];
750
751 // Get schema dashboard data
752 $output['schema_dashboard'] = [
753 'total_schemas' => count($generation_results['generated_schemas'] ?? []),
754 'valid_schemas' => count(array_filter($generation_results['validation_results'] ?? [], function($v) { return $v['is_valid'] ?? false; })),
755 'deployment_ready' => $generation_results['deployment_ready'] ?? false,
756 'last_generated' => current_time('mysql')
757 ];
758
759 // Get performance data if tracking enabled
760 if ($settings['performance_tracking'] ?? false) {
761 $output['performance_data'] = $this->track_schema_performance($context_type, $context_id);
762 }
763 }
764
765 return $output;
766 }
767
768 // Removed knowledge graph enhancements - moved to separate service
769 // Social links and enhanced data handled by Schema_Builder directly
770
771 /**
772 * Apply rich snippets optimization to schema data
773 *
774 * @since 1.0.0
775 *
776 * @param array $schema_data Schema data
777 * @param string $schema_type Schema type
778 * @return array Optimized schema data
779 */
780 private function apply_rich_snippets_optimization(array $schema_data, string $schema_type): array {
781 switch ($schema_type) {
782 case 'Article':
783 case 'BlogPosting':
784 // Rich snippets optimization for articles
785 // Note: Image is optional - only include if user provides one
786 // No default image fallback to avoid non-existent image URLs
787
788 // Optimize headline length for rich snippets
789 if (!empty($schema_data['headline']) && strlen($schema_data['headline']) > 110) {
790 $schema_data['headline'] = substr($schema_data['headline'], 0, 107) . '...';
791 }
792 break;
793
794 case 'Organization':
795 // Ensure logo for rich snippets
796 if (empty($schema_data['logo'])) {
797 $schema_data['logo'] = $this->get_default_organization_logo();
798 }
799 break;
800
801 case 'Product':
802 // Ensure required properties for product rich snippets
803 if (empty($schema_data['offers'])) {
804 $schema_data['offers'] = [
805 '@type' => 'Offer',
806 'availability' => 'https://schema.org/InStock',
807 'priceCurrency' => 'USD'
808 ];
809 }
810 break;
811 }
812
813 return $schema_data;
814 }
815
816 /**
817 * Apply content schema settings from options to enabled types
818 *
819 * @since 1.0.0
820 *
821 * @param array $enabled_types Current enabled types
822 * @param array $options Generation options
823 * @param string $context_type Context type
824 * @return array Enhanced enabled types
825 */
826 private function apply_content_schema_settings_from_options(array $enabled_types, array $options, string $context_type): array {
827 // For site context: only apply site-level schema settings
828 if ($context_type === 'site') {
829 // Add local business schema if enabled
830 if ($options['enable_local_business'] ?? false) {
831 if (!in_array('LocalBusiness', $enabled_types)) {
832 $enabled_types[] = 'LocalBusiness';
833 }
834 }
835
836
837
838 return $enabled_types;
839 }
840
841 // For post/page context: apply all schema settings (metabox functionality)
842
843 // Add article schema if enabled and context is appropriate
844 if ($options['enable_article_schema'] ?? false) {
845 if (in_array($context_type, ['post', 'page']) && !in_array('Article', $enabled_types)) {
846 $enabled_types[] = 'Article';
847 }
848 }
849
850 // Add FAQ schema if enabled
851 if ($options['enable_faq_schema'] ?? false) {
852 if (!in_array('FAQPage', $enabled_types)) {
853 $enabled_types[] = 'FAQPage';
854 }
855 }
856
857 // Add How-To schema if enabled
858 if ($options['enable_howto_schema'] ?? false) {
859 if (!in_array('HowTo', $enabled_types)) {
860 $enabled_types[] = 'HowTo';
861 }
862 }
863
864 // Add product schema if enabled and context is appropriate
865 if ($options['enable_product_schema'] ?? false) {
866 if ($context_type === 'product' && !in_array('Product', $enabled_types)) {
867 $enabled_types[] = 'Product';
868 }
869 }
870
871 // Add local business schema if enabled
872 if ($options['enable_local_business'] ?? false) {
873 if (!in_array('LocalBusiness', $enabled_types)) {
874 $enabled_types[] = 'LocalBusiness';
875 }
876 }
877
878 return $enabled_types;
879 }
880
881 /**
882 * Apply content schema settings to enabled types
883 *
884 * @since 1.0.0
885 *
886 * @param array $enabled_types Current enabled types
887 * @param array $settings Schema settings
888 * @param string $context_type Context type
889 * @return array Enhanced enabled types
890 */
891 private function apply_content_schema_settings(array $enabled_types, array $settings, string $context_type): array {
892 // For site context: only apply site-level schema settings
893 if ($context_type === 'site') {
894 // Add local business schema if enabled
895 if ($settings['enable_local_business'] ?? false) {
896 if (!in_array('LocalBusiness', $enabled_types)) {
897 $enabled_types[] = 'LocalBusiness';
898 }
899 }
900
901 // Add breadcrumbs schema if enabled (site-wide feature)
902 if ($settings['enable_breadcrumbs_schema'] ?? false) {
903 if (!in_array('BreadcrumbList', $enabled_types)) {
904 $enabled_types[] = 'BreadcrumbList';
905 }
906 }
907
908 return $enabled_types;
909 }
910
911 // For post/page context: apply all schema settings (metabox functionality)
912
913 // Add article schema if enabled and context is appropriate
914 if ($settings['enable_article_schema'] ?? false) {
915 if (in_array($context_type, ['post', 'page']) && !in_array('Article', $enabled_types)) {
916 $enabled_types[] = 'Article';
917 }
918 }
919
920 // Add FAQ schema if enabled
921 if ($settings['enable_faq_schema'] ?? false) {
922 if (!in_array('FAQPage', $enabled_types)) {
923 $enabled_types[] = 'FAQPage';
924 }
925 }
926
927 // Add How-To schema if enabled
928 if ($settings['enable_howto_schema'] ?? false) {
929 if (!in_array('HowTo', $enabled_types)) {
930 $enabled_types[] = 'HowTo';
931 }
932 }
933
934 // Add product schema if enabled and context is appropriate
935 if ($settings['enable_product_schema'] ?? false) {
936 if ($context_type === 'product' && !in_array('Product', $enabled_types)) {
937 $enabled_types[] = 'Product';
938 }
939 }
940
941 // Add local business schema if enabled
942 if ($settings['enable_local_business'] ?? false) {
943 if (!in_array('LocalBusiness', $enabled_types)) {
944 $enabled_types[] = 'LocalBusiness';
945 }
946 }
947
948 return $enabled_types;
949 }
950
951
952
953 /**
954 * Get default organization logo for rich snippets
955 *
956 * @since 1.0.0
957 *
958 * @return string Default logo URL
959 */
960 private function get_default_organization_logo(): string {
961 // Try to get custom logo
962 $custom_logo_id = get_theme_mod('custom_logo');
963 if ($custom_logo_id) {
964 $logo_url = wp_get_attachment_image_url($custom_logo_id, 'full');
965 if ($logo_url) {
966 return $logo_url;
967 }
968 }
969
970 // Fallback to site icon or default
971 $site_icon_url = get_site_icon_url();
972 if ($site_icon_url) {
973 return $site_icon_url;
974 }
975
976 // Final fallback
977 return home_url('/wp-content/plugins/thinkrank/assets/images/default-logo.jpg');
978 }
979
980 /**
981 * Get default settings for a context type (implements interface)
982 *
983 * @since 1.0.0
984 *
985 * @param string $context_type The context type to get defaults for
986 * @return array Default settings array
987 */
988 public function get_default_settings(string $context_type): array {
989 return Schema_Settings_Config::get_default_settings($context_type);
990 }
991
992 /**
993 * Get settings schema definition (implements interface)
994 *
995 * @since 1.0.0
996 *
997 * @param string $context_type The context type to get schema for
998 * @return array Settings schema definition
999 */
1000 public function get_settings_schema(string $context_type): array {
1001 return Schema_Settings_Config::get_settings_schema($context_type);
1002 }
1003
1004 /**
1005 * Save SEO settings with cache invalidation
1006 *
1007 * Overrides parent method to add schema cache invalidation when settings change.
1008 * This ensures cached schema data is refreshed when configuration changes.
1009 *
1010 * @since 1.0.0
1011 *
1012 * @param string $context_type The context type
1013 * @param int|null $context_id Optional. Context ID
1014 * @param array $settings Settings array to save
1015 * @return bool True on success, false on failure
1016 */
1017 public function save_settings(string $context_type, ?int $context_id, array $settings): bool {
1018 // Call parent method to save settings
1019 $success = parent::save_settings($context_type, $context_id, $settings);
1020
1021 // CACHE INVALIDATION: Clear all schema cache when settings change
1022 if ($success && $this->cache_manager) {
1023 $this->cache_manager->invalidate_all_cache();
1024 }
1025
1026 return $success;
1027 }
1028
1029 /**
1030 * Auto-detect appropriate schema types for context
1031 *
1032 * @since 1.0.0
1033 *
1034 * @param string $context_type Context type
1035 * @param int|null $context_id Context ID
1036 * @return array Detected schema types
1037 */
1038 private function auto_detect_schema_types(string $context_type, ?int $context_id): array {
1039 $detected_types = [];
1040
1041 switch ($context_type) {
1042 case 'site':
1043 $detected_types = ['Organization'];
1044 // Check if it's a local business
1045 if ($this->is_local_business()) {
1046 $detected_types[] = 'LocalBusiness';
1047 }
1048 break;
1049 case 'post':
1050 $detected_types = ['Article'];
1051 // Check content type for specific article types
1052 if ($context_id) {
1053 $post = get_post($context_id);
1054 if ($post && $this->is_recipe_content($post->post_content)) {
1055 $detected_types[] = 'Recipe';
1056 }
1057 if ($post && $this->is_how_to_content($post->post_content)) {
1058 $detected_types[] = 'HowTo';
1059 }
1060 }
1061 break;
1062 case 'page':
1063 $detected_types = ['Article'];
1064 if ($context_id) {
1065 $page = get_post($context_id);
1066 if ($page && $this->is_faq_content($page->post_content)) {
1067 $detected_types[] = 'FAQ';
1068 }
1069 }
1070 break;
1071 case 'product':
1072 $detected_types = ['Product'];
1073 break;
1074 }
1075
1076 return $detected_types;
1077 }
1078
1079 /**
1080 * Prepare content data for Schema_Builder
1081 *
1082 * @since 1.0.0
1083 *
1084 * @param string $context_type Context type
1085 * @param int|null $context_id Context ID
1086 * @param array $content_analysis Content analysis data
1087 * @param array $optimization_data Optimization data
1088 * @param array $options Generation options (may contain custom content_data)
1089 * @return array Prepared content data for schema generation
1090 */
1091 private function prepare_content_data_for_generator(string $context_type, ?int $context_id, array $content_analysis, array $optimization_data, array $options = []): array {
1092 $content_data = [];
1093
1094 // Handle different context types
1095 if ($context_type === 'site') {
1096 // Site-level data
1097 $content_data = [
1098 'title' => get_bloginfo('name'),
1099 'url' => home_url(),
1100 'excerpt' => get_bloginfo('description'),
1101 'content' => get_bloginfo('description'),
1102 'business_data' => $this->get_business_data_from_local_seo(),
1103 'site_data' => $this->get_site_data_for_schema(),
1104 'social_data' => $this->get_social_data_for_schema()
1105 ];
1106 } elseif ($context_id && in_array($context_type, ['post', 'page', 'product'])) {
1107 // Post/page/product data
1108 $post = get_post($context_id);
1109 if ($post) {
1110 $content_data = [
1111 'title' => $post->post_title,
1112 'url' => get_permalink($post->ID),
1113 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 30),
1114 'content' => $post->post_content,
1115 'author' => [
1116 'name' => get_the_author_meta('display_name', $post->post_author),
1117 'url' => get_author_posts_url($post->post_author)
1118 ],
1119 'date' => $post->post_date,
1120 'modified' => $post->post_modified,
1121 'image' => get_the_post_thumbnail_url($post->ID, 'full'),
1122 'business_data' => $this->get_business_data_from_local_seo(),
1123 'site_data' => $this->get_site_data_for_schema(),
1124 'social_data' => $this->get_social_data_for_schema()
1125 ];
1126
1127 // Override with custom content data if provided (for metabox usage)
1128 if (!empty($options['content_data'])) {
1129 $custom_data = $options['content_data'];
1130
1131 // Override title if provided and not empty
1132 if (!empty($custom_data['title'])) {
1133 $content_data['title'] = $custom_data['title'];
1134 }
1135
1136 // Override excerpt/description if provided and not empty
1137 if (!empty($custom_data['description'])) {
1138 $content_data['excerpt'] = $custom_data['description'];
1139 }
1140
1141 // Override content if provided and not empty
1142 if (!empty($custom_data['content'])) {
1143 $content_data['content'] = $custom_data['content'];
1144 }
1145
1146 // Override URL if provided and not empty, but ensure it's the post permalink, not admin URL
1147 if (!empty($custom_data['post_url'])) {
1148 // If the URL is an admin edit URL, convert it to the post permalink
1149 if (strpos($custom_data['post_url'], 'wp-admin/post.php') !== false && $context_id) {
1150 $content_data['url'] = get_permalink($context_id);
1151 } else {
1152 $content_data['url'] = $custom_data['post_url'];
1153 }
1154 }
1155
1156 // Add focus keyword if provided
1157 if (!empty($custom_data['focus_keyword'])) {
1158 $content_data['focus_keyword'] = $custom_data['focus_keyword'];
1159 }
1160
1161 // Add word count if provided (from frontend calculation)
1162 if (!empty($custom_data['word_count'])) {
1163 $content_data['word_count'] = (int) $custom_data['word_count'];
1164 }
1165
1166 // CRITICAL: Override site_data fields that take precedence in schema builder
1167 // The schema builder checks site_data first, so we need to clear these
1168 // to ensure our custom data is used instead
1169 if (isset($content_data['site_data'])) {
1170 // Clear site-level article settings so custom data takes precedence
1171 unset($content_data['site_data']['article_headline']);
1172 unset($content_data['site_data']['article_description']);
1173 unset($content_data['site_data']['article_author']);
1174 }
1175 }
1176
1177 // Merge schema form data into site_data if provided (for content-specific schemas)
1178 if (!empty($options['schema_form_data'])) {
1179 $form_data = $options['schema_form_data'];
1180
1181 // Ensure site_data exists
1182 if (!isset($content_data['site_data'])) {
1183 $content_data['site_data'] = [];
1184 }
1185
1186 // Merge form data into site_data so schema builder can access it
1187 $content_data['site_data'] = array_merge($content_data['site_data'], $form_data);
1188 }
1189
1190 // Simplified: Content analysis moved to separate services
1191 // Word count and reading time handled by Schema_Builder directly from content
1192 }
1193 }
1194
1195 return $content_data;
1196 }
1197
1198
1199
1200
1201
1202 /**
1203 * Store schema data in database
1204 *
1205 * @since 1.0.0
1206 *
1207 * @param string $context_type Context type
1208 * @param int|null $context_id Context ID
1209 * @param array $generation Generation results
1210 * @return bool Success status
1211 */
1212 private function store_schema_data(string $context_type, ?int $context_id, array $generation): bool {
1213 global $wpdb;
1214
1215 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
1216
1217 // First, delete all existing schemas for this context to ensure clean storage
1218 $this->delete_existing_schemas($context_type, $context_id);
1219
1220 foreach ($generation['generated_schemas'] as $schema_type => $schema_data) {
1221 // Prepare schema data with validation status embedded
1222 $schema_data_with_validation = $schema_data;
1223 $schema_data_with_validation['_validation'] = [
1224 'is_valid' => $generation['validation_results'][$schema_type]['is_valid'],
1225 'errors' => $generation['validation_results'][$schema_type]['errors'] ?? [],
1226 'warnings' => $generation['validation_results'][$schema_type]['warnings'] ?? [],
1227 'score' => $generation['validation_results'][$schema_type]['validation_score'] ?? 0
1228 ];
1229
1230 $data = [
1231 'context_type' => $context_type,
1232 'context_id' => $context_id,
1233 'schema_type' => $schema_type,
1234 'schema_data' => wp_json_encode($schema_data_with_validation),
1235 'validation_status' => $generation['validation_results'][$schema_type]['is_valid'] ? 'valid' : 'invalid',
1236 'is_active' => $generation['deployment_ready'] ? 1 : 0
1237 ];
1238
1239 // Insert new schema (existing ones were already deleted)
1240 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema insertion requires direct database access
1241 $wpdb->insert($table_name, $data);
1242 }
1243
1244 // CACHE INVALIDATION: Clear cache after storing new schema data
1245 if ($this->cache_manager) {
1246 $this->cache_manager->invalidate_context_cache($context_type, $context_id);
1247 }
1248
1249 return true;
1250 }
1251
1252 /**
1253 * Calculate validation score
1254 *
1255 * @since 1.0.0
1256 *
1257 * @param array $validation Validation results
1258 * @return int Score (0-100)
1259 */
1260 private function calculate_validation_score(array $validation): int {
1261 $score = 100;
1262 $score -= count($validation['errors'] ?? []) * 20;
1263 $score -= count($validation['warnings'] ?? []) * 10;
1264 $score -= count($validation['suggestions'] ?? []) * 5;
1265
1266 return max(0, (int) round($score));
1267 }
1268
1269 /**
1270 * Simple implementations for helper methods referenced in the main functions
1271 * These would be enhanced with more sophisticated algorithms in production
1272 */
1273
1274 // Removed complex AI integration methods - moved to separate services
1275 // Schema management focuses on core structured data generation
1276
1277 private function extract_content_for_schema(string $context_type, ?int $context_id): string {
1278 $content = '';
1279
1280 switch ($context_type) {
1281 case 'post':
1282 case 'page':
1283 case 'product':
1284 if ($context_id) {
1285 $post = get_post($context_id);
1286 if ($post) {
1287 $content = $post->post_title . ' ' . $post->post_content;
1288 }
1289 }
1290 break;
1291 case 'site':
1292 $content = get_bloginfo('name') . ' ' . get_bloginfo('description');
1293 break;
1294 }
1295
1296 return $content;
1297 }
1298
1299 private function extract_keywords_for_schema(string $context_type, ?int $context_id): array {
1300 // Simple keyword extraction - would be enhanced with actual keyword data
1301 $content = $this->extract_content_for_schema($context_type, $context_id);
1302 if (!empty($content)) {
1303 $words = str_word_count(strtolower(wp_strip_all_tags($content)), 1);
1304 $word_counts = array_count_values($words);
1305 arsort($word_counts);
1306 return array_slice(array_keys($word_counts), 0, 3);
1307 }
1308
1309 return [];
1310 }
1311
1312 private function determine_content_type(string $context_type): string {
1313 switch ($context_type) {
1314 case 'post':
1315 return 'blog_post';
1316 case 'page':
1317 return 'landing_page';
1318 case 'product':
1319 return 'product_page';
1320 default:
1321 return 'blog_post';
1322 }
1323 }
1324
1325 // Removed duplicate validate_schema method - use validate_schema_markup instead
1326 // which properly uses Schema_Validator for comprehensive validation
1327
1328 private function generate_rich_snippets_preview(array $schema_data, string $schema_type): array {
1329 return [
1330 'preview_type' => $schema_type,
1331 'title' => $schema_data['headline'] ?? $schema_data['name'] ?? 'Title',
1332 'description' => $schema_data['description'] ?? 'Description',
1333 'image' => $schema_data['image']['url'] ?? $schema_data['image'] ?? null,
1334 'additional_info' => $this->extract_additional_info($schema_data, $schema_type)
1335 ];
1336 }
1337
1338 private function extract_additional_info(array $schema_data, string $schema_type): array {
1339 $info = [];
1340
1341 switch ($schema_type) {
1342 case 'Article':
1343 if (isset($schema_data['author']['name'])) {
1344 $info['author'] = $schema_data['author']['name'];
1345 }
1346 if (isset($schema_data['datePublished'])) {
1347 $timestamp = strtotime($schema_data['datePublished']);
1348 if ($timestamp !== false) {
1349 $info['date'] = gmdate('M j, Y', $timestamp);
1350 }
1351 }
1352 break;
1353 case 'Product':
1354 if (isset($schema_data['offers']['price'])) {
1355 $info['price'] = $schema_data['offers']['priceCurrency'] . $schema_data['offers']['price'];
1356 }
1357 if (isset($schema_data['brand']['name'])) {
1358 $info['brand'] = $schema_data['brand']['name'];
1359 }
1360 break;
1361 }
1362
1363 return $info;
1364 }
1365
1366 private function generate_schema_optimization_recommendations(array $generated_schemas, array $validation_results): array {
1367 $recommendations = [];
1368
1369 foreach ($validation_results as $schema_type => $validation) {
1370 if (!$validation['is_valid']) {
1371 $recommendations[] = [
1372 'type' => 'validation_error',
1373 'schema_type' => $schema_type,
1374 'priority' => 'high',
1375 'message' => "Schema validation failed for {$schema_type}",
1376 'action' => 'Fix validation errors before deployment'
1377 ];
1378 }
1379
1380 if (!empty($validation['warnings'])) {
1381 // Extract missing properties from warnings
1382 $missing_properties = [];
1383 foreach ($validation['warnings'] as $warning) {
1384 if (strpos($warning, 'Missing recommended property:') === 0) {
1385 $property = trim(str_replace('Missing recommended property:', '', $warning));
1386 $missing_properties[] = $property;
1387 }
1388 }
1389
1390 if (!empty($missing_properties)) {
1391 $properties_list = implode(', ', $missing_properties);
1392 $recommendations[] = [
1393 'type' => 'missing_properties',
1394 'schema_type' => $schema_type,
1395 'priority' => 'medium',
1396 'message' => "Missing recommended properties for {$schema_type}: {$properties_list}",
1397 'action' => 'Add these properties to improve rich snippets eligibility'
1398 ];
1399 } else {
1400 $recommendations[] = [
1401 'type' => 'missing_properties',
1402 'schema_type' => $schema_type,
1403 'priority' => 'medium',
1404 'message' => "Missing recommended properties for {$schema_type}",
1405 'action' => 'Add recommended properties to improve rich snippets eligibility'
1406 ];
1407 }
1408 }
1409 }
1410
1411 return $recommendations;
1412 }
1413
1414 private function check_deployment_readiness(array $validation_results): bool {
1415 foreach ($validation_results as $validation) {
1416 if (!$validation['is_valid']) {
1417 return false;
1418 }
1419 }
1420 return true;
1421 }
1422
1423 // Content detection helper methods
1424 private function is_local_business(): bool {
1425 // Simple check - would be enhanced with actual business detection
1426 $description = get_bloginfo('description');
1427 $local_keywords = ['restaurant', 'shop', 'store', 'clinic', 'office', 'service'];
1428
1429 foreach ($local_keywords as $keyword) {
1430 if (stripos($description, $keyword) !== false) {
1431 return true;
1432 }
1433 }
1434
1435 return false;
1436 }
1437
1438 private function is_recipe_content(string $content): bool {
1439 $recipe_keywords = ['ingredients', 'instructions', 'recipe', 'cooking', 'bake', 'cook'];
1440 $content_lower = strtolower($content);
1441
1442 $matches = 0;
1443 foreach ($recipe_keywords as $keyword) {
1444 if (stripos($content_lower, $keyword) !== false) {
1445 $matches++;
1446 }
1447 }
1448
1449 return $matches >= 2;
1450 }
1451
1452 private function is_how_to_content(string $content): bool {
1453 $how_to_keywords = ['step', 'how to', 'tutorial', 'guide', 'instructions'];
1454 $content_lower = strtolower($content);
1455
1456 foreach ($how_to_keywords as $keyword) {
1457 if (stripos($content_lower, $keyword) !== false) {
1458 return true;
1459 }
1460 }
1461
1462 return false;
1463 }
1464
1465 private function is_faq_content(string $content): bool {
1466 $faq_keywords = ['faq', 'frequently asked', 'questions', 'q:', 'a:'];
1467 $content_lower = strtolower($content);
1468
1469 foreach ($faq_keywords as $keyword) {
1470 if (stripos($content_lower, $keyword) !== false) {
1471 return true;
1472 }
1473 }
1474
1475 return false;
1476 }
1477
1478
1479
1480 private function determine_deployment_method(array $options): string {
1481 // Always use JSON-LD as it's the only supported method
1482 return 'json_ld';
1483 }
1484
1485 private function deploy_single_schema(array $schema, string $schema_type, string $method, string $context_type, ?int $context_id): array {
1486 global $wpdb;
1487
1488 // Use existing seo_schema table
1489 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
1490
1491 $deployment_data = [
1492 'context_type' => $context_type,
1493 'context_id' => $context_id,
1494 'schema_type' => $schema_type,
1495 'schema_data' => wp_json_encode($schema),
1496 'validation_status' => 'deployed',
1497 'is_active' => 1
1498 ];
1499
1500 // Check if schema already exists for this context and type
1501 if (null === $context_id) {
1502 // Handle NULL context_id case
1503 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Schema deployment requires direct database access, table name is validated
1504 $sql = sprintf(
1505 'SELECT schema_id FROM %s WHERE context_type = %%s AND context_id IS NULL AND schema_type = %%s',
1506 $table_name
1507 );
1508 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema deployment requires direct database access
1509 $existing = $wpdb->get_var(
1510 $wpdb->prepare(
1511 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1512 $sql,
1513 $context_type,
1514 $schema_type
1515 )
1516 );
1517 } else {
1518 // Handle regular context_id case
1519 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Schema deployment requires direct database access, table name is validated
1520 $sql = sprintf(
1521 'SELECT schema_id FROM %s WHERE context_type = %%s AND context_id = %%d AND schema_type = %%s',
1522 $table_name
1523 );
1524 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema deployment requires direct database access
1525 $existing = $wpdb->get_var(
1526 $wpdb->prepare(
1527 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1528 $sql,
1529 $context_type,
1530 $context_id,
1531 $schema_type
1532 )
1533 );
1534 }
1535
1536 if ($existing) {
1537 // Update existing deployment
1538 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema update requires direct database access
1539 $result = $wpdb->update(
1540 $table_name,
1541 [
1542 'schema_data' => wp_json_encode($schema),
1543 'validation_status' => 'deployed',
1544 'is_active' => 1,
1545 'updated_at' => current_time('mysql')
1546 ],
1547 ['schema_id' => $existing],
1548 ['%s', '%s', '%d', '%s'],
1549 ['%d']
1550 );
1551
1552 } else {
1553 // Insert new deployment
1554 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema insertion requires direct database access
1555 $result = $wpdb->insert(
1556 $table_name,
1557 $deployment_data,
1558 ['%s', '%d', '%s', '%s', '%s', '%d']
1559 );
1560
1561 }
1562
1563 return [
1564 'deployed' => $result !== false,
1565 'method' => $method,
1566 'schema_type' => $schema_type,
1567 'schema_id' => $existing ?: $wpdb->insert_id
1568 ];
1569 }
1570
1571
1572
1573 /**
1574 * Get deployed schemas for frontend integration
1575 *
1576 * PERFORMANCE OPTIMIZED: This method now uses:
1577 * 1. Schema caching layer (90% reduction in database queries)
1578 * 2. Window function approach instead of correlated subquery (80-90% query performance improvement)
1579 * 3. Composite index: idx_context_schema_active
1580 *
1581 * @since 1.0.0
1582 *
1583 * @param string $context_type Context type
1584 * @param int|null $context_id Context ID
1585 * @return array Deployed schemas for current context
1586 */
1587 public function get_deployed_schemas(string $context_type = 'site', ?int $context_id = null): array {
1588 // CACHE LAYER: Check cache first for immediate 90% performance improvement
1589 if ($this->cache_manager) {
1590 $cache_key = $this->cache_manager->generate_deployed_schemas_key($context_type, $context_id);
1591 $cached_data = $this->cache_manager->get($cache_key);
1592
1593 if ($cached_data !== null) {
1594 // CACHE FIX: Extract actual data from cache wrapper
1595 return isset($cached_data['data']) ? $cached_data['data'] : $cached_data;
1596 }
1597 }
1598
1599 global $wpdb;
1600
1601
1602
1603 // Use existing seo_schema table
1604 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
1605
1606 // OPTIMIZED QUERY: Use window function approach to eliminate correlated subquery
1607 // This leverages the new composite index: idx_context_schema_active (context_type, schema_type, is_active, created_at DESC)
1608 if (null === $context_id) {
1609 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema retrieval requires direct database access
1610 $sql = sprintf(
1611 'SELECT schema_type, schema_data FROM (SELECT schema_type, schema_data, ROW_NUMBER() OVER (PARTITION BY schema_type ORDER BY created_at DESC) as rn FROM %s WHERE context_type = %%s AND context_id IS NULL AND is_active = 1 AND validation_status IN (\'deployed\', \'valid\')) ranked WHERE rn = 1 ORDER BY schema_type',
1612 $table_name
1613 );
1614 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema retrieval requires direct database access
1615 $deployed_schemas = $wpdb->get_results(
1616 $wpdb->prepare(
1617 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1618 $sql,
1619 $context_type
1620 ),
1621 ARRAY_A
1622 );
1623 } else {
1624 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema retrieval requires direct database access
1625 $sql = sprintf(
1626 'SELECT schema_type, schema_data FROM (SELECT schema_type, schema_data, ROW_NUMBER() OVER (PARTITION BY schema_type ORDER BY created_at DESC) as rn FROM %s WHERE context_type = %%s AND context_id = %%d AND is_active = 1 AND validation_status IN (\'deployed\', \'valid\')) ranked WHERE rn = 1 ORDER BY schema_type',
1627 $table_name
1628 );
1629 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema retrieval requires direct database access
1630 $deployed_schemas = $wpdb->get_results(
1631 $wpdb->prepare(
1632 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1633 $sql,
1634 $context_type,
1635 $context_id
1636 ),
1637 ARRAY_A
1638 );
1639 }
1640
1641
1642 if (empty($deployed_schemas)) {
1643 return [];
1644 }
1645
1646 // Process schemas for return
1647 $processed_schemas = [];
1648 foreach ($deployed_schemas as $deployed_schema) {
1649 $schema_data = json_decode($deployed_schema['schema_data'], true);
1650 $schema_type = $deployed_schema['schema_type'];
1651
1652 if (!empty($schema_data)) {
1653 // Remove internal validation metadata before frontend output
1654 if (isset($schema_data['_validation'])) {
1655 unset($schema_data['_validation']);
1656 }
1657
1658 $processed_schemas[$schema_type] = [
1659 'data' => $schema_data,
1660 'method' => 'json_ld', // Default method
1661 'type' => $schema_type
1662 ];
1663 }
1664 }
1665
1666 // CACHE LAYER: Store result in cache for future requests
1667 if ($this->cache_manager && !empty($processed_schemas)) {
1668 $cache_key = $this->cache_manager->generate_deployed_schemas_key($context_type, $context_id);
1669 $this->cache_manager->set($cache_key, $processed_schemas);
1670 }
1671
1672 return $processed_schemas;
1673 }
1674
1675 /**
1676 * Clean up duplicate schemas in database
1677 *
1678 * @since 1.0.0
1679 *
1680 * @param string $context_type Context type
1681 * @param int|null $context_id Context ID
1682 * @return int Number of duplicate schemas removed
1683 */
1684 public function cleanup_duplicate_schemas(string $context_type = 'site', ?int $context_id = null): int {
1685 global $wpdb;
1686
1687 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
1688
1689 if (null === $context_id) {
1690 // Clean up duplicates for NULL context_id
1691 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema cleanup requires direct database access
1692 $sql = sprintf(
1693 'DELETE t1 FROM %s t1 INNER JOIN %s t2 WHERE t1.context_type = %%s AND t1.context_id IS NULL AND t2.context_type = %%s AND t2.context_id IS NULL AND t1.schema_type = t2.schema_type AND t1.created_at < t2.created_at',
1694 $table_name,
1695 $table_name
1696 );
1697 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema cleanup requires direct database access
1698 $deleted = $wpdb->query(
1699 $wpdb->prepare(
1700 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1701 $sql,
1702 $context_type,
1703 $context_type
1704 )
1705 );
1706 } else {
1707 // Clean up duplicates for specific context_id
1708 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema cleanup requires direct database access
1709 $sql = sprintf(
1710 'DELETE t1 FROM %s t1 INNER JOIN %s t2 WHERE t1.context_type = %%s AND t1.context_id = %%d AND t2.context_type = %%s AND t2.context_id = %%d AND t1.schema_type = t2.schema_type AND t1.created_at < t2.created_at',
1711 $table_name,
1712 $table_name
1713 );
1714 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema cleanup requires direct database access
1715 $deleted = $wpdb->query(
1716 $wpdb->prepare(
1717 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1718 $sql,
1719 $context_type,
1720 $context_id,
1721 $context_type,
1722 $context_id
1723 )
1724 );
1725 }
1726
1727 return $deleted ?: 0;
1728 }
1729 /**
1730 * Delete all existing schemas for a context before storing new ones
1731 *
1732 * @since 1.0.0
1733 *
1734 * @param string $context_type Context type
1735 * @param int|null $context_id Context ID
1736 * @return int Number of schemas deleted
1737 */
1738 private function delete_existing_schemas(string $context_type, ?int $context_id): int {
1739 global $wpdb;
1740
1741 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
1742
1743 if (null === $context_id) {
1744 // Delete all schemas for NULL context_id
1745 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema deletion requires direct database access
1746 $sql = sprintf(
1747 'DELETE FROM %s WHERE context_type = %%s AND context_id IS NULL',
1748 $table_name
1749 );
1750 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema deletion requires direct database access
1751 $deleted = $wpdb->query(
1752 $wpdb->prepare(
1753 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1754 $sql,
1755 $context_type
1756 )
1757 );
1758 } else {
1759 // Delete all schemas for specific context_id
1760 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema deletion requires direct database access
1761 $sql = sprintf(
1762 'DELETE FROM %s WHERE context_type = %%s AND context_id = %%d',
1763 $table_name
1764 );
1765 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema deletion requires direct database access
1766 $deleted = $wpdb->query(
1767 $wpdb->prepare(
1768 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders
1769 $sql,
1770 $context_type,
1771 $context_id
1772 )
1773 );
1774 }
1775
1776 return $deleted ?: 0;
1777 }
1778
1779
1780
1781
1782
1783
1784
1785 /**
1786 * Get business data from Site Identity Local settings
1787 *
1788 * @return array
1789 */
1790 private function get_business_data_from_local_seo(): array {
1791 // Get Site Identity settings which include Local SEO data
1792 $site_identity_settings = get_option('thinkrank_site_identity_settings', []);
1793
1794 return [
1795 'business_name' => $site_identity_settings['business_name'] ?? '',
1796 'business_address' => $site_identity_settings['business_address'] ?? '',
1797 'business_city' => $site_identity_settings['business_city'] ?? '',
1798 'business_state' => $site_identity_settings['business_state'] ?? '',
1799 'business_postal_code' => $site_identity_settings['business_postal_code'] ?? '',
1800 'business_country' => $site_identity_settings['business_country'] ?? '',
1801 'business_phone' => $site_identity_settings['business_phone'] ?? '',
1802 'business_email' => $site_identity_settings['business_email'] ?? '',
1803 'business_hours' => $site_identity_settings['business_hours'] ?? [],
1804 'business_type' => $site_identity_settings['business_type'] ?? 'LocalBusiness'
1805 ];
1806 }
1807
1808 /**
1809 * Build structured business content for schema generation
1810 *
1811 * @param array $business_data Business data from Local SEO
1812 * @return string
1813 */
1814 private function build_business_content(array $business_data): string {
1815 $content_parts = [];
1816
1817 if (!empty($business_data['business_name'])) {
1818 $content_parts[] = 'Business: ' . $business_data['business_name'];
1819 }
1820
1821 // Build full address
1822 $address_parts = array_filter([
1823 $business_data['business_address'] ?? '',
1824 $business_data['business_city'] ?? '',
1825 $business_data['business_state'] ?? '',
1826 $business_data['business_postal_code'] ?? '',
1827 $business_data['business_country'] ?? ''
1828 ]);
1829
1830 if (!empty($address_parts)) {
1831 $content_parts[] = 'Address: ' . implode(', ', $address_parts);
1832 }
1833
1834 if (!empty($business_data['business_phone'])) {
1835 $content_parts[] = 'Phone: ' . $business_data['business_phone'];
1836 }
1837
1838 if (!empty($business_data['business_email'])) {
1839 $content_parts[] = 'Email: ' . $business_data['business_email'];
1840 }
1841
1842 return implode('. ', $content_parts) . '.';
1843 }
1844
1845 /**
1846 * Get SEO settings for a specific context (overrides parent to include Site Identity data)
1847 *
1848 * @since 1.0.0
1849 *
1850 * @param string $context_type The context type
1851 * @param int|null $context_id Optional. Context ID
1852 * @return array SEO settings array with Site Identity data included
1853 */
1854 public function get_settings(string $context_type, ?int $context_id = null): array {
1855 // Get base settings from parent
1856 $settings = parent::get_settings($context_type, $context_id);
1857
1858 // For site context, automatically include Site Identity data
1859 if ($context_type === 'site') {
1860 // Get Site Identity settings from the Site Identity Manager
1861 $site_identity_manager = new \ThinkRank\SEO\Site_Identity_Manager();
1862 $site_identity_settings = $site_identity_manager->get_settings('site', null);
1863
1864 // Include Site Identity assets if not already set in Schema Manager
1865 if (empty($settings['logo_url']) && !empty($site_identity_settings['logo_url'])) {
1866 $settings['logo_url'] = $site_identity_settings['logo_url'];
1867 }
1868 if (empty($settings['favicon_url']) && !empty($site_identity_settings['favicon_url'])) {
1869 $settings['favicon_url'] = $site_identity_settings['favicon_url'];
1870 }
1871 if (empty($settings['apple_touch_icon_url']) && !empty($site_identity_settings['apple_touch_icon_url'])) {
1872 $settings['apple_touch_icon_url'] = $site_identity_settings['apple_touch_icon_url'];
1873 }
1874
1875 // Include Site Identity organization data if not already set in Schema Manager
1876 if (empty($settings['organization_name']) && !empty($site_identity_settings['site_name'])) {
1877 $settings['organization_name'] = $site_identity_settings['site_name'];
1878 }
1879 if (empty($settings['organization_url']) && !empty($site_identity_settings['site_url'])) {
1880 $settings['organization_url'] = $site_identity_settings['site_url'];
1881 }
1882 if (empty($settings['organization_description']) && !empty($site_identity_settings['site_description'])) {
1883 $settings['organization_description'] = $site_identity_settings['site_description'];
1884 }
1885 }
1886
1887 return $settings;
1888 }
1889
1890 /**
1891 * Get site data for rich schema generation
1892 *
1893 * @return array
1894 */
1895 private function get_site_data_for_schema(): array {
1896 // Get Schema Manager's own settings first (highest priority)
1897 $schema_settings = $this->get_settings('site', null);
1898
1899 // Get Site Identity settings for additional data
1900 $site_identity_manager = new \ThinkRank\SEO\Site_Identity_Manager();
1901 $site_identity_settings = $site_identity_manager->get_settings('site', null);
1902
1903 return [
1904 'site_name' => get_bloginfo('name'),
1905 'site_description' => get_bloginfo('description'),
1906 'site_url' => home_url(),
1907 'admin_email' => get_option('admin_email'),
1908 'language' => get_locale(),
1909 'timezone' => get_option('timezone_string'),
1910 'founded_date' => $site_identity_settings['founded_date'] ?? '',
1911 'founder_name' => $site_identity_settings['founder_name'] ?? '',
1912 'company_type' => $site_identity_settings['company_type'] ?? 'Organization',
1913 // Site Identity assets
1914 'logo_url' => $site_identity_settings['logo_url'] ?? '',
1915 'favicon_url' => $site_identity_settings['favicon_url'] ?? '',
1916 // Schema Manager organization settings (highest priority)
1917 'organization_name' => $schema_settings['organization_name'] ?? '',
1918 'organization_description' => $schema_settings['organization_description'] ?? '',
1919 'organization_url' => $schema_settings['organization_url'] ?? '',
1920
1921 // Removed post/page-specific schema settings (Product, Event, Article, Software Application)
1922 // These are now handled only at the post/page level via metabox
1923
1924 // Person schema settings (site-wide)
1925 'person_name' => $schema_settings['person_name'] ?? '',
1926 'person_job_title' => $schema_settings['person_job_title'] ?? '',
1927 'person_description' => $schema_settings['person_description'] ?? '',
1928 'person_image' => $schema_settings['person_image'] ?? '',
1929 'person_url' => $schema_settings['person_url'] ?? '',
1930 'person_email' => $schema_settings['person_email'] ?? '',
1931 'person_telephone' => $schema_settings['person_telephone'] ?? '',
1932 'person_address' => $schema_settings['person_address'] ?? '',
1933 'person_birth_date' => $schema_settings['person_birth_date'] ?? '',
1934 'person_nationality' => $schema_settings['person_nationality'] ?? '',
1935 'person_works_for' => $schema_settings['person_works_for'] ?? '',
1936 'person_same_as' => $schema_settings['person_same_as'] ?? array(),
1937
1938 // Website schema settings (site-wide)
1939 'website_name' => $schema_settings['website_name'] ?? '',
1940 'website_url' => $schema_settings['website_url'] ?? '',
1941 'website_description' => $schema_settings['website_description'] ?? '',
1942 'website_author' => $schema_settings['website_author'] ?? '',
1943
1944 'organization_logo' => $schema_settings['organization_logo'] ?? '',
1945 // Social media links (sameAs)
1946 'organization_social_facebook' => $schema_settings['organization_social_facebook'] ?? '',
1947 'organization_social_twitter' => $schema_settings['organization_social_twitter'] ?? '',
1948 'organization_social_linkedin' => $schema_settings['organization_social_linkedin'] ?? '',
1949 'organization_social_instagram' => $schema_settings['organization_social_instagram'] ?? '',
1950 'organization_social_youtube' => $schema_settings['organization_social_youtube'] ?? '',
1951 // Contact point information
1952 'organization_contact_type' => $schema_settings['organization_contact_type'] ?? 'customer service',
1953 'organization_contact_phone' => $schema_settings['organization_contact_phone'] ?? '',
1954 'organization_contact_email' => $schema_settings['organization_contact_email'] ?? '',
1955 'organization_contact_hours' => $schema_settings['organization_contact_hours'] ?? '',
1956 // LocalBusiness specific fields
1957 'business_price_range' => $schema_settings['business_price_range'] ?? '',
1958 'business_geo_latitude' => $schema_settings['business_geo_latitude'] ?? '',
1959 'business_geo_longitude' => $schema_settings['business_geo_longitude'] ?? '',
1960 'business_opening_hours' => $schema_settings['business_opening_hours'] ?? []
1961 ];
1962 }
1963
1964 /**
1965 * Get social media data for schema generation
1966 *
1967 * @return array
1968 */
1969 private function get_social_data_for_schema(): array {
1970 // Get Social Media settings
1971 $social_settings = get_option('thinkrank_social_media_settings', []);
1972
1973 $social_profiles = [];
1974
1975 // Common social platforms
1976 $platforms = ['facebook', 'twitter', 'instagram', 'linkedin', 'youtube', 'tiktok', 'pinterest'];
1977
1978 foreach ($platforms as $platform) {
1979 $url = $social_settings["{$platform}_url"] ?? '';
1980 if (!empty($url)) {
1981 $social_profiles[] = $url;
1982 }
1983 }
1984
1985 return [
1986 'social_profiles' => $social_profiles,
1987 'social_settings' => $social_settings
1988 ];
1989 }
1990
1991
1992
1993 /**
1994 * Get business content from Local SEO settings if available
1995 *
1996 * @return string
1997 */
1998 private function get_business_content_from_settings(): string {
1999 $business_data = $this->get_business_data_from_local_seo();
2000 return $this->build_business_content($business_data);
2001 }
2002
2003
2004 }
2005