# thinkrank/1.0.1/includes/seo/class-robots-meta-manager.php

ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console &amp; Local SEO, version 1.0.1. 1,291 lines.

- Page: https://pluginprobe.com/plugins/thinkrank/1.0.1/code/includes/seo/class-robots-meta-manager.php
- Raw: https://pluginprobe.com/plugins/thinkrank/1.0.1/raw/includes/seo/class-robots-meta-manager.php
- Modified: 2025-08-10T07:35:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/thinkrank/1.0.1/code/includes/seo/class-robots-meta-manager.php#L10-L20`.

```php
<?php
/**
 * Robots Meta Manager Class
 *
 * Universal robots meta tag generation and validation for search engine optimization.
 * Implements 2025 search engine guidelines with real robots meta specifications,
 * canonical URL management, and advanced robots directives handling.
 *
 * @package ThinkRank
 * @subpackage SEO
 * @since 1.0.0
 */

declare(strict_types=1);

namespace ThinkRank\SEO;

/**
 * Robots Meta Manager Class
 *
 * Generates and validates robots meta tags for search engine optimization.
 * Provides context-aware robots meta generation with canonical URL management
 * and advanced robots directives handling.
 *
 * @since 1.0.0
 */
class Robots_Meta_Manager extends Abstract_SEO_Manager {

    /**
     * Supported robots directives with their specifications
     *
     * @since 1.0.0
     * @var array
     */
    private array $robots_directives = [
        // Index directives
        'index' => [
            'type' => 'index',
            'opposite' => 'noindex',
            'description' => 'Allow search engines to index this page',
            'default' => true,
            'search_engines' => ['google', 'bing', 'yahoo', 'yandex', 'baidu']
        ],
        'noindex' => [
            'type' => 'index',
            'opposite' => 'index',
            'description' => 'Prevent search engines from indexing this page',
            'default' => false,
            'search_engines' => ['google', 'bing', 'yahoo', 'yandex', 'baidu']
        ],
        
        // Follow directives
        'follow' => [
            'type' => 'follow',
            'opposite' => 'nofollow',
            'description' => 'Allow search engines to follow links on this page',
            'default' => true,
            'search_engines' => ['google', 'bing', 'yahoo', 'yandex', 'baidu']
        ],
        'nofollow' => [
            'type' => 'follow',
            'opposite' => 'follow',
            'description' => 'Prevent search engines from following links on this page',
            'default' => false,
            'search_engines' => ['google', 'bing', 'yahoo', 'yandex', 'baidu']
        ],
        
        // Archive directives
        'archive' => [
            'type' => 'archive',
            'opposite' => 'noarchive',
            'description' => 'Allow search engines to show cached versions of this page',
            'default' => true,
            'search_engines' => ['google', 'bing', 'yahoo']
        ],
        'noarchive' => [
            'type' => 'archive',
            'opposite' => 'archive',
            'description' => 'Prevent search engines from showing cached versions of this page',
            'default' => false,
            'search_engines' => ['google', 'bing', 'yahoo']
        ],
        
        // Snippet directives
        'snippet' => [
            'type' => 'snippet',
            'opposite' => 'nosnippet',
            'description' => 'Allow search engines to show text snippets of this page',
            'default' => true,
            'search_engines' => ['google', 'bing']
        ],
        'nosnippet' => [
            'type' => 'snippet',
            'opposite' => 'snippet',
            'description' => 'Prevent search engines from showing text snippets of this page',
            'default' => false,
            'search_engines' => ['google', 'bing']
        ],
        
        // Image directives
        'noimageindex' => [
            'type' => 'image',
            'opposite' => null,
            'description' => 'Prevent search engines from indexing images on this page',
            'default' => false,
            'search_engines' => ['google']
        ],
        
        // Translation directives
        'notranslate' => [
            'type' => 'translate',
            'opposite' => null,
            'description' => 'Prevent search engines from offering translation of this page',
            'default' => false,
            'search_engines' => ['google']
        ],
        
        // Video directives
        'novideoindex' => [
            'type' => 'video',
            'opposite' => null,
            'description' => 'Prevent search engines from indexing videos on this page',
            'default' => false,
            'search_engines' => ['google']
        ]
    ];

    /**
     * Advanced robots directives with parameters
     *
     * @since 1.0.0
     * @var array
     */
    private array $advanced_directives = [
        'max-snippet' => [
            'type' => 'parameterized',
            'description' => 'Maximum number of characters to show in text snippets',
            'default_value' => -1, // -1 means no limit
            'min_value' => -1,
            'max_value' => 320,
            'search_engines' => ['google']
        ],
        'max-image-preview' => [
            'type' => 'parameterized',
            'description' => 'Maximum size of image preview',
            'default_value' => 'large',
            'allowed_values' => ['none', 'standard', 'large'],
            'search_engines' => ['google']
        ],
        'max-video-preview' => [
            'type' => 'parameterized',
            'description' => 'Maximum duration of video preview in seconds',
            'default_value' => -1, // -1 means no limit
            'min_value' => -1,
            'max_value' => 3600, // 1 hour
            'search_engines' => ['google']
        ],
        'unavailable_after' => [
            'type' => 'date',
            'description' => 'Date after which the page should not be indexed',
            'format' => 'RFC 850',
            'search_engines' => ['google']
        ]
    ];

    /**
     * Search engine specific robots directives
     *
     * @since 1.0.0
     * @var array
     */
    private array $search_engine_specific = [
        'google' => ['googlebot'],
        'bing' => ['bingbot', 'msnbot'],
        'yahoo' => ['slurp'],
        'yandex' => ['yandexbot'],
        'baidu' => ['baiduspider'],
        'duckduckgo' => ['duckduckbot']
    ];

    /**
     * Constructor
     *
     * @since 1.0.0
     */
    public function __construct() {
        parent::__construct('robots_meta');
    }

    /**
     * Generate robots meta tags for content
     *
     * @since 1.0.0
     *
     * @param array  $data    Content data for robots generation
     * @param string $context Context type ('site', 'post', 'page', etc.)
     * @return array Generated robots meta tags
     */
    public function generate_robots_meta(array $data, string $context = 'site'): array {
        $robots_meta = [];
        
        // Get robots settings for context
        $settings = $this->get_settings($context, $data['context_id'] ?? null);
        
        // Generate main robots directive
        $main_directive = $this->build_main_robots_directive($settings, $context, $data);
        if (!empty($main_directive)) {
            $robots_meta['robots'] = $main_directive;
        }
        
        // Generate search engine specific directives
        $specific_directives = $this->build_search_engine_specific_directives($settings, $context, $data);
        $robots_meta = array_merge($robots_meta, $specific_directives);
        
        // Generate canonical URL
        $canonical_url = $this->generate_canonical_url($data, $context, $settings);
        if (!empty($canonical_url)) {
            $robots_meta['canonical'] = $canonical_url;
        }
        
        // Add other meta directives
        $other_meta = $this->generate_other_meta_directives($settings, $context, $data);
        $robots_meta = array_merge($robots_meta, $other_meta);
        
        return $robots_meta;
    }

    /**
     * Validate robots meta settings
     *
     * @since 1.0.0
     *
     * @param array $robots_meta Robots meta to validate
     * @return array Validation results with errors, warnings, and suggestions
     */
    public function validate_robots_meta(array $robots_meta): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => [],
            'score' => 100
        ];

        // Validate main robots directive
        if (isset($robots_meta['robots'])) {
            $robots_validation = $this->validate_robots_directive($robots_meta['robots']);
            $validation = array_merge_recursive($validation, $robots_validation);
        }

        // Validate canonical URL
        if (isset($robots_meta['canonical'])) {
            $canonical_validation = $this->validate_canonical_url($robots_meta['canonical']);
            $validation = array_merge_recursive($validation, $canonical_validation);
        }

        // Validate advanced directives
        foreach ($this->advanced_directives as $directive => $spec) {
            if (isset($robots_meta[$directive])) {
                $directive_validation = $this->validate_advanced_directive($directive, $robots_meta[$directive], $spec);
                $validation = array_merge_recursive($validation, $directive_validation);
            }
        }

        // Check for conflicting directives
        $conflict_validation = $this->check_directive_conflicts($robots_meta);
        $validation = array_merge_recursive($validation, $conflict_validation);

        // Calculate overall score
        $validation['score'] = $this->calculate_robots_score($validation);

        // Add SEO suggestions
        $validation = $this->add_robots_seo_suggestions($robots_meta, $validation);

        return $validation;
    }

    /**
     * Generate canonical URL for content
     *
     * @since 1.0.0
     *
     * @param array  $data     Content data
     * @param string $context  Context type
     * @param array  $settings Robots settings
     * @return string Canonical URL
     */
    public function generate_canonical_url(array $data, string $context, array $settings = []): string {
        // Check if custom canonical is set
        if (!empty($settings['custom_canonical'])) {
            return $this->validate_and_clean_url($settings['custom_canonical']);
        }

        // Auto-generate canonical based on context
        switch ($context) {
            case 'site':
                return home_url();
            case 'post':
            case 'page':
            case 'product':
                if (!empty($data['url'])) {
                    return $this->clean_canonical_url($data['url']);
                }
                break;
        }

        // Fallback to current URL
        return $this->get_current_canonical_url();
    }

    /**
     * Check for duplicate content and suggest canonical URLs
     *
     * @since 1.0.0
     *
     * @param string $url     URL to check
     * @param string $context Context type
     * @return array Duplicate content analysis
     */
    public function analyze_duplicate_content(string $url, string $context): array {
        $analysis = [
            'has_duplicates' => false,
            'duplicate_urls' => [],
            'suggested_canonical' => $url,
            'recommendations' => []
        ];

        // Check for common duplicate patterns
        $duplicate_patterns = $this->detect_duplicate_patterns($url);
        if (!empty($duplicate_patterns)) {
            $analysis['has_duplicates'] = true;
            $analysis['duplicate_urls'] = $duplicate_patterns;
            $analysis['suggested_canonical'] = $this->suggest_canonical_from_duplicates($duplicate_patterns);
            $analysis['recommendations'] = $this->get_duplicate_content_recommendations($duplicate_patterns);
        }

        return $analysis;
    }

    /**
     * Validate SEO settings (implements interface)
     *
     * @since 1.0.0
     *
     * @param array $settings Settings array to validate
     * @return array Validation results
     */
    public function validate_settings(array $settings): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => []
        ];

        // Validate robots index setting
        if (isset($settings['robots_index'])) {
            if (!in_array($settings['robots_index'], ['index', 'noindex'], true)) {
                $validation['errors'][] = 'robots_index must be either "index" or "noindex"';
                $validation['valid'] = false;
            }
        }

        // Validate robots follow setting
        if (isset($settings['robots_follow'])) {
            if (!in_array($settings['robots_follow'], ['follow', 'nofollow'], true)) {
                $validation['errors'][] = 'robots_follow must be either "follow" or "nofollow"';
                $validation['valid'] = false;
            }
        }

        // Validate advanced directives
        foreach ($this->advanced_directives as $directive => $spec) {
            if (isset($settings[$directive])) {
                $directive_validation = $this->validate_advanced_directive($directive, $settings[$directive], $spec);
                if (!$directive_validation['valid']) {
                    $validation['errors'] = array_merge($validation['errors'], $directive_validation['errors']);
                    $validation['valid'] = false;
                }
                $validation['warnings'] = array_merge($validation['warnings'], $directive_validation['warnings']);
            }
        }

        // Validate canonical URL
        if (isset($settings['custom_canonical']) && !empty($settings['custom_canonical'])) {
            if (!filter_var($settings['custom_canonical'], FILTER_VALIDATE_URL)) {
                $validation['errors'][] = 'custom_canonical must be a valid URL';
                $validation['valid'] = false;
            }
        }

        // Check for conflicting settings
        if (isset($settings['robots_index'], $settings['robots_follow'])) {
            if ($settings['robots_index'] === 'noindex' && $settings['robots_follow'] === 'follow') {
                $validation['warnings'][] = 'Using "noindex, follow" - consider using "noindex, nofollow" for better crawl budget optimization';
            }
        }

        return $validation;
    }

    /**
     * Get output data for frontend rendering (implements interface)
     *
     * @since 1.0.0
     *
     * @param string   $context_type The context type
     * @param int|null $context_id   Optional. Context ID
     * @return array Output data ready for frontend rendering
     */
    public function get_output_data(string $context_type, ?int $context_id): array {
        $settings = $this->get_settings($context_type, $context_id);
        $output = [
            'robots_meta' => [],
            'meta_tags' => [],
            'enabled' => false
        ];

        // Check if robots meta is enabled
        if (empty($settings['robots_enabled'])) {
            return $output;
        }

        $output['enabled'] = true;

        // Extract content data
        $content_data = $this->extract_robots_content_data($context_type, $context_id);

        // Generate robots meta
        $output['robots_meta'] = $this->generate_robots_meta($content_data, $context_type);

        // Convert to HTML meta tags
        $output['meta_tags'] = $this->convert_robots_to_meta_tags($output['robots_meta']);

        return $output;
    }

    /**
     * Get default settings for a context type (implements interface)
     *
     * @since 1.0.0
     *
     * @param string $context_type The context type to get defaults for
     * @return array Default settings array
     */
    public function get_default_settings(string $context_type): array {
        $defaults = [
            'robots_enabled' => true,
            'robots_index' => 'index',
            'robots_follow' => 'follow',
            'robots_archive' => 'archive',
            'robots_snippet' => 'snippet',
            'max_snippet' => -1,
            'max_image_preview' => 'large',
            'max_video_preview' => -1,
            'custom_canonical' => '',
            'auto_canonical' => true,
            'noindex_search' => true,
            'noindex_archives' => false
        ];

        // Context-specific defaults
        switch ($context_type) {
            case 'site':
                // Site-wide defaults are already set above
                break;
            case 'post':
                // Posts should generally be indexed
                $defaults['robots_index'] = 'index';
                break;
            case 'page':
                // Pages should generally be indexed
                $defaults['robots_index'] = 'index';
                break;
            case 'product':
                // Products should definitely be indexed
                $defaults['robots_index'] = 'index';
                $defaults['max_image_preview'] = 'large'; // Important for product images
                break;
        }

        return $defaults;
    }

    /**
     * Get settings schema definition (implements interface)
     *
     * @since 1.0.0
     *
     * @param string $context_type The context type to get schema for
     * @return array Settings schema definition
     */
    public function get_settings_schema(string $context_type): array {
        return [
            'robots_enabled' => [
                'type' => 'boolean',
                'title' => 'Enable Robots Meta',
                'description' => 'Generate robots meta tags for search engine crawling control',
                'default' => true
            ],
            'robots_index' => [
                'type' => 'string',
                'title' => 'Index Directive',
                'description' => 'Control whether search engines should index this content',
                'enum' => ['index', 'noindex'],
                'default' => 'index'
            ],
            'robots_follow' => [
                'type' => 'string',
                'title' => 'Follow Directive',
                'description' => 'Control whether search engines should follow links on this content',
                'enum' => ['follow', 'nofollow'],
                'default' => 'follow'
            ],
            'robots_archive' => [
                'type' => 'string',
                'title' => 'Archive Directive',
                'description' => 'Control whether search engines should show cached versions',
                'enum' => ['archive', 'noarchive'],
                'default' => 'archive'
            ],
            'robots_snippet' => [
                'type' => 'string',
                'title' => 'Snippet Directive',
                'description' => 'Control whether search engines should show text snippets',
                'enum' => ['snippet', 'nosnippet'],
                'default' => 'snippet'
            ],
            'max_snippet' => [
                'type' => 'integer',
                'title' => 'Max Snippet Length',
                'description' => 'Maximum number of characters in text snippets (-1 for no limit)',
                'minimum' => -1,
                'maximum' => 320,
                'default' => -1
            ],
            'max_image_preview' => [
                'type' => 'string',
                'title' => 'Max Image Preview',
                'description' => 'Maximum size of image preview in search results',
                'enum' => ['none', 'standard', 'large'],
                'default' => 'large'
            ],
            'max_video_preview' => [
                'type' => 'integer',
                'title' => 'Max Video Preview',
                'description' => 'Maximum duration of video preview in seconds (-1 for no limit)',
                'minimum' => -1,
                'maximum' => 3600,
                'default' => -1
            ],
            'custom_canonical' => [
                'type' => 'string',
                'title' => 'Custom Canonical URL',
                'description' => 'Custom canonical URL for this content (overrides auto-generated)',
                'format' => 'uri',
                'default' => ''
            ],
            'auto_canonical' => [
                'type' => 'boolean',
                'title' => 'Auto-generate Canonical',
                'description' => 'Automatically generate canonical URLs',
                'default' => true
            ],
            'noindex_search' => [
                'type' => 'boolean',
                'title' => 'Noindex Search Results',
                'description' => 'Prevent indexing of search result pages',
                'default' => true
            ],
            'noindex_archives' => [
                'type' => 'boolean',
                'title' => 'Noindex Archive Pages',
                'description' => 'Prevent indexing of archive pages (categories, tags, etc.)',
                'default' => false
            ]
        ];
    }

    /**
     * Extract content data for robots meta generation
     *
     * @since 1.0.0
     *
     * @param string   $context_type The context type
     * @param int|null $context_id   Optional. Context ID
     * @return array Extracted content data
     */
    private function extract_robots_content_data(string $context_type, ?int $context_id): array {
        $data = [
            'context_type' => $context_type,
            'context_id' => $context_id,
            'url' => '',
            'is_search' => false,
            'is_archive' => false,
            'is_404' => false,
            'is_private' => false,
            'post_status' => '',
            'post_password' => false
        ];

        if ($context_type === 'site') {
            $data['url'] = home_url();
        } elseif ($context_id && in_array($context_type, ['post', 'page', 'product'], true)) {
            $post = get_post($context_id);
            if ($post) {
                $data['url'] = get_permalink($post);
                $data['post_status'] = $post->post_status;
                $data['post_password'] = !empty($post->post_password);
                $data['is_private'] = $post->post_status === 'private';
            }
        }

        // Detect special page types
        if (is_search()) {
            $data['is_search'] = true;
        }
        if (is_archive() || is_category() || is_tag() || is_tax()) {
            $data['is_archive'] = true;
        }
        if (is_404()) {
            $data['is_404'] = true;
        }

        return $data;
    }

    /**
     * Build main robots directive
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return string Robots directive
     */
    private function build_main_robots_directive(array $settings, string $context, array $data): string {
        $directives = [];

        // Determine index directive
        $index_directive = $this->determine_index_directive($settings, $context, $data);
        if ($index_directive) {
            $directives[] = $index_directive;
        }

        // Determine follow directive
        $follow_directive = $this->determine_follow_directive($settings, $context, $data);
        if ($follow_directive) {
            $directives[] = $follow_directive;
        }

        // Add other directives
        $other_directives = $this->get_other_robots_directives($settings, $context, $data);
        $directives = array_merge($directives, $other_directives);

        // Add advanced directives
        $advanced_directives = $this->get_advanced_robots_directives($settings);
        $directives = array_merge($directives, $advanced_directives);

        return implode(', ', array_filter($directives));
    }

    /**
     * Determine index directive based on settings and context
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return string Index directive
     */
    private function determine_index_directive(array $settings, string $context, array $data): string {
        // Check for explicit noindex conditions
        if ($data['is_search'] && !empty($settings['noindex_search'])) {
            return 'noindex';
        }

        if ($data['is_archive'] && !empty($settings['noindex_archives'])) {
            return 'noindex';
        }

        if ($data['is_404']) {
            return 'noindex';
        }

        if ($data['is_private'] || $data['post_password']) {
            return 'noindex';
        }

        if ($data['post_status'] === 'draft' || $data['post_status'] === 'pending') {
            return 'noindex';
        }

        // Use setting or default
        return $settings['robots_index'] ?? 'index';
    }

    /**
     * Determine follow directive based on settings and context
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return string Follow directive
     */
    private function determine_follow_directive(array $settings, string $context, array $data): string {
        // If noindex, consider nofollow for crawl budget optimization
        $index_directive = $this->determine_index_directive($settings, $context, $data);
        if ($index_directive === 'noindex') {
            // For noindex pages, default to nofollow unless explicitly set to follow
            if (!isset($settings['robots_follow']) || $settings['robots_follow'] !== 'follow') {
                return 'nofollow';
            }
        }

        // Use setting or default
        return $settings['robots_follow'] ?? 'follow';
    }

    /**
     * Get other robots directives
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return array Other directives
     */
    private function get_other_robots_directives(array $settings, string $context, array $data): array {
        $directives = [];

        // Archive directive
        if (isset($settings['robots_archive']) && $settings['robots_archive'] === 'noarchive') {
            $directives[] = 'noarchive';
        }

        // Snippet directive
        if (isset($settings['robots_snippet']) && $settings['robots_snippet'] === 'nosnippet') {
            $directives[] = 'nosnippet';
        }

        // Image index directive
        if (!empty($settings['noimageindex'])) {
            $directives[] = 'noimageindex';
        }

        // Video index directive
        if (!empty($settings['novideoindex'])) {
            $directives[] = 'novideoindex';
        }

        // Translation directive
        if (!empty($settings['notranslate'])) {
            $directives[] = 'notranslate';
        }

        return $directives;
    }

    /**
     * Get advanced robots directives with parameters
     *
     * @since 1.0.0
     *
     * @param array $settings Robots settings
     * @return array Advanced directives
     */
    private function get_advanced_robots_directives(array $settings): array {
        $directives = [];

        // Max snippet
        if (isset($settings['max_snippet']) && $settings['max_snippet'] !== -1) {
            $directives[] = 'max-snippet:' . (int) $settings['max_snippet'];
        }

        // Max image preview
        if (isset($settings['max_image_preview']) && $settings['max_image_preview'] !== 'large') {
            $directives[] = 'max-image-preview:' . $settings['max_image_preview'];
        }

        // Max video preview
        if (isset($settings['max_video_preview']) && $settings['max_video_preview'] !== -1) {
            $directives[] = 'max-video-preview:' . (int) $settings['max_video_preview'];
        }

        // Unavailable after
        if (!empty($settings['unavailable_after'])) {
            $directives[] = 'unavailable_after:' . $settings['unavailable_after'];
        }

        return $directives;
    }

    /**
     * Build search engine specific directives
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return array Search engine specific directives
     */
    private function build_search_engine_specific_directives(array $settings, string $context, array $data): array {
        $specific_directives = [];

        // Check if any search engine specific settings are configured
        foreach ($this->search_engine_specific as $engine => $bots) {
            $engine_settings = $settings[$engine] ?? [];
            if (!empty($engine_settings)) {
                foreach ($bots as $bot) {
                    $directive = $this->build_main_robots_directive($engine_settings, $context, $data);
                    if (!empty($directive)) {
                        $specific_directives[$bot] = $directive;
                    }
                }
            }
        }

        return $specific_directives;
    }

    /**
     * Generate other meta directives
     *
     * @since 1.0.0
     *
     * @param array  $settings Robots settings
     * @param string $context  Context type
     * @param array  $data     Content data
     * @return array Other meta directives
     */
    private function generate_other_meta_directives(array $settings, string $context, array $data): array {
        $meta = [];

        // Refresh directive for dynamic content
        if (!empty($settings['refresh_interval'])) {
            $meta['refresh'] = (int) $settings['refresh_interval'];
        }

        // Rating directive for content rating
        if (!empty($settings['rating'])) {
            $meta['rating'] = $settings['rating'];
        }

        return $meta;
    }

    /**
     * Clean canonical URL
     *
     * @since 1.0.0
     *
     * @param string $url URL to clean
     * @return string Cleaned canonical URL
     */
    private function clean_canonical_url(string $url): string {
        // Remove query parameters that don't affect content
        $parsed_url = wp_parse_url($url);

        if (!$parsed_url) {
            return $url;
        }

        // Rebuild URL without unwanted query parameters
        $clean_url = $parsed_url['scheme'] . '://' . $parsed_url['host'];

        if (!empty($parsed_url['port']) && $parsed_url['port'] !== 80 && $parsed_url['port'] !== 443) {
            $clean_url .= ':' . $parsed_url['port'];
        }

        if (!empty($parsed_url['path'])) {
            $clean_url .= $parsed_url['path'];
        }

        // Only include specific query parameters that affect content
        if (!empty($parsed_url['query'])) {
            parse_str($parsed_url['query'], $query_params);
            $allowed_params = ['p', 'page_id', 'cat', 'tag', 's']; // WordPress core parameters
            $filtered_params = array_intersect_key($query_params, array_flip($allowed_params));

            if (!empty($filtered_params)) {
                $clean_url .= '?' . http_build_query($filtered_params);
            }
        }

        return $clean_url;
    }

    /**
     * Get current canonical URL
     *
     * @since 1.0.0
     *
     * @return string Current canonical URL
     */
    private function get_current_canonical_url(): string {
        if (is_admin()) {
            return home_url();
        }

        global $wp;
        $current_url = home_url(add_query_arg([], $wp->request));
        return $this->clean_canonical_url($current_url);
    }

    /**
     * Validate and clean URL
     *
     * @since 1.0.0
     *
     * @param string $url URL to validate and clean
     * @return string Validated and cleaned URL
     */
    private function validate_and_clean_url(string $url): string {
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            return '';
        }

        return $this->clean_canonical_url($url);
    }

    /**
     * Validate robots directive
     *
     * @since 1.0.0
     *
     * @param string $directive Robots directive to validate
     * @return array Validation results
     */
    private function validate_robots_directive(string $directive): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => []
        ];

        if (empty($directive)) {
            $validation['warnings'][] = 'Empty robots directive';
            return $validation;
        }

        $directives = array_map('trim', explode(',', $directive));
        $known_directives = array_keys($this->robots_directives);
        $advanced_directives = array_keys($this->advanced_directives);

        foreach ($directives as $single_directive) {
            // Check for parameterized directives
            if (strpos($single_directive, ':') !== false) {
                [$directive_name, $directive_value] = explode(':', $single_directive, 2);
                $directive_name = trim($directive_name);

                if (!in_array($directive_name, $advanced_directives, true)) {
                    $validation['warnings'][] = "Unknown advanced directive: {$directive_name}";
                }
            } else {
                // Check basic directives
                if (!in_array($single_directive, $known_directives, true)) {
                    $validation['warnings'][] = "Unknown robots directive: {$single_directive}";
                }
            }
        }

        // Check for conflicting directives
        if (in_array('index', $directives, true) && in_array('noindex', $directives, true)) {
            $validation['errors'][] = 'Conflicting directives: index and noindex cannot be used together';
            $validation['valid'] = false;
        }

        if (in_array('follow', $directives, true) && in_array('nofollow', $directives, true)) {
            $validation['errors'][] = 'Conflicting directives: follow and nofollow cannot be used together';
            $validation['valid'] = false;
        }

        return $validation;
    }

    /**
     * Validate canonical URL
     *
     * @since 1.0.0
     *
     * @param string $canonical_url Canonical URL to validate
     * @return array Validation results
     */
    private function validate_canonical_url(string $canonical_url): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => []
        ];

        if (empty($canonical_url)) {
            $validation['warnings'][] = 'No canonical URL specified';
            return $validation;
        }

        if (!filter_var($canonical_url, FILTER_VALIDATE_URL)) {
            $validation['errors'][] = 'Invalid canonical URL format';
            $validation['valid'] = false;
            return $validation;
        }

        // Check if canonical URL is accessible
        $parsed_url = wp_parse_url($canonical_url);
        $current_host = wp_parse_url(home_url(), PHP_URL_HOST);

        if ($parsed_url['host'] !== $current_host) {
            $validation['warnings'][] = 'Canonical URL points to external domain - ensure this is intentional';
        }

        // Check for common canonical URL issues
        if (strpos($canonical_url, '?') !== false) {
            $validation['suggestions'][] = 'Consider removing query parameters from canonical URL for cleaner URLs';
        }

        if (substr($canonical_url, -1) !== '/' && !pathinfo($canonical_url, PATHINFO_EXTENSION)) {
            $validation['suggestions'][] = 'Consider adding trailing slash to canonical URL for consistency';
        }

        return $validation;
    }

    /**
     * Validate advanced directive
     *
     * @since 1.0.0
     *
     * @param string $directive_name  Directive name
     * @param mixed  $directive_value Directive value
     * @param array  $spec           Directive specification
     * @return array Validation results
     */
    private function validate_advanced_directive(string $directive_name, $directive_value, array $spec): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => []
        ];

        switch ($spec['type']) {
            case 'parameterized':
                if (isset($spec['allowed_values'])) {
                    if (!in_array($directive_value, $spec['allowed_values'], true)) {
                        $validation['errors'][] = "{$directive_name} must be one of: " . implode(', ', $spec['allowed_values']);
                        $validation['valid'] = false;
                    }
                } elseif (isset($spec['min_value'], $spec['max_value'])) {
                    $value = (int) $directive_value;
                    if ($value < $spec['min_value'] || $value > $spec['max_value']) {
                        $validation['errors'][] = "{$directive_name} must be between {$spec['min_value']} and {$spec['max_value']}";
                        $validation['valid'] = false;
                    }
                }
                break;
            case 'date':
                if (!strtotime($directive_value)) {
                    $validation['errors'][] = "{$directive_name} must be a valid date";
                    $validation['valid'] = false;
                }
                break;
        }

        return $validation;
    }

    /**
     * Check for directive conflicts
     *
     * @since 1.0.0
     *
     * @param array $robots_meta Robots meta array
     * @return array Validation results
     */
    private function check_directive_conflicts(array $robots_meta): array {
        $validation = [
            'valid' => true,
            'errors' => [],
            'warnings' => [],
            'suggestions' => []
        ];

        if (isset($robots_meta['robots'])) {
            $directives = array_map('trim', explode(',', $robots_meta['robots']));

            // Check for logical conflicts
            if (in_array('noindex', $directives, true) && in_array('nosnippet', $directives, true)) {
                $validation['suggestions'][] = 'nosnippet is redundant when noindex is used';
            }

            if (in_array('noindex', $directives, true) && in_array('noarchive', $directives, true)) {
                $validation['suggestions'][] = 'noarchive is redundant when noindex is used';
            }
        }

        return $validation;
    }

    /**
     * Calculate robots score based on validation
     *
     * @since 1.0.0
     *
     * @param array $validation Validation results
     * @return int Robots score (0-100)
     */
    private function calculate_robots_score(array $validation): int {
        $score = 100;

        // Deduct points for errors and warnings
        $score -= count($validation['errors']) * 20;
        $score -= count($validation['warnings']) * 10;

        return max(0, $score);
    }

    /**
     * Add robots SEO suggestions
     *
     * @since 1.0.0
     *
     * @param array $robots_meta Robots meta array
     * @param array $validation  Current validation results
     * @return array Updated validation results
     */
    private function add_robots_seo_suggestions(array $robots_meta, array $validation): array {
        // Suggest canonical URL if missing
        if (!isset($robots_meta['canonical'])) {
            $validation['suggestions'][] = 'Add canonical URL to prevent duplicate content issues';
        }

        // Suggest max-image-preview for content with images
        if (!isset($robots_meta['max-image-preview'])) {
            $validation['suggestions'][] = 'Consider setting max-image-preview:large for better image visibility in search results';
        }

        // 2025 SEO best practices
        if (isset($robots_meta['robots'])) {
            $directives = array_map('trim', explode(',', $robots_meta['robots']));

            if (!in_array('index', $directives, true) && !in_array('noindex', $directives, true)) {
                $validation['suggestions'][] = 'Explicitly specify index or noindex directive for clarity';
            }
        }

        return $validation;
    }

    /**
     * Convert robots meta to HTML meta tags
     *
     * @since 1.0.0
     *
     * @param array $robots_meta Robots meta array
     * @return array HTML meta tags
     */
    private function convert_robots_to_meta_tags(array $robots_meta): array {
        $meta_tags = [];

        foreach ($robots_meta as $name => $content) {
            if (!empty($content)) {
                if ($name === 'canonical') {
                    $meta_tags[] = [
                        'rel' => 'canonical',
                        'href' => esc_url($content)
                    ];
                } else {
                    $meta_tags[] = [
                        'name' => esc_attr($name),
                        'content' => esc_attr($content)
                    ];
                }
            }
        }

        return $meta_tags;
    }

    /**
     * Detect duplicate content patterns
     *
     * @since 1.0.0
     *
     * @param string $url URL to check for duplicates
     * @return array Duplicate URL patterns
     */
    private function detect_duplicate_patterns(string $url): array {
        $duplicates = [];
        $parsed_url = wp_parse_url($url);

        if (!$parsed_url) {
            return $duplicates;
        }

        $base_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . ($parsed_url['path'] ?? '');

        // Check for common duplicate patterns
        $patterns = [
            $base_url . '/',                    // With trailing slash
            rtrim($base_url, '/'),              // Without trailing slash
            $base_url . '?utm_source=test',     // With tracking parameters
            $base_url . '#section',             // With fragment
            $base_url . '?ref=homepage'         // With referrer parameters
        ];

        foreach ($patterns as $pattern) {
            if ($pattern !== $url) {
                $duplicates[] = $pattern;
            }
        }

        return $duplicates;
    }

    /**
     * Suggest canonical from duplicates
     *
     * @since 1.0.0
     *
     * @param array $duplicates Array of duplicate URLs
     * @return string Suggested canonical URL
     */
    private function suggest_canonical_from_duplicates(array $duplicates): string {
        if (empty($duplicates)) {
            return '';
        }

        // Prefer URLs without query parameters and with trailing slashes
        $scored_urls = [];

        foreach ($duplicates as $url) {
            $score = 0;

            // Prefer URLs without query parameters
            if (strpos($url, '?') === false) {
                $score += 10;
            }

            // Prefer URLs with trailing slashes for directories
            if (substr($url, -1) === '/' && !pathinfo($url, PATHINFO_EXTENSION)) {
                $score += 5;
            }

            // Prefer HTTPS
            if (strpos($url, 'https://') === 0) {
                $score += 3;
            }

            $scored_urls[$url] = $score;
        }

        arsort($scored_urls);
        return array_key_first($scored_urls);
    }

    /**
     * Get duplicate content recommendations
     *
     * @since 1.0.0
     *
     * @param array $duplicates Array of duplicate URLs
     * @return array Recommendations
     */
    private function get_duplicate_content_recommendations(array $duplicates): array {
        $recommendations = [];

        if (count($duplicates) > 1) {
            $recommendations[] = 'Set up 301 redirects from duplicate URLs to the canonical version';
            $recommendations[] = 'Use consistent URL structure across your site';
            $recommendations[] = 'Implement proper canonical tags on all pages';
        }

        foreach ($duplicates as $url) {
            if (strpos($url, '?') !== false) {
                $recommendations[] = 'Remove unnecessary query parameters from URLs';
                break;
            }
        }

        return array_unique($recommendations);
    }
}

```
