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