PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.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-site-identity-manager.php

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

3,193 lines 112.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Identity Manager Class
4 *
5 * Comprehensive site identity management with title formats, separators,
6 * breadcrumb navigation, robots.txt management, and site identity optimization.
7 * Implements 2025 SEO best practices with real industry-standard algorithms.
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 // Ensure dependencies are loaded
19 if (!class_exists('ThinkRank\\SEO\\Abstract_SEO_Manager')) {
20 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-abstract-seo-manager.php';
21 }
22
23 if (!interface_exists('ThinkRank\\SEO\\Interfaces\\SEO_Manager_Interface')) {
24 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/interfaces/class-seo-manager-interface.php';
25 }
26
27 /**
28 * Site Identity Manager Class
29 *
30 * Manages all aspects of site identity including title formats, breadcrumbs,
31 * robots.txt, and global SEO settings with context-aware optimization.
32 *
33 * @since 1.0.0
34 */
35 class Site_Identity_Manager extends Abstract_SEO_Manager {
36
37 /**
38 * WordPress filesystem instance
39 *
40 * @since 1.0.0
41 * @var \WP_Filesystem_Base|null
42 */
43 private $filesystem = null;
44
45 /**
46 * Title format templates with dynamic placeholders
47 *
48 * @since 1.0.0
49 * @var array
50 */
51 private array $title_templates = [
52 'default' => '%title% %separator% %sitename%',
53 'reverse' => '%sitename% %separator% %title%',
54 'title_only' => '%title%',
55 'sitename_only' => '%sitename%',
56 'custom' => '%title% %separator% %sitename% %separator% %tagline%',
57 'category' => '%title% %separator% %category% %separator% %sitename%',
58 'author' => '%title% %separator% %author% %separator% %sitename%',
59 'date' => '%title% %separator% %date% %separator% %sitename%',
60 'search' => 'Search Results for "%searchterm%" %separator% %sitename%',
61 '404' => 'Page Not Found %separator% %sitename%'
62 ];
63
64 /**
65 * Available title separators with their specifications
66 *
67 * @since 1.0.0
68 * @var array
69 */
70 private array $title_separators = [
71 'pipe' => [
72 'symbol' => '|',
73 'name' => 'Pipe',
74 'description' => 'Vertical bar separator (most common)',
75 'seo_score' => 10
76 ],
77 'dash' => [
78 'symbol' => '-',
79 'name' => 'Dash',
80 'description' => 'Hyphen separator (clean and readable)',
81 'seo_score' => 9
82 ],
83 'bullet' => [
84 'symbol' => '',
85 'name' => 'Bullet',
86 'description' => 'Bullet point separator (modern)',
87 'seo_score' => 8
88 ],
89 'colon' => [
90 'symbol' => ':',
91 'name' => 'Colon',
92 'description' => 'Colon separator (formal)',
93 'seo_score' => 7
94 ],
95 'greater' => [
96 'symbol' => '>',
97 'name' => 'Greater Than',
98 'description' => 'Arrow-like separator (hierarchical)',
99 'seo_score' => 6
100 ],
101 'tilde' => [
102 'symbol' => '~',
103 'name' => 'Tilde',
104 'description' => 'Wave separator (unique)',
105 'seo_score' => 5
106 ]
107 ];
108
109 /**
110 * Breadcrumb types and their configurations
111 *
112 * @since 1.0.0
113 * @var array
114 */
115 private array $breadcrumb_types = [
116 'hierarchical' => [
117 'name' => 'Hierarchical',
118 'description' => 'Based on page hierarchy and categories',
119 'schema_type' => 'BreadcrumbList',
120 'seo_value' => 10
121 ],
122 'taxonomy' => [
123 'name' => 'Taxonomy-based',
124 'description' => 'Based on post categories and tags',
125 'schema_type' => 'BreadcrumbList',
126 'seo_value' => 9
127 ],
128 'path' => [
129 'name' => 'URL Path',
130 'description' => 'Based on URL structure',
131 'schema_type' => 'BreadcrumbList',
132 'seo_value' => 8
133 ],
134 'custom' => [
135 'name' => 'Custom',
136 'description' => 'Manually defined breadcrumb structure',
137 'schema_type' => 'BreadcrumbList',
138 'seo_value' => 7
139 ]
140 ];
141
142 /**
143 * Robots.txt directives and their specifications
144 *
145 * @since 1.0.0
146 * @var array
147 */
148 private array $robots_directives = [
149 'user_agent' => [
150 'required' => true,
151 'description' => 'Specifies which web crawler the rules apply to',
152 'examples' => ['*', 'Googlebot', 'Bingbot', 'Yandexbot']
153 ],
154 'disallow' => [
155 'required' => false,
156 'description' => 'Specifies paths that should not be crawled',
157 'examples' => ['/admin/', '/wp-admin/', '/wp-includes/', '/private/']
158 ],
159 'allow' => [
160 'required' => false,
161 'description' => 'Specifies paths that should be crawled (overrides disallow)',
162 'examples' => ['/wp-admin/admin-ajax.php', '/wp-content/uploads/']
163 ],
164 'sitemap' => [
165 'required' => false,
166 'description' => 'Specifies the location of XML sitemaps',
167 'examples' => ['/sitemap.xml', '/sitemap_index.xml']
168 ],
169 'crawl_delay' => [
170 'required' => false,
171 'description' => 'Specifies delay between requests (in seconds)',
172 'examples' => ['1', '5', '10']
173 ]
174 ];
175
176 /**
177 * Site identity elements configuration
178 *
179 * @since 1.0.0
180 * @var array
181 */
182 private array $identity_elements = [
183 'logo' => [
184 'type' => 'image',
185 'required' => false,
186 'description' => 'Site logo for branding and schema markup',
187 'recommended_size' => '600x60',
188 'max_size' => '2MB'
189 ],
190 'favicon' => [
191 'type' => 'image',
192 'required' => false,
193 'description' => 'Site favicon for browser tabs',
194 'recommended_size' => '32x32',
195 'formats' => ['ico', 'png']
196 ],
197 'apple_touch_icon' => [
198 'type' => 'image',
199 'required' => false,
200 'description' => 'Apple touch icon for iOS devices',
201 'recommended_size' => '180x180',
202 'format' => 'png'
203 ],
204 'site_name' => [
205 'type' => 'text',
206 'required' => true,
207 'description' => 'Official site name for branding',
208 'max_length' => 60
209 ],
210 'tagline' => [
211 'type' => 'text',
212 'required' => false,
213 'description' => 'Site tagline or slogan',
214 'max_length' => 160
215 ],
216 'description' => [
217 'type' => 'text',
218 'required' => false,
219 'description' => 'Site description for meta tags',
220 'max_length' => 160
221 ]
222 ];
223
224 /**
225 * Constructor
226 *
227 * @since 1.0.0
228 */
229 public function __construct() {
230 parent::__construct('site_identity');
231 }
232
233 /**
234 * Initialize WordPress filesystem
235 *
236 * @since 1.0.0
237 * @return bool True if filesystem is initialized, false otherwise
238 */
239 private function init_filesystem(): bool {
240 if ($this->filesystem !== null) {
241 return true;
242 }
243
244 global $wp_filesystem;
245
246 if (!function_exists('WP_Filesystem')) {
247 require_once ABSPATH . 'wp-admin/includes/file.php';
248 }
249
250 $credentials = request_filesystem_credentials('', '', false, false, null);
251 if (!WP_Filesystem($credentials)) {
252 return false;
253 }
254
255 $this->filesystem = $wp_filesystem;
256 return true;
257 }
258
259 /**
260 * Check if directory is writable using WP_Filesystem
261 *
262 * @since 1.0.0
263 * @param string $path Directory path to check
264 * @return bool True if writable, false otherwise
265 */
266 private function is_directory_writable(string $path): bool {
267 if (!$this->init_filesystem()) {
268 return false;
269 }
270
271 return $this->filesystem->is_writable($path);
272 }
273
274 /**
275 * Check if file is writable using WP_Filesystem
276 *
277 * @since 1.0.0
278 * @param string $file File path to check
279 * @return bool True if writable, false otherwise
280 */
281 private function is_file_writable(string $file): bool {
282 if (!$this->init_filesystem()) {
283 return false;
284 }
285
286 return $this->filesystem->is_writable($file);
287 }
288
289 /**
290 * Set file permissions using WP_Filesystem
291 *
292 * @since 1.0.0
293 * @param string $file File path
294 * @param int $mode File permissions mode
295 * @return bool True if successful, false otherwise
296 */
297 private function set_file_permissions(string $file, int $mode): bool {
298 if (!$this->init_filesystem()) {
299 return false;
300 }
301
302 return $this->filesystem->chmod($file, $mode);
303 }
304
305 /**
306 * Generate dynamic title using templates and placeholders
307 *
308 * @since 1.0.0
309 *
310 * @param string $template_name Template name to use
311 * @param array $data Data for placeholder replacement
312 * @param string $context Context type
313 * @return string Generated title
314 */
315 public function generate_title(string $template_name = 'default', array $data = [], string $context = 'site'): string {
316 // Get template
317 $template = $this->title_templates[$template_name] ?? $this->title_templates['default'];
318
319 // Get site settings
320 $settings = $this->get_settings('site');
321 $separator = $this->get_title_separator($settings['title_separator'] ?? 'pipe');
322
323 // Prepare placeholder data
324 $placeholders = $this->prepare_title_placeholders($data, $context, $settings);
325
326 // Replace placeholders
327 $title = $this->replace_title_placeholders($template, $placeholders, $separator);
328
329 // Clean and optimize title
330 $title = $this->optimize_title($title, $context);
331
332 return $title;
333 }
334
335 /**
336 * Generate breadcrumb navigation with schema markup
337 *
338 * @since 1.0.0
339 *
340 * @param string $type Breadcrumb type
341 * @param array $options Breadcrumb options
342 * @return array Breadcrumb data with schema markup
343 */
344 public function generate_breadcrumbs(string $type = 'hierarchical', array $options = []): array {
345 $breadcrumbs = [
346 'items' => [],
347 'schema' => [],
348 'html' => '',
349 'type' => $type,
350 'count' => 0
351 ];
352
353 // Get breadcrumb settings
354 $settings = $this->get_settings('site');
355 $breadcrumb_settings = $settings['breadcrumbs'] ?? [];
356
357 // Generate breadcrumb items based on type
358 switch ($type) {
359 case 'hierarchical':
360 $breadcrumbs['items'] = $this->generate_hierarchical_breadcrumbs($options);
361 break;
362 case 'taxonomy':
363 $breadcrumbs['items'] = $this->generate_taxonomy_breadcrumbs($options);
364 break;
365 case 'path':
366 $breadcrumbs['items'] = $this->generate_path_breadcrumbs($options);
367 break;
368 case 'custom':
369 $breadcrumbs['items'] = $this->generate_custom_breadcrumbs($options);
370 break;
371 }
372
373 // Generate schema markup
374 $breadcrumbs['schema'] = $this->generate_breadcrumb_schema($breadcrumbs['items']);
375
376 // Generate HTML output
377 $breadcrumbs['html'] = $this->generate_breadcrumb_html($breadcrumbs['items'], $breadcrumb_settings);
378
379 // Set count
380 $breadcrumbs['count'] = count($breadcrumbs['items']);
381
382 return $breadcrumbs;
383 }
384
385 /**
386 * Generate and manage robots.txt content
387 *
388 * @since 1.0.0
389 *
390 * @param array $custom_rules Optional custom rules to add
391 * @return array Robots.txt data and validation
392 */
393 public function generate_robots_txt(array $custom_rules = []): array {
394 $robots_data = [
395 'content' => '',
396 'rules' => [],
397 'validation' => [],
398 'file_exists' => false,
399 'writable' => false
400 ];
401
402 // Check if robots.txt file exists and is writable
403 $robots_file = ABSPATH . 'robots.txt';
404 $robots_data['file_exists'] = file_exists($robots_file);
405 $robots_data['writable'] = $this->is_directory_writable(dirname($robots_file));
406
407 // Get site settings
408 $settings = $this->get_settings('site');
409
410 // Generate default rules (pass full settings so sitemap_url is available)
411 $default_rules = $this->generate_default_robots_rules($settings);
412
413 // Merge with custom rules
414 $all_rules = array_merge($default_rules, $custom_rules);
415
416 // Validate rules
417 $robots_data['validation'] = $this->validate_robots_rules($all_rules);
418
419 // Generate robots.txt content
420 $robots_data['content'] = $this->build_robots_txt_content($all_rules);
421 $robots_data['rules'] = $all_rules;
422
423 return $robots_data;
424 }
425
426 /**
427 * Write robots.txt content to filesystem
428 *
429 * @since 1.0.0
430 *
431 * @param string $content Robots.txt content to write
432 * @return array Write operation result
433 */
434 public function write_robots_txt(string $content): array {
435 $result = [
436 'success' => false,
437 'message' => '',
438 'file_path' => '',
439 'permissions' => []
440 ];
441
442 $robots_file = ABSPATH . 'robots.txt';
443
444 // Security: Validate file path to prevent path traversal attacks
445 $real_robots_file = realpath(dirname($robots_file)) . DIRECTORY_SEPARATOR . basename($robots_file);
446 $allowed_dir = realpath(ABSPATH);
447
448 if (!$allowed_dir || strpos(dirname($real_robots_file), $allowed_dir) !== 0) {
449 $result['message'] = 'Invalid file path detected for security reasons.';
450 return $result;
451 }
452
453 $result['file_path'] = $robots_file;
454
455 // Check directory permissions
456 $result['permissions'] = [
457 'directory_writable' => $this->is_directory_writable(ABSPATH),
458 'file_exists' => file_exists($robots_file),
459 'file_writable' => file_exists($robots_file) ? $this->is_file_writable($robots_file) : null
460 ];
461
462 // Check if we can write to the directory
463 if (!$result['permissions']['directory_writable']) {
464 $result['message'] = 'WordPress root directory is not writable. Please check file permissions.';
465 return $result;
466 }
467
468 // Check if existing file is writable (if it exists)
469 if ($result['permissions']['file_exists'] && !$result['permissions']['file_writable']) {
470 $result['message'] = 'Existing robots.txt file is not writable. Please check file permissions.';
471 return $result;
472 }
473
474 try {
475 // Write new content (directly replace existing file)
476 $bytes_written = file_put_contents($robots_file, $content, LOCK_EX);
477
478 if ($bytes_written !== false) {
479 $result['success'] = true;
480 $result['message'] = 'Robots.txt file written successfully.';
481 $result['bytes_written'] = $bytes_written;
482
483 // Set appropriate file permissions (644)
484 $this->set_file_permissions($robots_file, 0644);
485 } else {
486 $result['message'] = 'Failed to write robots.txt file.';
487 }
488
489 } catch (\Exception $e) {
490 $result['message'] = 'Error writing robots.txt file: ' . $e->getMessage();
491 }
492
493 return $result;
494 }
495
496 /**
497 * Optimize site identity data with comprehensive analysis
498 *
499 * @since 1.0.0
500 *
501 * @param array $identity_data Site identity data to optimize
502 * @param array $options Optimization options including section focus
503 * @return array Optimized identity data with validation
504 */
505 public function optimize_site_identity(array $identity_data, array $options = []): array {
506 $optimization = [
507 'optimized_data' => [],
508 'validation' => [],
509 'suggestions' => [],
510 'warnings' => [],
511 'improvements' => [],
512 'score' => 0,
513 'section_scores' => []
514 ];
515
516 // Determine optimization focus
517 $focus = $options['focus'] ?? 'all';
518
519 // Section-specific optimization
520 if ($focus === 'title_formats' || $focus === 'all') {
521 $title_optimization = $this->optimize_title_formats($identity_data);
522 $optimization['section_scores']['title_formats'] = $title_optimization['score'];
523 $optimization['suggestions'] = array_merge($optimization['suggestions'], $title_optimization['suggestions']);
524 $optimization['warnings'] = array_merge($optimization['warnings'], $title_optimization['warnings']);
525 }
526
527 if ($focus === 'breadcrumbs' || $focus === 'all') {
528 $breadcrumb_optimization = $this->optimize_breadcrumbs($identity_data);
529 $optimization['section_scores']['breadcrumbs'] = $breadcrumb_optimization['score'];
530 $optimization['suggestions'] = array_merge($optimization['suggestions'], $breadcrumb_optimization['suggestions']);
531 $optimization['warnings'] = array_merge($optimization['warnings'], $breadcrumb_optimization['warnings']);
532 }
533
534 if ($focus === 'robots_txt' || $focus === 'all') {
535 $robots_optimization = $this->optimize_robots_txt($identity_data);
536 $optimization['section_scores']['robots_txt'] = $robots_optimization['score'];
537 $optimization['suggestions'] = array_merge($optimization['suggestions'], $robots_optimization['suggestions']);
538 $optimization['warnings'] = array_merge($optimization['warnings'], $robots_optimization['warnings']);
539 }
540
541 if ($focus === 'site_assets' || $focus === 'all') {
542 $assets_optimization = $this->optimize_site_assets($identity_data);
543 $optimization['section_scores']['site_assets'] = $assets_optimization['score'];
544 $optimization['suggestions'] = array_merge($optimization['suggestions'], $assets_optimization['suggestions']);
545 $optimization['warnings'] = array_merge($optimization['warnings'], $assets_optimization['warnings']);
546 }
547
548 // Legacy element-by-element optimization for basic site info
549 if ($focus === 'site_info' || $focus === 'all') {
550 foreach ($this->identity_elements as $element => $config) {
551 if (isset($identity_data[$element])) {
552 $element_optimization = $this->optimize_identity_element(
553 $element,
554 $identity_data[$element],
555 $config
556 );
557
558 $optimization['optimized_data'][$element] = $element_optimization['optimized_value'];
559 $optimization['validation'][$element] = $element_optimization['validation'];
560 $optimization['suggestions'] = array_merge(
561 $optimization['suggestions'],
562 $element_optimization['suggestions']
563 );
564 }
565 }
566 }
567
568 // Calculate overall optimization score
569 if (!empty($optimization['section_scores'])) {
570 $optimization['score'] = (int) round(array_sum($optimization['section_scores']) / count($optimization['section_scores']));
571 } else {
572 $optimization['score'] = $this->calculate_identity_score($optimization['validation']);
573 }
574
575 // Store optimization results in seo_analysis table
576 $this->store_optimization_results($optimization, $focus);
577
578 return $optimization;
579 }
580
581 /**
582 * Optimize title formats with enhanced rules
583 *
584 * @since 1.0.0
585 *
586 * @param array $settings Title format settings
587 * @return array Optimization results
588 */
589 public function optimize_title_formats(array $settings): array {
590 $optimization = [
591 'score' => 100,
592 'suggestions' => [],
593 'warnings' => [],
594 'improvements' => []
595 ];
596
597 // Validate title template
598 $template = $settings['title_template'] ?? 'default';
599 if ($template === 'default') {
600 $optimization['suggestions'][] = 'Consider using "Post Title | Site Name" template for better SEO';
601 $optimization['score'] -= 10;
602 }
603
604 // Check separator choice
605 $separator = $settings['title_separator'] ?? 'pipe';
606 $separator_data = $this->title_separators[$separator] ?? null;
607 if ($separator_data) {
608 $seo_score = $separator_data['seo_score'] ?? 5;
609 if ($seo_score < 8) {
610 $optimization['suggestions'][] = "Consider using '|' or '-' separators for better SEO performance";
611 $optimization['score'] -= (10 - $seo_score);
612 }
613 }
614
615 // Test title length with sample data
616 $sample_title = $this->generate_sample_title($settings);
617 $title_length = strlen($sample_title);
618 if ($title_length > 60) {
619 $optimization['warnings'][] = "Title template generates {$title_length}-character titles (over 60-character limit)";
620 $optimization['score'] -= 15;
621 } elseif ($title_length < 30) {
622 $optimization['suggestions'][] = "Title template generates short {$title_length}-character titles";
623 $optimization['score'] -= 5;
624 }
625
626 // Check for site name inclusion
627 $template_content = $this->title_templates[$template] ?? '';
628 if (strpos($template_content, '%sitename%') === false) {
629 $optimization['suggestions'][] = 'Include site name in title template for brand recognition';
630 $optimization['score'] -= 10;
631 }
632
633 return $optimization;
634 }
635
636 /**
637 * Optimize breadcrumb settings with UX best practices
638 *
639 * @since 1.0.0
640 *
641 * @param array $settings Breadcrumb settings
642 * @return array Optimization results
643 */
644 public function optimize_breadcrumbs(array $settings): array {
645 $optimization = [
646 'score' => 100,
647 'suggestions' => [],
648 'warnings' => [],
649 'improvements' => []
650 ];
651
652 // Check if breadcrumbs are enabled
653 if (!($settings['breadcrumbs_enabled'] ?? true)) {
654 $optimization['suggestions'][] = 'Enable breadcrumbs to improve user navigation and SEO (recommended by Google)';
655 $optimization['score'] = 20; // Major penalty for disabled breadcrumbs
656 return $optimization;
657 }
658
659 // Validate breadcrumb type
660 $type = $settings['breadcrumb_type'] ?? 'hierarchical';
661 $type_scores = [
662 'hierarchical' => 100,
663 'category_based' => 90,
664 'simple' => 70,
665 'custom' => 80
666 ];
667
668 $type_score = $type_scores[$type] ?? 60;
669 $optimization['score'] = min($optimization['score'], $type_score);
670
671 if ($type === 'simple') {
672 $optimization['suggestions'][] = 'Consider hierarchical breadcrumbs for better site structure representation';
673 }
674
675 // Validate separator choice
676 $separator = $settings['breadcrumb_separator'] ?? '';
677 $separator_ux = [
678 '' => ['score' => 100, 'note' => 'Clear directional indicator'],
679 '>' => ['score' => 95, 'note' => 'Simple and effective'],
680 '/' => ['score' => 85, 'note' => 'Familiar but can confuse with URLs'],
681 '|' => ['score' => 75, 'note' => 'Less intuitive for navigation'],
682 '»' => ['score' => 90, 'note' => 'Distinctive double arrow']
683 ];
684
685 $sep_data = $separator_ux[$separator] ?? ['score' => 50, 'note' => 'Unusual choice'];
686 $optimization['score'] = min($optimization['score'], $sep_data['score']);
687
688 if ($sep_data['score'] < 95) {
689 $optimization['suggestions'][] = "Separator '{$separator}': {$sep_data['note']}";
690 }
691
692 // Validate home text
693 $home_text = $settings['breadcrumb_home_text'] ?? 'Home';
694 if (empty($home_text)) {
695 $optimization['warnings'][] = 'Empty home text reduces accessibility for screen readers';
696 $optimization['score'] -= 15;
697 } elseif (strlen($home_text) > 20) {
698 $optimization['suggestions'][] = 'Keep home text concise (current: ' . strlen($home_text) . ' chars)';
699 $optimization['score'] -= 5;
700 }
701
702 // Check prefix usage
703 $prefix = $settings['breadcrumb_prefix'] ?? '';
704 if (!empty($prefix) && strlen($prefix) > 50) {
705 $optimization['suggestions'][] = 'Breadcrumb prefix is quite long (' . strlen($prefix) . ' chars) - consider shortening';
706 $optimization['score'] -= 5;
707 }
708
709 // Current page display
710 if (!($settings['show_current_page'] ?? true)) {
711 $optimization['suggestions'][] = 'Show current page in breadcrumbs for better user orientation';
712 $optimization['score'] -= 10;
713 }
714
715 return $optimization;
716 }
717
718 /**
719 * Optimize robots.txt settings with technical SEO best practices
720 *
721 * @since 1.0.0
722 *
723 * @param array $settings Robots.txt settings
724 * @return array Optimization results
725 */
726 public function optimize_robots_txt(array $settings): array {
727 $optimization = [
728 'score' => 100,
729 'suggestions' => [],
730 'warnings' => [],
731 'improvements' => []
732 ];
733
734 // Check if robots.txt management is enabled
735 if (!($settings['robots_txt_enabled'] ?? true)) {
736 $optimization['suggestions'][] = 'Enable robots.txt management for better SEO control and automated updates';
737 $optimization['score'] = 30;
738 return $optimization;
739 }
740
741 // Critical: Search engine access
742 if (!($settings['allow_search_engines'] ?? true)) {
743 $optimization['warnings'][] = 'CRITICAL: Search engines are blocked - your site will not be indexed by Google, Bing, etc.';
744 $optimization['score'] = 10; // Severe penalty
745 }
746
747 // Sitemap URL validation (now from sitemap settings)
748 $sitemap_urls = $this->get_sitemap_urls_for_robots();
749 if (empty($sitemap_urls)) {
750 $optimization['suggestions'][] = 'Enable sitemap generation to include sitemap URLs in robots.txt';
751 $optimization['score'] -= 15;
752 } else {
753 // Validate first sitemap accessibility (representative check)
754 $first_sitemap = $sitemap_urls[0];
755 $sitemap_response = wp_remote_head($first_sitemap, ['timeout' => 10]);
756 if (is_wp_error($sitemap_response) || wp_remote_retrieve_response_code($sitemap_response) !== 200) {
757 $optimization['warnings'][] = 'Primary sitemap URL is not accessible - check sitemap generation';
758 $optimization['score'] -= 10;
759 }
760 }
761
762 // File system permissions
763 $robots_file = ABSPATH . 'robots.txt';
764 $robots_dir = dirname($robots_file);
765
766 if (!$this->is_directory_writable($robots_dir)) {
767 $optimization['warnings'][] = 'WordPress root directory is not writable - robots.txt cannot be managed automatically';
768 $optimization['score'] -= 15;
769 } elseif (file_exists($robots_file) && !$this->is_file_writable($robots_file)) {
770 $optimization['warnings'][] = 'Existing robots.txt file is not writable - cannot update automatically';
771 $optimization['score'] -= 10;
772 }
773
774 // Content analysis
775 $custom_content = $settings['robots_txt_content'] ?? '';
776 if (!empty($custom_content)) {
777 // Check for dangerous patterns
778 if (preg_match('/User-agent:\s*\*\s*\n\s*Disallow:\s*\/\s*$/m', $custom_content)) {
779 $optimization['warnings'][] = 'Blocking all content for all crawlers - this will prevent search engine indexing';
780 $optimization['score'] -= 30;
781 }
782
783 // Check for sitemap declaration in content
784 if (!empty($sitemap_urls) && strpos($custom_content, 'Sitemap:') === false) {
785 $optimization['suggestions'][] = 'Sitemap URLs are automatically included in generated robots.txt';
786 $optimization['score'] -= 5;
787 }
788 }
789
790 return $optimization;
791 }
792
793 /**
794 * Optimize site assets (logo, favicon, apple touch icon)
795 *
796 * @since 1.0.0
797 *
798 * @param array $settings Site assets settings
799 * @return array Optimization results
800 */
801 public function optimize_site_assets(array $settings): array {
802 $optimization = [
803 'score' => 100,
804 'suggestions' => [],
805 'warnings' => [],
806 'improvements' => []
807 ];
808
809 // Check site logo
810 $logo_url = $settings['logo_url'] ?? '';
811 if (empty($logo_url)) {
812 $optimization['suggestions'][] = 'Add a site logo for better branding and professional appearance';
813 $optimization['score'] -= 20;
814 } else {
815 // Validate logo URL and dimensions
816 if (!filter_var($logo_url, FILTER_VALIDATE_URL)) {
817 $optimization['warnings'][] = 'Logo URL format is invalid';
818 $optimization['score'] -= 15;
819 }
820 }
821
822 // Check favicon
823 $favicon_url = $settings['favicon_url'] ?? '';
824 if (empty($favicon_url)) {
825 $optimization['suggestions'][] = 'Add a favicon for better browser tab identification';
826 $optimization['score'] -= 15;
827 }
828
829 // Check Apple touch icon
830 $apple_icon_url = $settings['apple_touch_icon_url'] ?? '';
831 if (empty($apple_icon_url)) {
832 $optimization['suggestions'][] = 'Add an Apple touch icon for better iOS device experience';
833 $optimization['score'] -= 10;
834 }
835
836 // Additional logo analysis for local images
837 if (!empty($logo_url) && filter_var($logo_url, FILTER_VALIDATE_URL)) {
838 $attachment_id = attachment_url_to_postid($logo_url);
839 if ($attachment_id) {
840 $image_meta = wp_get_attachment_metadata($attachment_id);
841 if ($image_meta && isset($image_meta['width'], $image_meta['height'])) {
842 $width = $image_meta['width'];
843 $height = $image_meta['height'];
844
845 if ($width < 112 || $height < 112) {
846 $optimization['warnings'][] = "Logo dimensions ({$width}x{$height}) are below recommended minimum (112x112)";
847 $optimization['score'] -= 10;
848 }
849
850 if ($width > 1920 || $height > 1920) {
851 $optimization['suggestions'][] = "Logo dimensions ({$width}x{$height}) are very large - consider optimizing for faster loading";
852 $optimization['score'] -= 5;
853 }
854
855 // Aspect ratio check
856 $ratio = $width / $height;
857 if ($ratio < 0.5 || $ratio > 2.0) {
858 $optimization['suggestions'][] = 'Logo aspect ratio should be between 1:2 and 2:1 for optimal display';
859 $optimization['score'] -= 5;
860 }
861 }
862 }
863 }
864
865 return $optimization;
866 }
867
868 /**
869 * Optimize local SEO settings for better local search visibility
870 *
871 * @since 1.0.0
872 *
873 * @param array $settings Local SEO settings
874 * @return array Optimization results
875 */
876 public function optimize_local_seo(array $settings): array {
877 $optimization = [
878 'score' => 100,
879 'suggestions' => [],
880 'warnings' => [],
881 'improvements' => [],
882 'optimized_data' => []
883 ];
884
885 // Check if local SEO is enabled
886 if (empty($settings['local_seo_enabled'])) {
887 $optimization['warnings'][] = 'Local SEO is disabled - enable it to improve local search visibility';
888 $optimization['score'] -= 20;
889 return $optimization;
890 }
891
892 // Validate business name (required for local SEO)
893 if (empty($settings['business_name'])) {
894 $optimization['warnings'][] = 'Business name is required for local SEO';
895 $optimization['score'] -= 25;
896 } else {
897 // Optimize business name
898 $optimized_name = $this->optimize_business_name($settings['business_name']);
899 if ($optimized_name !== $settings['business_name']) {
900 $optimization['optimized_data']['business_name'] = $optimized_name;
901 $optimization['suggestions'][] = 'Business name optimized for better local search visibility';
902 }
903 }
904
905 // Validate complete address (NAP consistency)
906 $address_score = $this->validate_business_address($settings, $optimization);
907 $optimization['score'] -= (100 - $address_score);
908
909 // Validate phone number
910 if (empty($settings['business_phone'])) {
911 $optimization['warnings'][] = 'Business phone number is missing - important for local SEO and NAP consistency';
912 $optimization['score'] -= 15;
913 } else {
914 $optimized_phone = $this->optimize_phone_number($settings['business_phone']);
915 if ($optimized_phone !== $settings['business_phone']) {
916 $optimization['optimized_data']['business_phone'] = $optimized_phone;
917 $optimization['suggestions'][] = 'Phone number formatted for better consistency';
918 }
919 }
920
921 // Validate business hours
922 if (empty($settings['business_hours']) || !is_array($settings['business_hours'])) {
923 $optimization['suggestions'][] = 'Add business hours to improve local search visibility and customer experience';
924 $optimization['score'] -= 10;
925 } else {
926 $hours_validation = $this->validate_business_hours($settings['business_hours']);
927 if (!$hours_validation['valid']) {
928 $optimization['warnings'] = array_merge($optimization['warnings'], $hours_validation['warnings']);
929 $optimization['score'] -= $hours_validation['penalty'];
930 }
931 }
932
933 // Check for geo-coordinates
934 if (empty($settings['business_latitude']) || empty($settings['business_longitude'])) {
935 $optimization['suggestions'][] = 'Add latitude and longitude coordinates for precise location targeting';
936 $optimization['score'] -= 10;
937 } else {
938 // Validate coordinates
939 if (!$this->validate_coordinates($settings['business_latitude'], $settings['business_longitude'])) {
940 $optimization['warnings'][] = 'Invalid latitude or longitude coordinates';
941 $optimization['score'] -= 15;
942 }
943 }
944
945 // Business type validation
946 if (empty($settings['business_type'])) {
947 $optimization['suggestions'][] = 'Select a specific business type for better schema markup';
948 $optimization['score'] -= 5;
949 }
950
951 // Email validation
952 if (!empty($settings['business_email']) && !is_email($settings['business_email'])) {
953 $optimization['warnings'][] = 'Business email format is invalid';
954 $optimization['score'] -= 10;
955 }
956
957 // Local SEO best practices
958 $this->add_local_seo_best_practices($optimization, $settings);
959
960 return $optimization;
961 }
962
963 /**
964 * Optimize business name for local SEO
965 *
966 * @param string $business_name Original business name
967 * @return string Optimized business name
968 */
969 private function optimize_business_name(string $business_name): string {
970 // Remove excessive punctuation and normalize spacing
971 $optimized = preg_replace('/[^\w\s\-&.,]/', '', $business_name);
972 $optimized = preg_replace('/\s+/', ' ', $optimized);
973 $optimized = trim($optimized);
974
975 // Ensure proper capitalization
976 $optimized = ucwords(strtolower($optimized));
977
978 return $optimized;
979 }
980
981 /**
982 * Validate business address components
983 *
984 * @param array $settings Business settings
985 * @param array &$optimization Optimization results (passed by reference)
986 * @return int Address completeness score (0-100)
987 */
988 private function validate_business_address(array $settings, array &$optimization): int {
989 $score = 100;
990 $required_fields = ['business_address', 'business_city', 'business_state', 'business_country'];
991 $missing_fields = [];
992
993 foreach ($required_fields as $field) {
994 if (empty($settings[$field])) {
995 $missing_fields[] = str_replace('business_', '', $field);
996 $score -= 20;
997 }
998 }
999
1000 if (!empty($missing_fields)) {
1001 $optimization['warnings'][] = 'Missing address components: ' . implode(', ', $missing_fields) . ' - important for NAP consistency';
1002 }
1003
1004 // Postal code is recommended but not required
1005 if (empty($settings['business_postal_code'])) {
1006 $optimization['suggestions'][] = 'Add postal code for more precise location targeting';
1007 $score -= 5;
1008 }
1009
1010 return max(0, $score);
1011 }
1012
1013 /**
1014 * Optimize phone number format for consistency
1015 *
1016 * @param string $phone_number Original phone number
1017 * @return string Optimized phone number
1018 */
1019 private function optimize_phone_number(string $phone_number): string {
1020 // Remove all non-numeric characters except + for international numbers
1021 $cleaned = preg_replace('/[^\d+]/', '', $phone_number);
1022
1023 // If it's a US number (10 digits), format as (XXX) XXX-XXXX
1024 if (preg_match('/^(\d{10})$/', $cleaned, $matches)) {
1025 return '(' . substr($matches[1], 0, 3) . ') ' . substr($matches[1], 3, 3) . '-' . substr($matches[1], 6);
1026 }
1027
1028 // If it's a US number with country code, format as +1 (XXX) XXX-XXXX
1029 if (preg_match('/^1(\d{10})$/', $cleaned, $matches)) {
1030 return '+1 (' . substr($matches[1], 0, 3) . ') ' . substr($matches[1], 3, 3) . '-' . substr($matches[1], 6);
1031 }
1032
1033 // For international numbers, keep the + and return as-is
1034 return $cleaned;
1035 }
1036
1037 /**
1038 * Validate business hours format and completeness
1039 *
1040 * @param array $business_hours Business hours array
1041 * @return array Validation results
1042 */
1043 private function validate_business_hours(array $business_hours): array {
1044 $validation = [
1045 'valid' => true,
1046 'warnings' => [],
1047 'penalty' => 0
1048 ];
1049
1050 $days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
1051 $open_days = 0;
1052
1053 foreach ($days as $day) {
1054 if (!isset($business_hours[$day])) {
1055 continue;
1056 }
1057
1058 $day_data = $business_hours[$day];
1059
1060 if (empty($day_data['closed'])) {
1061 $open_days++;
1062
1063 // Validate time format
1064 if (empty($day_data['open']) || empty($day_data['close'])) {
1065 $validation['warnings'][] = "Missing opening or closing time for {$day}";
1066 $validation['penalty'] += 2;
1067 } else {
1068 // Validate time format (HH:MM)
1069 if (!preg_match('/^\d{2}:\d{2}$/', $day_data['open']) || !preg_match('/^\d{2}:\d{2}$/', $day_data['close'])) {
1070 $validation['warnings'][] = "Invalid time format for {$day} (use HH:MM format)";
1071 $validation['penalty'] += 2;
1072 }
1073 }
1074 }
1075 }
1076
1077 if ($open_days === 0) {
1078 $validation['warnings'][] = 'No business hours specified - all days marked as closed';
1079 $validation['penalty'] += 10;
1080 }
1081
1082 if ($validation['penalty'] > 0) {
1083 $validation['valid'] = false;
1084 }
1085
1086 return $validation;
1087 }
1088
1089 /**
1090 * Validate latitude and longitude coordinates
1091 *
1092 * @param string $latitude Latitude coordinate
1093 * @param string $longitude Longitude coordinate
1094 * @return bool True if coordinates are valid
1095 */
1096 private function validate_coordinates(string $latitude, string $longitude): bool {
1097 $lat = floatval($latitude);
1098 $lng = floatval($longitude);
1099
1100 // Validate latitude range (-90 to 90)
1101 if ($lat < -90 || $lat > 90) {
1102 return false;
1103 }
1104
1105 // Validate longitude range (-180 to 180)
1106 if ($lng < -180 || $lng > 180) {
1107 return false;
1108 }
1109
1110 return true;
1111 }
1112
1113 /**
1114 * Add local SEO best practices suggestions
1115 *
1116 * @param array &$optimization Optimization results (passed by reference)
1117 * @param array $settings Business settings
1118 * @return void
1119 */
1120 private function add_local_seo_best_practices(array &$optimization, array $settings): void {
1121 // Check for Google My Business integration
1122 if (empty($settings['google_my_business_url'])) {
1123 $optimization['suggestions'][] = 'Consider adding your Google My Business profile URL for better local visibility';
1124 }
1125
1126 // Check for social media profiles
1127 $social_platforms = ['facebook_url', 'twitter_url', 'instagram_url', 'linkedin_url'];
1128 $has_social = false;
1129 foreach ($social_platforms as $platform) {
1130 if (!empty($settings[$platform])) {
1131 $has_social = true;
1132 break;
1133 }
1134 }
1135
1136 if (!$has_social) {
1137 $optimization['suggestions'][] = 'Add social media profiles to improve local business credibility';
1138 }
1139
1140 // Check for business description
1141 if (empty($settings['business_description'])) {
1142 $optimization['suggestions'][] = 'Add a business description for better context in local search results';
1143 }
1144
1145 // Service area suggestions
1146 if (empty($settings['service_areas'])) {
1147 $optimization['suggestions'][] = 'Define service areas if your business serves multiple locations';
1148 }
1149 }
1150
1151 /**
1152 * Generate sample title for testing template length
1153 *
1154 * @since 1.0.0
1155 *
1156 * @param array $settings Title format settings
1157 * @return string Generated sample title
1158 */
1159 private function generate_sample_title(array $settings): string {
1160 $template = $settings['title_template'] ?? 'default';
1161 $separator = $settings['title_separator'] ?? 'pipe';
1162
1163 $template_content = $this->title_templates[$template] ?? $this->title_templates['default'];
1164 $separator_symbol = $this->title_separators[$separator]['symbol'] ?? '|';
1165
1166 // Sample data for testing
1167 $sample_data = [
1168 '%title%' => 'How to Optimize Your Website for Better SEO Results',
1169 '%sitename%' => get_bloginfo('name') ?: 'Your Site Name',
1170 '%separator%' => ' ' . $separator_symbol . ' ',
1171 '%tagline%' => get_bloginfo('description') ?: 'Your Site Tagline',
1172 '%category%' => 'SEO Tips',
1173 '%author%' => 'John Doe',
1174 '%date%' => gmdate('F Y')
1175 ];
1176
1177 return str_replace(array_keys($sample_data), array_values($sample_data), $template_content);
1178 }
1179
1180 /**
1181 * Store optimization results in seo_analysis table
1182 *
1183 * @since 1.0.0
1184 *
1185 * @param array $optimization Optimization results
1186 * @param string $focus Optimization focus section
1187 * @return void
1188 */
1189 private function store_optimization_results(array $optimization, string $focus): void {
1190 global $wpdb;
1191
1192 $table_name = $wpdb->prefix . 'thinkrank_seo_analysis';
1193
1194 // Only store if we have meaningful results
1195 if (empty($optimization['suggestions']) && empty($optimization['warnings'])) {
1196 return;
1197 }
1198
1199 $analysis_type = 'site_identity_rule_optimization';
1200 if ($focus !== 'all') {
1201 $analysis_type .= '_' . $focus;
1202 }
1203
1204 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Site identity analysis storage requires direct database access
1205 $wpdb->insert(
1206 $table_name,
1207 [
1208 'context_type' => 'site',
1209 'context_id' => null,
1210 'analysis_type' => $analysis_type,
1211 'analysis_data' => wp_json_encode($optimization),
1212 'score' => $optimization['score'],
1213 'status' => 'completed',
1214 'recommendations' => wp_json_encode($optimization['suggestions']),
1215 'validation_errors' => wp_json_encode($optimization['warnings']),
1216 'analyzed_by' => get_current_user_id()
1217 ],
1218 ['%s', '%d', '%s', '%s', '%d', '%s', '%s', '%s', '%d']
1219 );
1220 }
1221
1222 /**
1223 * Validate SEO settings (implements interface)
1224 *
1225 * @since 1.0.0
1226 *
1227 * @param array $settings Settings array to validate
1228 * @param string $tab_context Optional tab context for specific validation
1229 * @return array Validation results
1230 */
1231 public function validate_settings(array $settings, string $tab_context = ''): array {
1232 // If tab context is provided, use tab-specific validation
1233 if (!empty($tab_context)) {
1234 return $this->get_tab_specific_validation($settings, $tab_context);
1235 }
1236
1237 // Default comprehensive validation for backward compatibility
1238 $validation = [
1239 'valid' => true,
1240 'errors' => [],
1241 'warnings' => [],
1242 'suggestions' => [],
1243 'score' => 100
1244 ];
1245
1246 // Validate title template
1247 if (isset($settings['title_template'])) {
1248 if (!isset($this->title_templates[$settings['title_template']])) {
1249 $validation['errors'][] = 'Invalid title template specified';
1250 $validation['valid'] = false;
1251 }
1252 }
1253
1254 // Validate title separator
1255 if (isset($settings['title_separator'])) {
1256 if (!isset($this->title_separators[$settings['title_separator']])) {
1257 $validation['errors'][] = 'Invalid title separator specified';
1258 $validation['valid'] = false;
1259 }
1260 }
1261
1262 // Validate site name
1263 if (isset($settings['site_name'])) {
1264 if (empty($settings['site_name'])) {
1265 $validation['errors'][] = 'Site name is required';
1266 $validation['valid'] = false;
1267 } elseif (strlen($settings['site_name']) > 60) {
1268 $validation['warnings'][] = 'Site name is longer than 60 characters, may be truncated';
1269 }
1270 }
1271
1272 // Validate site description
1273 if (isset($settings['site_description']) && !empty($settings['site_description'])) {
1274 if (strlen($settings['site_description']) > 160) {
1275 $validation['warnings'][] = 'Site description is longer than 160 characters, may be truncated';
1276 } elseif (strlen($settings['site_description']) < 120) {
1277 $validation['suggestions'][] = 'Consider making site description longer (120-160 characters)';
1278 }
1279 }
1280
1281 // Validate logo URL
1282 if (isset($settings['logo_url']) && !empty($settings['logo_url'])) {
1283 if (!filter_var($settings['logo_url'], FILTER_VALIDATE_URL)) {
1284 $validation['errors'][] = 'Logo URL must be a valid URL';
1285 $validation['valid'] = false;
1286 }
1287 }
1288
1289 // Validate breadcrumb settings
1290 if (isset($settings['breadcrumb_type'])) {
1291 if (!isset($this->breadcrumb_types[$settings['breadcrumb_type']])) {
1292 $validation['errors'][] = 'Invalid breadcrumb type specified';
1293 $validation['valid'] = false;
1294 }
1295 }
1296
1297 // Validate robots.txt settings
1298 if (isset($settings['robots_txt_enabled']) && $settings['robots_txt_enabled']) {
1299 if (!$this->is_directory_writable(ABSPATH)) {
1300 $validation['warnings'][] = 'WordPress root directory is not writable, robots.txt cannot be automatically managed';
1301 }
1302 }
1303
1304 // Validate local SEO settings if enabled
1305 if (isset($settings['local_seo_enabled']) && $settings['local_seo_enabled']) {
1306 $local_seo_validation = $this->validate_local_seo_settings($settings);
1307 $validation['errors'] = array_merge($validation['errors'], $local_seo_validation['errors']);
1308 $validation['warnings'] = array_merge($validation['warnings'], $local_seo_validation['warnings']);
1309 $validation['suggestions'] = array_merge($validation['suggestions'], $local_seo_validation['suggestions']);
1310
1311 if (!$local_seo_validation['valid']) {
1312 $validation['valid'] = false;
1313 }
1314 }
1315
1316 // Calculate validation score
1317 $validation['score'] = $this->calculate_validation_score($validation);
1318
1319 // Add detailed field validation breakdown for generic validation
1320 $validation['field_details'] = $this->get_detailed_field_validation($settings, '');
1321
1322 return $validation;
1323 }
1324
1325 /**
1326 * Get tab-specific validation
1327 *
1328 * @since 1.0.0
1329 *
1330 * @param array $settings Settings array to validate
1331 * @param string $tab_context Tab context for specific validation
1332 * @return array Tab-specific validation results
1333 */
1334 private function get_tab_specific_validation(array $settings, string $tab_context): array {
1335 $validation = [
1336 'valid' => true,
1337 'errors' => [],
1338 'warnings' => [],
1339 'suggestions' => [],
1340 'score' => 100
1341 ];
1342
1343 // Get tab-specific field details
1344 $field_details = $this->get_detailed_field_validation($settings, $tab_context);
1345
1346 // Convert field details to validation format
1347 foreach ($field_details as $field) {
1348 switch ($field['status']) {
1349 case 'error':
1350 $validation['errors'][] = $field['label'];
1351 $validation['valid'] = false;
1352 $validation['score'] -= 20;
1353 break;
1354 case 'warning':
1355 $validation['warnings'][] = $field['label'];
1356 $validation['score'] -= 10;
1357 break;
1358 case 'suggestion':
1359 $validation['suggestions'][] = $field['label'];
1360 $validation['score'] -= 5;
1361 break;
1362 }
1363 }
1364
1365 // Ensure score doesn't go below 0
1366 $validation['score'] = max(0, $validation['score']);
1367
1368 // Add field details for frontend display
1369 $validation['field_details'] = $field_details;
1370
1371 return $validation;
1372 }
1373
1374 /**
1375 * Get detailed field validation breakdown
1376 *
1377 * @since 1.0.0
1378 *
1379 * @param array $settings Settings array to validate
1380 * @param string $tab_context Tab context for specific validation
1381 * @return array Detailed field validation results
1382 */
1383 private function get_detailed_field_validation(array $settings, string $tab_context = ''): array {
1384 $field_details = [];
1385
1386 // Return tab-specific validation based on context
1387 switch ($tab_context) {
1388 case 'local-seo':
1389 return $this->get_business_info_validation($settings);
1390 case 'hero-section':
1391 return $this->get_hero_section_validation($settings);
1392 case 'title-formats':
1393 return $this->get_title_formats_validation($settings);
1394 case 'breadcrumbs':
1395 return $this->get_breadcrumbs_validation($settings);
1396 default:
1397 // Default basic info validation
1398 return $this->get_basic_info_validation($settings);
1399 }
1400 }
1401
1402 /**
1403 * Get Business Info specific validation
1404 *
1405 * @since 1.0.0
1406 *
1407 * @param array $settings Settings array to validate
1408 * @return array Business Info validation results
1409 */
1410 private function get_business_info_validation(array $settings): array {
1411 $field_details = [];
1412
1413 // Check if Local SEO is enabled
1414 if (empty($settings['local_seo_enabled'])) {
1415 $field_details[] = [
1416 'field' => 'local_seo_enabled',
1417 'label' => 'Local SEO is disabled. Enable to configure business information.',
1418 'status' => 'warning',
1419 'icon' => ''
1420 ];
1421 return $field_details;
1422 }
1423
1424 // Business Name validation
1425 if (!empty($settings['business_name'])) {
1426 $field_details[] = [
1427 'field' => 'business_name',
1428 'label' => 'Business name is properly configured.',
1429 'status' => 'valid',
1430 'icon' => ''
1431 ];
1432 } else {
1433 $field_details[] = [
1434 'field' => 'business_name',
1435 'label' => 'Business name is required for local SEO.',
1436 'status' => 'error',
1437 'icon' => ''
1438 ];
1439 }
1440
1441 // Business Type validation
1442 if (!empty($settings['business_type']) && $settings['business_type'] !== 'LocalBusiness') {
1443 $field_details[] = [
1444 'field' => 'business_type',
1445 'label' => 'Business type is selected for proper schema markup.',
1446 'status' => 'valid',
1447 'icon' => ''
1448 ];
1449 } else {
1450 $field_details[] = [
1451 'field' => 'business_type',
1452 'label' => 'Specific business type selection recommended for better schema markup.',
1453 'status' => 'suggestion',
1454 'icon' => ''
1455 ];
1456 }
1457
1458 // Address validation (NAP consistency)
1459 $address_fields = ['business_address', 'business_city', 'business_state', 'business_country'];
1460 $address_complete = true;
1461 foreach ($address_fields as $field) {
1462 if (empty($settings[$field])) {
1463 $address_complete = false;
1464 break;
1465 }
1466 }
1467
1468 if ($address_complete) {
1469 $field_details[] = [
1470 'field' => 'business_address',
1471 'label' => 'Complete business address is configured for NAP consistency.',
1472 'status' => 'valid',
1473 'icon' => ''
1474 ];
1475 } else {
1476 $field_details[] = [
1477 'field' => 'business_address',
1478 'label' => 'Complete address (street, city, state, country) required for local SEO.',
1479 'status' => 'error',
1480 'icon' => ''
1481 ];
1482 }
1483
1484 // Phone validation
1485 if (!empty($settings['business_phone'])) {
1486 if ($this->validate_phone_format($settings['business_phone'])) {
1487 $field_details[] = [
1488 'field' => 'business_phone',
1489 'label' => 'Business phone number is properly formatted.',
1490 'status' => 'valid',
1491 'icon' => ''
1492 ];
1493 } else {
1494 $field_details[] = [
1495 'field' => 'business_phone',
1496 'label' => 'Business phone number format could be improved.',
1497 'status' => 'warning',
1498 'icon' => ''
1499 ];
1500 }
1501 } else {
1502 $field_details[] = [
1503 'field' => 'business_phone',
1504 'label' => 'Business phone number is important for local SEO and customer contact.',
1505 'status' => 'warning',
1506 'icon' => ''
1507 ];
1508 }
1509
1510 // Email validation
1511 if (!empty($settings['business_email'])) {
1512 if (is_email($settings['business_email'])) {
1513 $field_details[] = [
1514 'field' => 'business_email',
1515 'label' => 'Business email address is valid.',
1516 'status' => 'valid',
1517 'icon' => ''
1518 ];
1519 } else {
1520 $field_details[] = [
1521 'field' => 'business_email',
1522 'label' => 'Business email address format is invalid.',
1523 'status' => 'error',
1524 'icon' => ''
1525 ];
1526 }
1527 } else {
1528 $field_details[] = [
1529 'field' => 'business_email',
1530 'label' => 'Business email address recommended for contact information.',
1531 'status' => 'suggestion',
1532 'icon' => ''
1533 ];
1534 }
1535
1536 // Coordinates validation
1537 if (!empty($settings['business_latitude']) && !empty($settings['business_longitude'])) {
1538 if ($this->validate_coordinates($settings['business_latitude'], $settings['business_longitude'])) {
1539 $field_details[] = [
1540 'field' => 'business_coordinates',
1541 'label' => 'Business coordinates are properly configured for precise location.',
1542 'status' => 'valid',
1543 'icon' => ''
1544 ];
1545 } else {
1546 $field_details[] = [
1547 'field' => 'business_coordinates',
1548 'label' => 'Business coordinates appear to be invalid.',
1549 'status' => 'error',
1550 'icon' => ''
1551 ];
1552 }
1553 } else {
1554 $field_details[] = [
1555 'field' => 'business_coordinates',
1556 'label' => 'Business coordinates recommended for precise location targeting.',
1557 'status' => 'suggestion',
1558 'icon' => ''
1559 ];
1560 }
1561
1562 return $field_details;
1563 }
1564
1565 /**
1566 * Get Basic Info validation (default)
1567 *
1568 * @since 1.0.0
1569 *
1570 * @param array $settings Settings array to validate
1571 * @return array Basic Info validation results
1572 */
1573 private function get_basic_info_validation(array $settings): array {
1574 $field_details = [];
1575
1576 // Site Name validation
1577 if (!empty($settings['site_name'])) {
1578 $field_details[] = [
1579 'field' => 'site_name',
1580 'label' => 'Site name is properly configured.',
1581 'status' => 'valid',
1582 'icon' => ''
1583 ];
1584 } else {
1585 $field_details[] = [
1586 'field' => 'site_name',
1587 'label' => 'Site name is required.',
1588 'status' => 'error',
1589 'icon' => ''
1590 ];
1591 }
1592
1593 // Site Description validation
1594 if (!empty($settings['site_description'])) {
1595 $length = strlen($settings['site_description']);
1596 if ($length >= 120 && $length <= 160) {
1597 $field_details[] = [
1598 'field' => 'site_description',
1599 'label' => 'Site description is properly configured.',
1600 'status' => 'valid',
1601 'icon' => ''
1602 ];
1603 } else {
1604 $field_details[] = [
1605 'field' => 'site_description',
1606 'label' => 'Site description length could be optimized (120-160 characters recommended).',
1607 'status' => 'warning',
1608 'icon' => ''
1609 ];
1610 }
1611 } else {
1612 $field_details[] = [
1613 'field' => 'site_description',
1614 'label' => 'Site description is recommended for better SEO.',
1615 'status' => 'warning',
1616 'icon' => ''
1617 ];
1618 }
1619
1620 // Tagline validation
1621 if (!empty($settings['tagline'])) {
1622 $field_details[] = [
1623 'field' => 'tagline',
1624 'label' => 'Site tagline is configured.',
1625 'status' => 'valid',
1626 'icon' => ''
1627 ];
1628 } else {
1629 $field_details[] = [
1630 'field' => 'tagline',
1631 'label' => 'Site tagline recommended for better branding.',
1632 'status' => 'suggestion',
1633 'icon' => ''
1634 ];
1635 }
1636
1637 // Default Meta Description validation
1638 if (!empty($settings['default_meta_description'])) {
1639 $length = strlen($settings['default_meta_description']);
1640 if ($length >= 120 && $length <= 160) {
1641 $field_details[] = [
1642 'field' => 'default_meta_description',
1643 'label' => 'Default meta description is properly configured.',
1644 'status' => 'valid',
1645 'icon' => ''
1646 ];
1647 } else {
1648 $field_details[] = [
1649 'field' => 'default_meta_description',
1650 'label' => 'Default meta description length could be optimized (120-160 characters recommended).',
1651 'status' => 'warning',
1652 'icon' => ''
1653 ];
1654 }
1655 } else {
1656 $field_details[] = [
1657 'field' => 'default_meta_description',
1658 'label' => 'Default meta description recommended for pages without specific descriptions.',
1659 'status' => 'suggestion',
1660 'icon' => ''
1661 ];
1662 }
1663
1664 return $field_details;
1665 }
1666
1667 /**
1668 * Get Hero Section validation
1669 *
1670 * @since 1.0.0
1671 *
1672 * @param array $settings Settings array to validate
1673 * @return array Hero Section validation results
1674 */
1675 private function get_hero_section_validation(array $settings): array {
1676 $field_details = [];
1677
1678 // Hero Title validation
1679 if (!empty($settings['hero_title'])) {
1680 $field_details[] = [
1681 'field' => 'hero_title',
1682 'label' => 'Hero title is configured.',
1683 'status' => 'valid',
1684 'icon' => ''
1685 ];
1686 } else {
1687 $field_details[] = [
1688 'field' => 'hero_title',
1689 'label' => 'Hero title recommended for better homepage presentation.',
1690 'status' => 'suggestion',
1691 'icon' => ''
1692 ];
1693 }
1694
1695 // Hero Subtitle validation (correct field name)
1696 if (!empty($settings['hero_subtitle'])) {
1697 $field_details[] = [
1698 'field' => 'hero_subtitle',
1699 'label' => 'Hero subtitle is configured.',
1700 'status' => 'valid',
1701 'icon' => ''
1702 ];
1703 } else {
1704 $field_details[] = [
1705 'field' => 'hero_subtitle',
1706 'label' => 'Hero subtitle recommended for better user engagement.',
1707 'status' => 'suggestion',
1708 'icon' => ''
1709 ];
1710 }
1711
1712 // CTA Text validation
1713 if (!empty($settings['hero_cta_text'])) {
1714 $field_details[] = [
1715 'field' => 'hero_cta_text',
1716 'label' => 'Call-to-action text is configured.',
1717 'status' => 'valid',
1718 'icon' => ''
1719 ];
1720 } else {
1721 $field_details[] = [
1722 'field' => 'hero_cta_text',
1723 'label' => 'Call-to-action text recommended for better conversion.',
1724 'status' => 'suggestion',
1725 'icon' => ''
1726 ];
1727 }
1728
1729 // CTA URL validation
1730 if (!empty($settings['hero_cta_url'])) {
1731 if (filter_var($settings['hero_cta_url'], FILTER_VALIDATE_URL) || strpos($settings['hero_cta_url'], '/') === 0) {
1732 $field_details[] = [
1733 'field' => 'hero_cta_url',
1734 'label' => 'Call-to-action URL is properly configured.',
1735 'status' => 'valid',
1736 'icon' => ''
1737 ];
1738 } else {
1739 $field_details[] = [
1740 'field' => 'hero_cta_url',
1741 'label' => 'Call-to-action URL format appears invalid.',
1742 'status' => 'warning',
1743 'icon' => ''
1744 ];
1745 }
1746 } else {
1747 $field_details[] = [
1748 'field' => 'hero_cta_url',
1749 'label' => 'Call-to-action URL recommended for better conversion.',
1750 'status' => 'suggestion',
1751 'icon' => ''
1752 ];
1753 }
1754
1755 // Hero Background Image validation
1756 if (!empty($settings['hero_background_image'])) {
1757 $field_details[] = [
1758 'field' => 'hero_background_image',
1759 'label' => 'Hero background image is configured.',
1760 'status' => 'valid',
1761 'icon' => ''
1762 ];
1763 } else {
1764 $field_details[] = [
1765 'field' => 'hero_background_image',
1766 'label' => 'Hero background image recommended for visual appeal.',
1767 'status' => 'suggestion',
1768 'icon' => ''
1769 ];
1770 }
1771
1772 // Site Logo validation (from Site Assets section)
1773 if (!empty($settings['logo_url'])) {
1774 if (filter_var($settings['logo_url'], FILTER_VALIDATE_URL)) {
1775 $field_details[] = [
1776 'field' => 'logo_url',
1777 'label' => 'Site logo is properly configured.',
1778 'status' => 'valid',
1779 'icon' => ''
1780 ];
1781 } else {
1782 $field_details[] = [
1783 'field' => 'logo_url',
1784 'label' => 'Site logo URL format appears invalid.',
1785 'status' => 'warning',
1786 'icon' => ''
1787 ];
1788 }
1789 } else {
1790 $field_details[] = [
1791 'field' => 'logo_url',
1792 'label' => 'Site logo recommended for branding and schema markup.',
1793 'status' => 'suggestion',
1794 'icon' => ''
1795 ];
1796 }
1797
1798 // Favicon validation
1799 if (!empty($settings['favicon_url'])) {
1800 $field_details[] = [
1801 'field' => 'favicon_url',
1802 'label' => 'Favicon is configured.',
1803 'status' => 'valid',
1804 'icon' => ''
1805 ];
1806 } else {
1807 $field_details[] = [
1808 'field' => 'favicon_url',
1809 'label' => 'Favicon recommended for browser tab identification.',
1810 'status' => 'suggestion',
1811 'icon' => ''
1812 ];
1813 }
1814
1815 // Apple Touch Icon validation
1816 if (!empty($settings['apple_touch_icon_url'])) {
1817 $field_details[] = [
1818 'field' => 'apple_touch_icon_url',
1819 'label' => 'Apple touch icon is configured.',
1820 'status' => 'valid',
1821 'icon' => ''
1822 ];
1823 } else {
1824 $field_details[] = [
1825 'field' => 'apple_touch_icon_url',
1826 'label' => 'Apple touch icon recommended for iOS devices.',
1827 'status' => 'suggestion',
1828 'icon' => ''
1829 ];
1830 }
1831
1832 return $field_details;
1833 }
1834
1835 /**
1836 * Get Title Formats validation
1837 *
1838 * @since 1.0.0
1839 *
1840 * @param array $settings Settings array to validate
1841 * @return array Title Formats validation results
1842 */
1843 private function get_title_formats_validation(array $settings): array {
1844 $field_details = [];
1845
1846 // Title Separator validation
1847 if (!empty($settings['title_separator'])) {
1848 $field_details[] = [
1849 'field' => 'title_separator',
1850 'label' => 'Title separator is properly configured.',
1851 'status' => 'valid',
1852 'icon' => ''
1853 ];
1854 } else {
1855 $field_details[] = [
1856 'field' => 'title_separator',
1857 'label' => 'Title separator is required.',
1858 'status' => 'error',
1859 'icon' => ''
1860 ];
1861 }
1862
1863 // Homepage Title validation
1864 if (!empty($settings['homepage_title'])) {
1865 $field_details[] = [
1866 'field' => 'homepage_title',
1867 'label' => 'Homepage title format is configured.',
1868 'status' => 'valid',
1869 'icon' => ''
1870 ];
1871 } else {
1872 $field_details[] = [
1873 'field' => 'homepage_title',
1874 'label' => 'Homepage title format recommended.',
1875 'status' => 'suggestion',
1876 'icon' => ''
1877 ];
1878 }
1879
1880 // Post Title validation
1881 if (!empty($settings['post_title'])) {
1882 $field_details[] = [
1883 'field' => 'post_title',
1884 'label' => 'Post title format is configured.',
1885 'status' => 'valid',
1886 'icon' => ''
1887 ];
1888 } else {
1889 $field_details[] = [
1890 'field' => 'post_title',
1891 'label' => 'Post title format recommended.',
1892 'status' => 'suggestion',
1893 'icon' => ''
1894 ];
1895 }
1896
1897 // Page Title validation
1898 if (!empty($settings['page_title'])) {
1899 $field_details[] = [
1900 'field' => 'page_title',
1901 'label' => 'Page title format is configured.',
1902 'status' => 'valid',
1903 'icon' => ''
1904 ];
1905 } else {
1906 $field_details[] = [
1907 'field' => 'page_title',
1908 'label' => 'Page title format recommended.',
1909 'status' => 'suggestion',
1910 'icon' => ''
1911 ];
1912 }
1913
1914 // Category Title validation
1915 if (!empty($settings['category_title'])) {
1916 $field_details[] = [
1917 'field' => 'category_title',
1918 'label' => 'Category title format is configured.',
1919 'status' => 'valid',
1920 'icon' => ''
1921 ];
1922 } else {
1923 $field_details[] = [
1924 'field' => 'category_title',
1925 'label' => 'Category title format recommended.',
1926 'status' => 'suggestion',
1927 'icon' => ''
1928 ];
1929 }
1930
1931 // Search Title validation
1932 if (!empty($settings['search_title'])) {
1933 $field_details[] = [
1934 'field' => 'search_title',
1935 'label' => 'Search title format is configured.',
1936 'status' => 'valid',
1937 'icon' => ''
1938 ];
1939 } else {
1940 $field_details[] = [
1941 'field' => 'search_title',
1942 'label' => 'Search title format recommended.',
1943 'status' => 'suggestion',
1944 'icon' => ''
1945 ];
1946 }
1947
1948 return $field_details;
1949 }
1950
1951 /**
1952 * Get Breadcrumbs validation
1953 *
1954 * @since 1.0.0
1955 *
1956 * @param array $settings Settings array to validate
1957 * @return array Breadcrumbs validation results
1958 */
1959 private function get_breadcrumbs_validation(array $settings): array {
1960 $field_details = [];
1961
1962 // Breadcrumbs enabled validation
1963 if (!empty($settings['breadcrumbs_enabled'])) {
1964 $field_details[] = [
1965 'field' => 'breadcrumbs_enabled',
1966 'label' => 'Breadcrumbs are enabled for better navigation.',
1967 'status' => 'valid',
1968 'icon' => ''
1969 ];
1970
1971 // Only validate other fields if breadcrumbs are enabled
1972 // Breadcrumb Type validation
1973 if (!empty($settings['breadcrumb_type'])) {
1974 $field_details[] = [
1975 'field' => 'breadcrumb_type',
1976 'label' => 'Breadcrumb type is properly configured.',
1977 'status' => 'valid',
1978 'icon' => ''
1979 ];
1980 } else {
1981 $field_details[] = [
1982 'field' => 'breadcrumb_type',
1983 'label' => 'Breadcrumb type selection is required.',
1984 'status' => 'error',
1985 'icon' => ''
1986 ];
1987 }
1988
1989 // Home Text validation
1990 if (!empty($settings['breadcrumb_home_text'])) {
1991 $field_details[] = [
1992 'field' => 'breadcrumb_home_text',
1993 'label' => 'Home breadcrumb text is configured.',
1994 'status' => 'valid',
1995 'icon' => ''
1996 ];
1997 } else {
1998 $field_details[] = [
1999 'field' => 'breadcrumb_home_text',
2000 'label' => 'Home breadcrumb text recommended for clarity.',
2001 'status' => 'suggestion',
2002 'icon' => ''
2003 ];
2004 }
2005
2006 // Breadcrumb Separator validation
2007 if (!empty($settings['breadcrumb_separator'])) {
2008 $field_details[] = [
2009 'field' => 'breadcrumb_separator',
2010 'label' => 'Breadcrumb separator is configured.',
2011 'status' => 'valid',
2012 'icon' => ''
2013 ];
2014 } else {
2015 $field_details[] = [
2016 'field' => 'breadcrumb_separator',
2017 'label' => 'Breadcrumb separator recommended for better formatting.',
2018 'status' => 'suggestion',
2019 'icon' => ''
2020 ];
2021 }
2022
2023 // Breadcrumb Prefix validation (optional)
2024 if (!empty($settings['breadcrumb_prefix'])) {
2025 $field_details[] = [
2026 'field' => 'breadcrumb_prefix',
2027 'label' => 'Breadcrumb prefix is configured.',
2028 'status' => 'valid',
2029 'icon' => ''
2030 ];
2031 } else {
2032 $field_details[] = [
2033 'field' => 'breadcrumb_prefix',
2034 'label' => 'Breadcrumb prefix is optional but can improve user guidance.',
2035 'status' => 'suggestion',
2036 'icon' => ''
2037 ];
2038 }
2039
2040 // Show Current Page validation
2041 $field_details[] = [
2042 'field' => 'show_current_page',
2043 'label' => isset($settings['show_current_page']) ?
2044 'Current page display preference is configured.' :
2045 'Current page display preference is set to default.',
2046 'status' => 'valid',
2047 'icon' => ''
2048 ];
2049
2050 } else {
2051 $field_details[] = [
2052 'field' => 'breadcrumbs_enabled',
2053 'label' => 'Breadcrumbs recommended for better user experience and SEO.',
2054 'status' => 'suggestion',
2055 'icon' => ''
2056 ];
2057 }
2058
2059 return $field_details;
2060 }
2061
2062 /**
2063 * Validate local SEO settings
2064 *
2065 * @since 1.0.0
2066 *
2067 * @param array $settings Settings array to validate
2068 * @return array Local SEO validation results
2069 */
2070 private function validate_local_seo_settings(array $settings): array {
2071 $validation = [
2072 'valid' => true,
2073 'errors' => [],
2074 'warnings' => [],
2075 'suggestions' => []
2076 ];
2077
2078 // Validate business name (required for local SEO)
2079 if (empty($settings['business_name'])) {
2080 $validation['errors'][] = 'Business name is required when local SEO is enabled';
2081 $validation['valid'] = false;
2082 } elseif (strlen($settings['business_name']) > 100) {
2083 $validation['warnings'][] = 'Business name is very long, consider shortening for better display';
2084 }
2085
2086 // Validate business address components (NAP consistency)
2087 $required_address_fields = [
2088 'business_address' => 'Business address',
2089 'business_city' => 'Business city',
2090 'business_state' => 'Business state/province',
2091 'business_country' => 'Business country'
2092 ];
2093
2094 foreach ($required_address_fields as $field => $label) {
2095 if (empty($settings[$field])) {
2096 $validation['warnings'][] = "{$label} is missing - important for NAP consistency and local search";
2097 }
2098 }
2099
2100 // Validate postal code (recommended)
2101 if (empty($settings['business_postal_code'])) {
2102 $validation['suggestions'][] = 'Add postal code for more precise location targeting';
2103 }
2104
2105 // Validate phone number
2106 if (empty($settings['business_phone'])) {
2107 $validation['warnings'][] = 'Business phone number is missing - important for local SEO and customer contact';
2108 } elseif (!$this->validate_phone_format($settings['business_phone'])) {
2109 $validation['suggestions'][] = 'Phone number format could be improved for consistency';
2110 }
2111
2112 // Validate email address
2113 if (!empty($settings['business_email']) && !is_email($settings['business_email'])) {
2114 $validation['errors'][] = 'Business email address format is invalid';
2115 $validation['valid'] = false;
2116 }
2117
2118 // Validate coordinates if provided
2119 if (!empty($settings['business_latitude']) || !empty($settings['business_longitude'])) {
2120 if (empty($settings['business_latitude']) || empty($settings['business_longitude'])) {
2121 $validation['warnings'][] = 'Both latitude and longitude are required for geo-location';
2122 } elseif (!$this->validate_coordinates($settings['business_latitude'], $settings['business_longitude'])) {
2123 $validation['errors'][] = 'Invalid latitude or longitude coordinates';
2124 $validation['valid'] = false;
2125 }
2126 } else {
2127 $validation['suggestions'][] = 'Add latitude and longitude coordinates for precise location targeting';
2128 }
2129
2130 // Validate business hours
2131 if (!empty($settings['business_hours']) && is_array($settings['business_hours'])) {
2132 $hours_validation = $this->validate_business_hours($settings['business_hours']);
2133 if (!$hours_validation['valid']) {
2134 $validation['warnings'] = array_merge($validation['warnings'], $hours_validation['warnings']);
2135 }
2136 } else {
2137 $validation['suggestions'][] = 'Add business hours to improve local search visibility';
2138 }
2139
2140 // Validate business type
2141 if (empty($settings['business_type'])) {
2142 $validation['suggestions'][] = 'Select a specific business type for better schema markup';
2143 }
2144
2145 return $validation;
2146 }
2147
2148 /**
2149 * Validate phone number format
2150 *
2151 * @since 1.0.0
2152 *
2153 * @param string $phone_number Phone number to validate
2154 * @return bool True if format is acceptable
2155 */
2156 private function validate_phone_format(string $phone_number): bool {
2157 // Remove all non-numeric characters except + for international numbers
2158 $cleaned = preg_replace('/[^\d+]/', '', $phone_number);
2159
2160 // Check for common valid formats
2161 return (
2162 preg_match('/^\d{10}$/', $cleaned) || // 10 digits (US)
2163 preg_match('/^1\d{10}$/', $cleaned) || // 1 + 10 digits (US with country code)
2164 preg_match('/^\+\d{7,15}$/', $cleaned) // International format
2165 );
2166 }
2167
2168 /**
2169 * Get output data for frontend rendering (implements interface)
2170 *
2171 * @since 1.0.0
2172 *
2173 * @param string $context_type The context type
2174 * @param int|null $context_id Optional. Context ID
2175 * @return array Output data ready for frontend rendering
2176 */
2177 public function get_output_data(string $context_type, ?int $context_id): array {
2178 $settings = $this->get_settings($context_type, $context_id);
2179
2180 $output = [
2181 'title' => '',
2182 'breadcrumbs' => [],
2183 'identity' => [],
2184 'robots_txt' => [],
2185 'enabled' => $settings['enabled'] ?? true
2186 ];
2187
2188 if (!$output['enabled']) {
2189 return $output;
2190 }
2191
2192 // Generate title for current context
2193 $title_data = $this->extract_title_data($context_type, $context_id);
2194 $output['title'] = $this->generate_title(
2195 $settings['title_template'] ?? 'default',
2196 $title_data,
2197 $context_type
2198 );
2199
2200 // Generate breadcrumbs if enabled
2201 if (!empty($settings['breadcrumbs_enabled'])) {
2202 $breadcrumb_options = [
2203 'context_type' => $context_type,
2204 'context_id' => $context_id
2205 ];
2206 $output['breadcrumbs'] = $this->generate_breadcrumbs(
2207 $settings['breadcrumb_type'] ?? 'hierarchical',
2208 $breadcrumb_options
2209 );
2210 }
2211
2212 // Get site identity data
2213 $output['identity'] = $this->get_site_identity_data($settings);
2214
2215 // Get robots.txt data if enabled
2216 if (!empty($settings['robots_txt_enabled'])) {
2217 $output['robots_txt'] = $this->generate_robots_txt($settings['custom_robots_rules'] ?? []);
2218 }
2219
2220 return $output;
2221 }
2222
2223 /**
2224 * Get default settings for a context type (implements interface)
2225 *
2226 * @since 1.0.0
2227 *
2228 * @param string $context_type The context type to get defaults for
2229 * @return array Default settings array
2230 */
2231 public function get_default_settings(string $context_type): array {
2232 $defaults = [
2233 'enabled' => true,
2234 'title_template' => 'default',
2235 'title_separator' => 'pipe',
2236 'site_name' => get_bloginfo('name'),
2237 'site_description' => get_bloginfo('description'),
2238 'tagline' => get_bloginfo('description'),
2239 'breadcrumbs_enabled' => true,
2240 'breadcrumb_type' => 'hierarchical',
2241 'breadcrumb_home_text' => 'Home',
2242 'breadcrumb_separator' => '>',
2243 'robots_txt_enabled' => true,
2244 'allow_search_engines' => true,
2245 'robots_txt_content' => '',
2246 'logo_url' => '',
2247 'favicon_url' => '',
2248 'apple_touch_icon_url' => ''
2249 ];
2250
2251 // Context-specific defaults
2252 switch ($context_type) {
2253 case 'site':
2254 // Site-wide defaults are already set above
2255 break;
2256 case 'post':
2257 $defaults['title_template'] = 'default';
2258 $defaults['breadcrumb_type'] = 'taxonomy';
2259 break;
2260 case 'page':
2261 $defaults['title_template'] = 'default';
2262 $defaults['breadcrumb_type'] = 'hierarchical';
2263 break;
2264 case 'product':
2265 $defaults['title_template'] = 'category';
2266 $defaults['breadcrumb_type'] = 'taxonomy';
2267 break;
2268 }
2269
2270 return $defaults;
2271 }
2272
2273 /**
2274 * Get settings schema definition (implements interface)
2275 *
2276 * @since 1.0.0
2277 *
2278 * @param string $context_type The context type to get schema for
2279 * @return array Settings schema definition
2280 */
2281 public function get_settings_schema(string $context_type): array {
2282 return [
2283 'enabled' => [
2284 'type' => 'boolean',
2285 'title' => 'Enable Site Identity',
2286 'description' => 'Enable site identity management features',
2287 'default' => true
2288 ],
2289 'title_template' => [
2290 'type' => 'string',
2291 'title' => 'Title Template',
2292 'description' => 'Template for generating page titles',
2293 'enum' => array_keys($this->title_templates),
2294 'default' => 'default'
2295 ],
2296 'title_separator' => [
2297 'type' => 'string',
2298 'title' => 'Title Separator',
2299 'description' => 'Character used to separate title elements',
2300 'enum' => array_keys($this->title_separators),
2301 'default' => 'pipe'
2302 ],
2303 'site_name' => [
2304 'type' => 'string',
2305 'title' => 'Site Name',
2306 'description' => 'Official name of the website',
2307 'maxLength' => 60,
2308 'default' => get_bloginfo('name')
2309 ],
2310 'site_description' => [
2311 'type' => 'string',
2312 'title' => 'Site Description',
2313 'description' => 'Brief description of the website',
2314 'maxLength' => 160,
2315 'default' => get_bloginfo('description')
2316 ],
2317 'breadcrumbs_enabled' => [
2318 'type' => 'boolean',
2319 'title' => 'Enable Breadcrumbs',
2320 'description' => 'Enable breadcrumb navigation generation',
2321 'default' => true
2322 ],
2323 'breadcrumb_type' => [
2324 'type' => 'string',
2325 'title' => 'Breadcrumb Type',
2326 'description' => 'Type of breadcrumb navigation to generate',
2327 'enum' => array_keys($this->breadcrumb_types),
2328 'default' => 'hierarchical'
2329 ],
2330 'robots_txt_enabled' => [
2331 'type' => 'boolean',
2332 'title' => 'Enable Robots.txt Management',
2333 'description' => 'Enable automatic robots.txt generation and management',
2334 'default' => true
2335 ],
2336 'logo_url' => [
2337 'type' => 'string',
2338 'title' => 'Logo URL',
2339 'description' => 'URL of the site logo image',
2340 'format' => 'uri',
2341 'default' => ''
2342 ],
2343 'favicon_url' => [
2344 'type' => 'string',
2345 'title' => 'Favicon URL',
2346 'description' => 'URL of the site favicon',
2347 'format' => 'uri',
2348 'default' => ''
2349 ]
2350 ];
2351 }
2352
2353 /**
2354 * Prepare title placeholders for replacement
2355 *
2356 * @since 1.0.0
2357 *
2358 * @param array $data Content data
2359 * @param string $context Context type
2360 * @param array $settings Site settings
2361 * @return array Placeholder values
2362 */
2363 private function prepare_title_placeholders(array $data, string $context, array $settings): array {
2364 $placeholders = [
2365 '%title%' => $data['title'] ?? '',
2366 '%sitename%' => $settings['site_name'] ?? get_bloginfo('name'),
2367 '%tagline%' => $settings['tagline'] ?? get_bloginfo('description'),
2368 '%separator%' => '', // Will be replaced with actual separator
2369 '%category%' => '',
2370 '%author%' => '',
2371 '%date%' => '',
2372 '%searchterm%' => ''
2373 ];
2374
2375 // Context-specific placeholders
2376 switch ($context) {
2377 case 'post':
2378 case 'page':
2379 case 'product':
2380 if (!empty($data['context_id'])) {
2381 $post = get_post($data['context_id']);
2382 if ($post) {
2383 $placeholders['%title%'] = get_the_title($post);
2384 $placeholders['%author%'] = get_the_author_meta('display_name', $post->post_author);
2385 $placeholders['%date%'] = get_the_date('F j, Y', $post);
2386
2387 // Get primary category
2388 $categories = get_the_category($post->ID);
2389 if (!empty($categories)) {
2390 $placeholders['%category%'] = $categories[0]->name;
2391 }
2392 }
2393 }
2394 break;
2395 case 'search':
2396 $placeholders['%searchterm%'] = get_search_query();
2397 break;
2398 }
2399
2400 return $placeholders;
2401 }
2402
2403 /**
2404 * Replace title placeholders with actual values
2405 *
2406 * @since 1.0.0
2407 *
2408 * @param string $template Title template
2409 * @param array $placeholders Placeholder values
2410 * @param string $separator Title separator
2411 * @return string Processed title
2412 */
2413 private function replace_title_placeholders(string $template, array $placeholders, string $separator): string {
2414 // Replace separator placeholder
2415 $placeholders['%separator%'] = $separator;
2416
2417 // Replace all placeholders
2418 $title = str_replace(array_keys($placeholders), array_values($placeholders), $template);
2419
2420 // Clean up empty placeholders and extra separators
2421 $title = preg_replace('/\s*' . preg_quote($separator, '/') . '\s*' . preg_quote($separator, '/') . '\s*/', ' ' . $separator . ' ', $title);
2422 $title = preg_replace('/^\s*' . preg_quote($separator, '/') . '\s*|\s*' . preg_quote($separator, '/') . '\s*$/', '', $title);
2423
2424 return trim($title);
2425 }
2426
2427 /**
2428 * Get title separator symbol
2429 *
2430 * @since 1.0.0
2431 *
2432 * @param string $separator_key Separator key
2433 * @return string Separator symbol
2434 */
2435 private function get_title_separator(string $separator_key): string {
2436 return $this->title_separators[$separator_key]['symbol'] ?? $this->title_separators['pipe']['symbol'];
2437 }
2438
2439 /**
2440 * Optimize title for SEO
2441 *
2442 * @since 1.0.0
2443 *
2444 * @param string $title Title to optimize
2445 * @param string $context Context type
2446 * @return string Optimized title
2447 */
2448 private function optimize_title(string $title, string $context): string {
2449 // Remove extra whitespace
2450 $title = preg_replace('/\s+/', ' ', $title);
2451 $title = trim($title);
2452
2453 // Ensure title is not too long (60 characters max for SEO)
2454 if (strlen($title) > 60) {
2455 // Try to truncate at word boundary
2456 $title = wp_trim_words($title, 8, '...');
2457 if (strlen($title) > 60) {
2458 $title = substr($title, 0, 57) . '...';
2459 }
2460 }
2461
2462 // Ensure title is not empty
2463 if (empty($title)) {
2464 $title = get_bloginfo('name');
2465 }
2466
2467 return $title;
2468 }
2469
2470 /**
2471 * Extract title data from context
2472 *
2473 * @since 1.0.0
2474 *
2475 * @param string $context_type Context type
2476 * @param int|null $context_id Context ID
2477 * @return array Title data
2478 */
2479 private function extract_title_data(string $context_type, ?int $context_id): array {
2480 $data = [
2481 'title' => '',
2482 'context_type' => $context_type,
2483 'context_id' => $context_id
2484 ];
2485
2486 switch ($context_type) {
2487 case 'site':
2488 $data['title'] = get_bloginfo('name');
2489 break;
2490 case 'post':
2491 case 'page':
2492 case 'product':
2493 if ($context_id) {
2494 $data['title'] = get_the_title($context_id);
2495 }
2496 break;
2497 case 'search':
2498 $data['title'] = 'Search Results';
2499 break;
2500 case '404':
2501 $data['title'] = 'Page Not Found';
2502 break;
2503 }
2504
2505 return $data;
2506 }
2507
2508 /**
2509 * Generate hierarchical breadcrumbs
2510 *
2511 * @since 1.0.0
2512 *
2513 * @param array $options Breadcrumb options
2514 * @return array Breadcrumb items
2515 */
2516 private function generate_hierarchical_breadcrumbs(array $options): array {
2517 $breadcrumbs = [];
2518
2519 // Add home breadcrumb
2520 $breadcrumbs[] = [
2521 'title' => 'Home',
2522 'url' => home_url(),
2523 'position' => 1
2524 ];
2525
2526 $context_type = $options['context_type'] ?? '';
2527 $context_id = $options['context_id'] ?? null;
2528
2529 if ($context_type === 'post' || $context_type === 'page' || $context_type === 'product') {
2530 if ($context_id) {
2531 $post = get_post($context_id);
2532 if ($post) {
2533 // Add parent pages for hierarchical content
2534 $ancestors = get_post_ancestors($post);
2535 $ancestors = array_reverse($ancestors);
2536
2537 $position = 2;
2538 foreach ($ancestors as $ancestor_id) {
2539 $breadcrumbs[] = [
2540 'title' => get_the_title($ancestor_id),
2541 'url' => get_permalink($ancestor_id),
2542 'position' => $position++
2543 ];
2544 }
2545
2546 // Add current page
2547 $breadcrumbs[] = [
2548 'title' => get_the_title($post),
2549 'url' => get_permalink($post),
2550 'position' => $position,
2551 'current' => true
2552 ];
2553 }
2554 }
2555 }
2556
2557 return $breadcrumbs;
2558 }
2559
2560 /**
2561 * Generate taxonomy-based breadcrumbs
2562 *
2563 * @since 1.0.0
2564 *
2565 * @param array $options Breadcrumb options
2566 * @return array Breadcrumb items
2567 */
2568 private function generate_taxonomy_breadcrumbs(array $options): array {
2569 $breadcrumbs = [];
2570
2571 // Add home breadcrumb
2572 $breadcrumbs[] = [
2573 'title' => 'Home',
2574 'url' => home_url(),
2575 'position' => 1
2576 ];
2577
2578 $context_type = $options['context_type'] ?? '';
2579 $context_id = $options['context_id'] ?? null;
2580
2581 if (($context_type === 'post' || $context_type === 'product') && $context_id) {
2582 $post = get_post($context_id);
2583 if ($post) {
2584 // Get primary category
2585 $categories = get_the_category($post->ID);
2586 if (!empty($categories)) {
2587 $primary_category = $categories[0];
2588
2589 // Add category hierarchy
2590 $category_ancestors = get_ancestors($primary_category->term_id, 'category');
2591 $category_ancestors = array_reverse($category_ancestors);
2592
2593 $position = 2;
2594 foreach ($category_ancestors as $ancestor_id) {
2595 $ancestor = get_category($ancestor_id);
2596 $breadcrumbs[] = [
2597 'title' => $ancestor->name,
2598 'url' => get_category_link($ancestor_id),
2599 'position' => $position++
2600 ];
2601 }
2602
2603 // Add primary category
2604 $breadcrumbs[] = [
2605 'title' => $primary_category->name,
2606 'url' => get_category_link($primary_category->term_id),
2607 'position' => $position++
2608 ];
2609 }
2610
2611 // Add current post
2612 $breadcrumbs[] = [
2613 'title' => get_the_title($post),
2614 'url' => get_permalink($post),
2615 'position' => $position,
2616 'current' => true
2617 ];
2618 }
2619 }
2620
2621 return $breadcrumbs;
2622 }
2623
2624 /**
2625 * Generate path-based breadcrumbs
2626 *
2627 * @since 1.0.0
2628 *
2629 * @param array $options Breadcrumb options
2630 * @return array Breadcrumb items
2631 */
2632 private function generate_path_breadcrumbs(array $options): array {
2633 $breadcrumbs = [];
2634
2635 // Add home breadcrumb
2636 $breadcrumbs[] = [
2637 'title' => 'Home',
2638 'url' => home_url(),
2639 'position' => 1
2640 ];
2641
2642 // Get current URL path
2643 $current_url = home_url(add_query_arg([]));
2644 $path = wp_parse_url($current_url, PHP_URL_PATH);
2645 $path_parts = array_filter(explode('/', trim($path, '/')));
2646
2647 $position = 2;
2648 $cumulative_path = '';
2649
2650 foreach ($path_parts as $part) {
2651 $cumulative_path .= '/' . $part;
2652 $url = home_url($cumulative_path);
2653
2654 // Try to get a meaningful title
2655 $title = ucwords(str_replace(['-', '_'], ' ', $part));
2656
2657 $breadcrumbs[] = [
2658 'title' => $title,
2659 'url' => $url,
2660 'position' => $position++,
2661 'current' => $cumulative_path === $path
2662 ];
2663 }
2664
2665 return $breadcrumbs;
2666 }
2667
2668 /**
2669 * Generate custom breadcrumbs
2670 *
2671 * @since 1.0.0
2672 *
2673 * @param array $options Breadcrumb options
2674 * @return array Breadcrumb items
2675 */
2676 private function generate_custom_breadcrumbs(array $options): array {
2677 // Return custom breadcrumbs if provided in options
2678 return $options['custom_breadcrumbs'] ?? [];
2679 }
2680
2681 /**
2682 * Generate breadcrumb schema markup
2683 *
2684 * @since 1.0.0
2685 *
2686 * @param array $breadcrumb_items Breadcrumb items
2687 * @return array Schema markup
2688 */
2689 private function generate_breadcrumb_schema(array $breadcrumb_items): array {
2690 $schema = [
2691 '@context' => 'https://schema.org',
2692 '@type' => 'BreadcrumbList',
2693 'itemListElement' => []
2694 ];
2695
2696 foreach ($breadcrumb_items as $item) {
2697 $schema['itemListElement'][] = [
2698 '@type' => 'ListItem',
2699 'position' => $item['position'],
2700 'name' => $item['title'],
2701 'item' => $item['url']
2702 ];
2703 }
2704
2705 return $schema;
2706 }
2707
2708 /**
2709 * Generate breadcrumb HTML
2710 *
2711 * @since 1.0.0
2712 *
2713 * @param array $breadcrumb_items Breadcrumb items
2714 * @param array $settings Breadcrumb settings
2715 * @return string HTML output
2716 */
2717 private function generate_breadcrumb_html(array $breadcrumb_items, array $settings): string {
2718 if (empty($breadcrumb_items)) {
2719 return '';
2720 }
2721
2722 $separator = $settings['separator'] ?? '>';
2723 $html = '<nav class="thinkrank-breadcrumbs" aria-label="Breadcrumb">';
2724 $html .= '<ol class="breadcrumb-list">';
2725
2726 foreach ($breadcrumb_items as $item) {
2727 $html .= '<li class="breadcrumb-item">';
2728
2729 if (!empty($item['current'])) {
2730 $html .= '<span class="breadcrumb-current" aria-current="page">' . esc_html($item['title']) . '</span>';
2731 } else {
2732 $html .= '<a href="' . esc_url($item['url']) . '">' . esc_html($item['title']) . '</a>';
2733 }
2734
2735 if ($item['position'] < count($breadcrumb_items)) {
2736 $html .= ' <span class="breadcrumb-separator">' . esc_html($separator) . '</span> ';
2737 }
2738
2739 $html .= '</li>';
2740 }
2741
2742 $html .= '</ol>';
2743 $html .= '</nav>';
2744
2745 return $html;
2746 }
2747
2748 /**
2749 * Generate default robots.txt rules
2750 *
2751 * @since 1.0.0
2752 *
2753 * @param array $settings Robots.txt settings
2754 * @return array Default rules
2755 */
2756 private function generate_default_robots_rules(array $settings): array {
2757 $rules = [];
2758
2759 // Default user agent rule
2760 $rules[] = [
2761 'directive' => 'user_agent',
2762 'value' => '*'
2763 ];
2764
2765 // WordPress core disallows
2766 $default_disallows = [
2767 '/wp-admin/',
2768 '/wp-includes/',
2769 '/wp-content/plugins/',
2770 '/wp-content/themes/',
2771 '/wp-json/',
2772 '/xmlrpc.php',
2773 '/wp-*.php',
2774 '/readme.html',
2775 '/license.txt'
2776 ];
2777
2778 foreach ($default_disallows as $disallow) {
2779 $rules[] = [
2780 'directive' => 'disallow',
2781 'value' => $disallow
2782 ];
2783 }
2784
2785 // Allow specific files
2786 $default_allows = [
2787 '/wp-admin/admin-ajax.php',
2788 '/wp-content/uploads/'
2789 ];
2790
2791 foreach ($default_allows as $allow) {
2792 $rules[] = [
2793 'directive' => 'allow',
2794 'value' => $allow
2795 ];
2796 }
2797
2798 // Add sitemap URLs from sitemap settings (auto-sync)
2799 $sitemap_urls = $this->get_sitemap_urls_for_robots();
2800
2801 foreach ($sitemap_urls as $sitemap_url) {
2802 if (!empty($sitemap_url)) {
2803 $rules[] = [
2804 'directive' => 'sitemap',
2805 'value' => $sitemap_url
2806 ];
2807 }
2808 }
2809
2810 // Add crawl delay if specified
2811 if (!empty($settings['crawl_delay'])) {
2812 $rules[] = [
2813 'directive' => 'crawl_delay',
2814 'value' => (int) $settings['crawl_delay']
2815 ];
2816 }
2817
2818 return $rules;
2819 }
2820
2821 /**
2822 * Get sitemap URLs from sitemap settings for robots.txt integration
2823 *
2824 * @since 1.0.0
2825 * @return array Array of sitemap URLs
2826 */
2827 private function get_sitemap_urls_for_robots(): array {
2828 try {
2829 // Get sitemap settings
2830 $sitemap_generator = new \ThinkRank\SEO\Sitemap_Generator();
2831 $sitemap_settings = $sitemap_generator->get_settings('site');
2832
2833 // If sitemap is disabled, return default
2834 if (empty($sitemap_settings['enabled'])) {
2835 return [home_url('/sitemap.xml')];
2836 }
2837
2838 $sitemap_urls = [];
2839 $site_url = home_url();
2840
2841 // Extract enabled sitemap URLs
2842 if (!empty($sitemap_settings['sitemap_urls']) && is_array($sitemap_settings['sitemap_urls'])) {
2843 foreach ($sitemap_settings['sitemap_urls'] as $sitemap) {
2844 if (!empty($sitemap['enabled']) && !empty($sitemap['url'])) {
2845 $sitemap_urls[] = $site_url . $sitemap['url'];
2846 }
2847 }
2848 }
2849
2850 // Fallback to default if no URLs found
2851 if (empty($sitemap_urls)) {
2852 $sitemap_urls[] = home_url('/sitemap.xml');
2853 }
2854
2855 return $sitemap_urls;
2856
2857 } catch (\Exception $e) {
2858 // Fallback to default on error
2859 return [home_url('/sitemap.xml')];
2860 }
2861 }
2862
2863 /**
2864 * Validate robots.txt rules
2865 *
2866 * @since 1.0.0
2867 *
2868 * @param array $rules Rules to validate
2869 * @return array Validation results
2870 */
2871 private function validate_robots_rules(array $rules): array {
2872 $validation = [
2873 'valid' => true,
2874 'errors' => [],
2875 'warnings' => [],
2876 'suggestions' => []
2877 ];
2878
2879 $has_user_agent = false;
2880
2881 foreach ($rules as $rule) {
2882 $directive = $rule['directive'] ?? '';
2883 $value = $rule['value'] ?? '';
2884
2885 // Check if directive is valid
2886 if (!isset($this->robots_directives[$directive])) {
2887 $validation['errors'][] = "Unknown robots.txt directive: {$directive}";
2888 $validation['valid'] = false;
2889 continue;
2890 }
2891
2892 // Check for required user-agent
2893 if ($directive === 'user_agent') {
2894 $has_user_agent = true;
2895 }
2896
2897 // Validate directive-specific rules
2898 switch ($directive) {
2899 case 'disallow':
2900 case 'allow':
2901 if (!str_starts_with($value, '/')) {
2902 $validation['warnings'][] = "Path '{$value}' should start with '/'";
2903 }
2904 break;
2905 case 'sitemap':
2906 if (!filter_var($value, FILTER_VALIDATE_URL)) {
2907 $validation['errors'][] = "Invalid sitemap URL: {$value}";
2908 $validation['valid'] = false;
2909 }
2910 break;
2911 case 'crawl_delay':
2912 if (!is_numeric($value) || $value < 0) {
2913 $validation['errors'][] = "Crawl delay must be a positive number";
2914 $validation['valid'] = false;
2915 }
2916 break;
2917 }
2918 }
2919
2920 if (!$has_user_agent) {
2921 $validation['errors'][] = 'robots.txt must include at least one User-agent directive';
2922 $validation['valid'] = false;
2923 }
2924
2925 return $validation;
2926 }
2927
2928 /**
2929 * Build robots.txt content from rules
2930 *
2931 * @since 1.0.0
2932 *
2933 * @param array $rules Robots.txt rules
2934 * @return string Robots.txt content
2935 */
2936 private function build_robots_txt_content(array $rules): string {
2937 $content = "# Robots.txt generated by ThinkRank SEO\n";
2938 $content .= "# " . gmdate('Y-m-d H:i:s') . " UTC\n\n";
2939
2940 $current_user_agent = '';
2941
2942 foreach ($rules as $rule) {
2943 $directive = $rule['directive'] ?? '';
2944 $value = $rule['value'] ?? '';
2945
2946 switch ($directive) {
2947 case 'user_agent':
2948 if ($current_user_agent !== $value) {
2949 $content .= "\nUser-agent: {$value}\n";
2950 $current_user_agent = $value;
2951 }
2952 break;
2953 case 'disallow':
2954 $content .= "Disallow: {$value}\n";
2955 break;
2956 case 'allow':
2957 $content .= "Allow: {$value}\n";
2958 break;
2959 case 'crawl_delay':
2960 $content .= "Crawl-delay: {$value}\n";
2961 break;
2962 case 'sitemap':
2963 $content .= "\nSitemap: {$value}\n";
2964 break;
2965 }
2966 }
2967
2968 return $content;
2969 }
2970
2971 /**
2972 * Check if sitemap exists
2973 *
2974 * @since 1.0.0
2975 *
2976 * @param string $sitemap_url Sitemap URL to check
2977 * @return bool True if sitemap exists
2978 */
2979 private function sitemap_exists(string $sitemap_url): bool {
2980 $response = wp_remote_head($sitemap_url);
2981 return !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200;
2982 }
2983
2984 /**
2985 * Get site identity data
2986 *
2987 * @since 1.0.0
2988 *
2989 * @param array $settings Site settings
2990 * @return array Site identity data
2991 */
2992 private function get_site_identity_data(array $settings): array {
2993 return [
2994 'site_name' => $settings['site_name'] ?? get_bloginfo('name'),
2995 'site_description' => $settings['site_description'] ?? get_bloginfo('description'),
2996 'tagline' => $settings['tagline'] ?? get_bloginfo('description'),
2997 'logo_url' => $settings['logo_url'] ?? '',
2998 'favicon_url' => $settings['favicon_url'] ?? '',
2999 'apple_touch_icon_url' => $settings['apple_touch_icon_url'] ?? ''
3000 ];
3001 }
3002
3003 /**
3004 * Optimize individual identity element
3005 *
3006 * @since 1.0.0
3007 *
3008 * @param string $element Element name
3009 * @param mixed $value Element value
3010 * @param array $config Element configuration
3011 * @return array Optimization results
3012 */
3013 private function optimize_identity_element(string $element, $value, array $config): array {
3014 $optimization = [
3015 'optimized_value' => $value,
3016 'validation' => [
3017 'valid' => true,
3018 'errors' => [],
3019 'warnings' => []
3020 ],
3021 'suggestions' => []
3022 ];
3023
3024 switch ($config['type']) {
3025 case 'text':
3026 $optimization = $this->optimize_text_element($element, $value, $config, $optimization);
3027 break;
3028 case 'image':
3029 $optimization = $this->optimize_image_element($element, $value, $config, $optimization);
3030 break;
3031 }
3032
3033 return $optimization;
3034 }
3035
3036 /**
3037 * Optimize text identity element
3038 *
3039 * @since 1.0.0
3040 *
3041 * @param string $element Element name
3042 * @param string $value Element value
3043 * @param array $config Element configuration
3044 * @param array $optimization Current optimization
3045 * @return array Updated optimization
3046 */
3047 private function optimize_text_element(string $element, string $value, array $config, array $optimization): array {
3048 if (empty($value) && !empty($config['required'])) {
3049 $optimization['validation']['errors'][] = "{$element} is required";
3050 $optimization['validation']['valid'] = false;
3051 }
3052
3053 if (!empty($value) && isset($config['max_length'])) {
3054 if (strlen($value) > $config['max_length']) {
3055 $optimization['validation']['warnings'][] = "{$element} exceeds maximum length of {$config['max_length']} characters";
3056 $optimization['optimized_value'] = substr($value, 0, $config['max_length']);
3057 }
3058 }
3059
3060 // SEO-specific optimizations
3061 if ($element === 'site_name' && !empty($value)) {
3062 // Remove excessive punctuation
3063 $optimization['optimized_value'] = preg_replace('/[!@#$%^&*()]+/', '', $value);
3064 }
3065
3066 return $optimization;
3067 }
3068
3069 /**
3070 * Optimize image identity element
3071 *
3072 * @since 1.0.0
3073 *
3074 * @param string $element Element name
3075 * @param string $value Element value
3076 * @param array $config Element configuration
3077 * @param array $optimization Current optimization
3078 * @return array Updated optimization
3079 */
3080 private function optimize_image_element(string $element, string $value, array $config, array $optimization): array {
3081 if (empty($value)) {
3082 if (!empty($config['required'])) {
3083 $optimization['validation']['errors'][] = "{$element} is required";
3084 $optimization['validation']['valid'] = false;
3085 }
3086 return $optimization;
3087 }
3088
3089 // Validate URL
3090 if (!filter_var($value, FILTER_VALIDATE_URL)) {
3091 $optimization['validation']['errors'][] = "{$element} must be a valid URL";
3092 $optimization['validation']['valid'] = false;
3093 return $optimization;
3094 }
3095
3096 // Check if it's a local image
3097 $attachment_id = attachment_url_to_postid($value);
3098 if ($attachment_id) {
3099 $image_meta = wp_get_attachment_metadata($attachment_id);
3100
3101 if ($image_meta && isset($image_meta['width'], $image_meta['height'])) {
3102 // Check recommended size
3103 if (isset($config['recommended_size'])) {
3104 [$rec_width, $rec_height] = explode('x', $config['recommended_size']);
3105
3106 if ($image_meta['width'] != $rec_width || $image_meta['height'] != $rec_height) {
3107 $optimization['suggestions'][] = "Consider using {$config['recommended_size']} size for optimal {$element}";
3108 }
3109 }
3110
3111 // Check file size
3112 if (isset($config['max_size'])) {
3113 $file_path = get_attached_file($attachment_id);
3114 if ($file_path && file_exists($file_path)) {
3115 $file_size = filesize($file_path);
3116 $max_size_bytes = $this->parse_size_string($config['max_size']);
3117
3118 if ($file_size > $max_size_bytes) {
3119 $optimization['validation']['warnings'][] = "{$element} file size exceeds {$config['max_size']}";
3120 }
3121 }
3122 }
3123 }
3124 }
3125
3126 return $optimization;
3127 }
3128
3129 /**
3130 * Parse size string to bytes
3131 *
3132 * @since 1.0.0
3133 *
3134 * @param string $size_string Size string (e.g., '2MB', '500KB')
3135 * @return int Size in bytes
3136 */
3137 private function parse_size_string(string $size_string): int {
3138 $size_string = strtoupper(trim($size_string));
3139 $size = (int) $size_string;
3140
3141 if (strpos($size_string, 'KB') !== false) {
3142 return $size * 1024;
3143 } elseif (strpos($size_string, 'MB') !== false) {
3144 return $size * 1024 * 1024;
3145 } elseif (strpos($size_string, 'GB') !== false) {
3146 return $size * 1024 * 1024 * 1024;
3147 }
3148
3149 return $size;
3150 }
3151
3152 /**
3153 * Calculate identity optimization score
3154 *
3155 * @since 1.0.0
3156 *
3157 * @param array $validations Element validations
3158 * @return int Score (0-100)
3159 */
3160 private function calculate_identity_score(array $validations): int {
3161 $total_score = 0;
3162 $element_count = 0;
3163
3164 foreach ($validations as $validation) {
3165 $element_score = 100;
3166 $element_score -= count($validation['errors']) * 30;
3167 $element_score -= count($validation['warnings']) * 15;
3168
3169 $total_score += max(0, $element_score);
3170 $element_count++;
3171 }
3172
3173 return $element_count > 0 ? (int) round($total_score / $element_count) : 0;
3174 }
3175
3176 /**
3177 * Calculate validation score
3178 *
3179 * @since 1.0.0
3180 *
3181 * @param array $validation Validation results
3182 * @return int Score (0-100)
3183 */
3184 private function calculate_validation_score(array $validation): int {
3185 $score = 100;
3186 $score -= count($validation['errors']) * 20;
3187 $score -= count($validation['warnings']) * 10;
3188 $score -= count($validation['suggestions']) * 5;
3189
3190 return max(0, $score);
3191 }
3192 }
3193