initialize_site_identity_manager(); // Initialize Social Meta Manager $this->initialize_social_meta_manager(); // Initialize Schema Manager for enhanced schema output $this->initialize_schema_manager(); // Initialize Google Analytics Tracking Manager $this->initialize_google_analytics_tracking(); // Initialize current post and context data first add_action('wp', [$this, 'initialize_current_context']); // Use HIGH PRIORITY hooks to override other SEO plugins // Priority 1-5 ensures ThinkRank runs before other SEO plugins // Override WordPress title with HIGH priority add_filter('pre_get_document_title', [$this, 'override_document_title'], 1); add_filter('wp_title', [$this, 'override_wp_title'], 1, 2); // Output meta tags with HIGH priority add_action('wp_head', [$this, 'output_meta_description'], 1); add_action('wp_head', [$this, 'output_seo_meta_tags'], 2); add_action('wp_head', [$this, 'output_open_graph_tags'], 3); add_action('wp_head', [$this, 'output_twitter_card_tags'], 4); add_action('wp_head', [$this, 'output_platform_meta_tags'], 5); add_action('wp_head', [$this, 'output_canonical_url'], 6); // Add Site Identity specific outputs add_action('wp_head', [$this, 'output_site_schema_markup'], 7); add_action('wp_head', [$this, 'output_breadcrumb_schema'], 8); // Add closing comment (runs last) add_action('wp_head', [$this, 'output_closing_comment'], 99); // Add breadcrumb display hook add_action('thinkrank_breadcrumbs', [$this, 'display_breadcrumbs']); // Add robots.txt filter hook add_filter('robots_txt', [$this, 'filter_robots_txt'], 10, 2); } /** * Initialize Site Identity Manager * * @return void */ private function initialize_site_identity_manager(): void { if (!class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) { require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-site-identity-manager.php'; } $this->site_identity_manager = new \ThinkRank\SEO\Site_Identity_Manager(); } /** * Initialize Social Meta Manager * * @return void */ private function initialize_social_meta_manager(): void { if (!class_exists('ThinkRank\\SEO\\Social_Meta_Manager')) { require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-social-meta-manager.php'; } $this->social_manager = new \ThinkRank\SEO\Social_Meta_Manager(); } /** * Initialize Schema Manager for enhanced schema output * * @return void */ private function initialize_schema_manager(): void { if (!class_exists('ThinkRank\\SEO\\Schema_Management_System')) { require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-schema-management-system.php'; } // Initialize Schema Manager and store reference for integration $this->schema_manager = new \ThinkRank\SEO\Schema_Management_System(); } /** * Initialize Google Analytics Tracking Manager * * @return void */ private function initialize_google_analytics_tracking(): void { if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) { require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php'; } // Initialize Google Analytics Tracking Manager new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager(); } /** * Initialize current context and post data * * @return void */ public function initialize_current_context(): void { // Determine current context $this->current_context = $this->detect_current_context(); // Initialize post data if singular if (is_singular()) { $post_id = get_the_ID(); if ($post_id) { $this->current_post_id = $post_id; $this->current_metadata = $this->get_post_seo_metadata($post_id); } } // Load site identity data $this->load_site_identity_data(); } /** * Detect current page context * * @return string Current context type */ private function detect_current_context(): string { if (is_home() || is_front_page()) { return 'homepage'; } elseif (is_single()) { return 'post'; } elseif (is_page()) { return 'page'; } elseif (is_category()) { return 'category'; } elseif (is_tag()) { return 'tag'; } elseif (is_author()) { return 'author'; } elseif (is_search()) { return 'search'; } elseif (is_archive()) { return 'archive'; } return 'site'; } /** * Load site identity data * * @return void */ private function load_site_identity_data(): void { if ($this->site_identity_manager && $this->site_identity_data === null) { $this->site_identity_data = $this->site_identity_manager->get_output_data('site', null); } } /** * Get SEO metadata for a post * * @param int $post_id Post ID * @return array SEO metadata */ private function get_post_seo_metadata(int $post_id): array { return [ 'title' => get_post_meta($post_id, '_thinkrank_seo_title', true), 'description' => get_post_meta($post_id, '_thinkrank_meta_description', true), 'focus_keyword' => get_post_meta($post_id, '_thinkrank_focus_keyword', true), 'seo_score' => get_post_meta($post_id, '_thinkrank_seo_score', true), ]; } /** * Override WordPress document title (HIGH PRIORITY) * Post-specific metadata takes priority over Site Identity templates * * @param string $title Original title * @return string Modified title */ public function override_document_title($title): string { // First priority: Post-specific ThinkRank metadata if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['title'])) { return $this->current_metadata['title']; } // Second priority: Site Identity templates $generated_title = $this->generate_context_title(); if ($generated_title) { return $generated_title; } return $title; } /** * Override WordPress wp_title (HIGH PRIORITY) * Post-specific metadata takes priority over Site Identity templates * * @param string $title Original title * @param string $sep Title separator * @return string Modified title */ public function override_wp_title(string $title, string $sep = ''): string { // First priority: Post-specific ThinkRank metadata if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['title'])) { $site_name = get_bloginfo('name'); return $this->current_metadata['title'] . ($sep ? " $sep " : ' | ') . $site_name; } // Second priority: Site Identity templates $generated_title = $this->generate_context_title(); if ($generated_title) { return $generated_title; } return $title; } /** * Output meta description (HIGH PRIORITY) * Uses post-specific metadata or Site Identity default fallback * * @return void */ public function output_meta_description(): void { $description = $this->get_meta_description(); if ($description) { // Output main ThinkRank SEO header comment (only once) static $header_output = false; if (!$header_output) { echo "\n"; $header_output = true; } // Ensure description is within optimal length (150-160 characters) if (strlen($description) > 160) { $description = wp_trim_words($description, 25, '...'); } echo "\n"; echo '' . "\n"; echo "\n"; } } /** * Output SEO meta tags * * @return void */ public function output_seo_meta_tags(): void { echo "\n"; // Output robots meta tag with proper directives $robots_content = $this->get_robots_meta_content(); echo '' . "\n"; // Output focus keyword as meta keywords (if available and not empty) if (!empty($this->current_metadata['focus_keyword'])) { $keywords = trim($this->current_metadata['focus_keyword']); if (!empty($keywords)) { echo '' . "\n"; } } // Output local SEO meta tags if business info is available $this->output_local_seo_meta_tags(); // Output generator meta tag echo '' . "\n"; // Output viewport meta tag if not already present if (!has_action('wp_head', 'wp_site_icon') || !wp_is_mobile()) { echo '' . "\n"; } echo "\n"; } /** * Get robots meta content based on context and settings * * @return string Robots meta content */ private function get_robots_meta_content(): string { $robots = []; // Default directives $robots[] = 'index'; $robots[] = 'follow'; // Add advanced directives for better SEO $robots[] = 'max-snippet:-1'; $robots[] = 'max-image-preview:large'; $robots[] = 'max-video-preview:-1'; // Check for specific page settings if (is_singular()) { // Check if post has noindex setting $noindex = get_post_meta(get_the_ID(), '_thinkrank_noindex', true); if ($noindex) { $robots = ['noindex', 'nofollow']; } } // Check for archive pages if (is_archive() || is_search()) { // Allow indexing of category/tag archives but be more conservative if (is_paged()) { $robots = ['noindex', 'follow']; } } // Apply filters for customization $robots = apply_filters('thinkrank_robots_meta', $robots); return implode(', ', $robots); } /** * Output local SEO meta tags for business information * * @return void */ private function output_local_seo_meta_tags(): void { if (!$this->site_identity_manager) { return; } $settings = $this->site_identity_manager->get_settings('site'); // Only output if local SEO is enabled and business info is available if (empty($settings['local_seo_enabled']) || empty($settings['business_name'])) { return; } echo "\n"; // NAP (Name, Address, Phone) Consistency Meta Tags if (!empty($settings['business_name'])) { echo '' . "\n"; } // Business address components if (!empty($settings['business_address'])) { echo '' . "\n"; } if (!empty($settings['business_city'])) { echo '' . "\n"; echo '' . "\n"; } if (!empty($settings['business_state'])) { echo '' . "\n"; } if (!empty($settings['business_postal_code'])) { echo '' . "\n"; } if (!empty($settings['business_country'])) { echo '' . "\n"; } // Phone number if (!empty($settings['business_phone'])) { echo '' . "\n"; } // Email address if (!empty($settings['business_email'])) { echo '' . "\n"; } // Geo-location meta tags (if coordinates are available) if (!empty($settings['business_latitude']) && !empty($settings['business_longitude'])) { $coordinates = $settings['business_latitude'] . ';' . $settings['business_longitude']; echo '' . "\n"; echo '' . "\n"; } // Regional meta tag (state/country combination) if (!empty($settings['business_state']) && !empty($settings['business_country'])) { $region = strtoupper($settings['business_country']) . '-' . strtoupper($settings['business_state']); echo '' . "\n"; } // Business hours in structured format if (!empty($settings['business_hours']) && is_array($settings['business_hours'])) { $formatted_hours = $this->format_business_hours_for_meta($settings['business_hours']); if (!empty($formatted_hours)) { echo '' . "\n"; } } // Business type if (!empty($settings['business_type'])) { echo '' . "\n"; } echo "\n"; } /** * Format business hours for meta tag output * * @param array $business_hours Business hours array * @return string Formatted hours string */ private function format_business_hours_for_meta(array $business_hours): string { $formatted_days = []; $day_abbreviations = [ 'monday' => 'Mo', 'tuesday' => 'Tu', 'wednesday' => 'We', 'thursday' => 'Th', 'friday' => 'Fr', 'saturday' => 'Sa', 'sunday' => 'Su' ]; foreach ($day_abbreviations as $day => $abbrev) { if (isset($business_hours[$day]) && !empty($business_hours[$day])) { $day_data = $business_hours[$day]; if (!empty($day_data['closed']) || empty($day_data['open']) || empty($day_data['close'])) { continue; // Skip closed days } $formatted_days[] = $abbrev . ' ' . $day_data['open'] . '-' . $day_data['close']; } } return implode(', ', $formatted_days); } /** * Output social media Open Graph tags from Social Meta Manager * * @param array $og_tags Open Graph tags array * @return void */ private function output_social_og_tags(array $og_tags): void { echo "\n"; // Define optimal order for Open Graph tags $og_order = [ 'og:title', 'og:description', 'og:type', 'og:url', 'og:site_name', 'og:locale', 'og:image', 'og:image:width', 'og:image:height', 'og:image:type', 'og:image:alt', 'article:published_time', 'article:modified_time', 'article:author', 'article:section' ]; // Output tags in optimal order foreach ($og_order as $property) { if (!empty($og_tags[$property])) { echo '' . "\n"; } } // Output any remaining tags not in the order list foreach ($og_tags as $property => $content) { if (!empty($content) && !in_array($property, $og_order, true)) { echo '' . "\n"; } } echo "\n"; } /** * Output social media Twitter Card tags from Social Meta Manager * * @param array $twitter_tags Twitter Card tags array * @return void */ private function output_social_twitter_tags(array $twitter_tags): void { echo "\n"; // Define optimal order for Twitter Card tags $twitter_order = [ 'twitter:card', 'twitter:title', 'twitter:description', 'twitter:site', 'twitter:creator', 'twitter:image', 'twitter:image:alt', 'twitter:player', 'twitter:player:width', 'twitter:player:height' ]; // Output tags in optimal order foreach ($twitter_order as $name) { if (!empty($twitter_tags[$name])) { echo '' . "\n"; } } // Output any remaining tags not in the order list foreach ($twitter_tags as $name => $content) { if (!empty($content) && !in_array($name, $twitter_order, true)) { echo '' . "\n"; } } echo "\n"; } /** * Output platform-specific meta tags * * @return void */ public function output_platform_meta_tags(): void { // Try Social Meta Manager for platform tags if ($this->social_manager) { // Map context for Social Meta Manager (homepage -> site for site-wide settings) $social_context = $this->current_context === 'homepage' ? 'site' : $this->current_context; $social_data = $this->social_manager->get_output_data( $social_context, $this->current_post_id ); if ($social_data['enabled'] && !empty($social_data['platform_tags'])) { $this->output_social_platform_tags($social_data['platform_tags']); } } } /** * Output social media platform tags from Social Meta Manager * * @param array $platform_tags Platform tags array * @return void */ private function output_social_platform_tags(array $platform_tags): void { echo "\n"; foreach ($platform_tags as $name => $content) { if (!empty($content)) { // Determine if it should be property or name attribute if (strpos($name, 'fb:') === 0) { // Facebook tags use property attribute echo '' . "\n"; } else { // Other platform tags use name attribute echo '' . "\n"; } } } echo "\n"; } /** * Output Open Graph meta tags (HIGH PRIORITY) * Uses Social Meta Manager with fallback to Site Identity templates * * @return void */ public function output_open_graph_tags(): void { // Priority 1: Try Social Meta Manager (Social Media tab settings) if ($this->social_manager) { // Map context for Social Meta Manager (homepage -> site for site-wide settings) $social_context = $this->current_context === 'homepage' ? 'site' : $this->current_context; $social_data = $this->social_manager->get_output_data( $social_context, $this->current_post_id ); if ($social_data['enabled'] && !empty($social_data['og_tags'])) { $this->output_social_og_tags($social_data['og_tags']); return; } } // Priority 2: Fallback to current implementation $this->output_basic_og_tags(); } /** * Output basic Open Graph tags (fallback implementation) * * @return void */ private function output_basic_og_tags(): void { // Get title using priority system: post-specific > Site Identity > default $title = ''; if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['title'])) { $title = $this->current_metadata['title']; } else { $title = $this->generate_context_title(); } if (!$title) { $title = is_singular() ? get_the_title() : get_bloginfo('name'); } $description = $this->get_meta_description(); if (!$description) { $description = is_singular() ? wp_trim_words(get_the_excerpt(), 30) : get_bloginfo('description'); } $url = is_singular() ? get_permalink() : home_url(); $site_name = $this->site_identity_data && !empty($this->site_identity_data['identity']['site_name']) ? $this->site_identity_data['identity']['site_name'] : get_bloginfo('name'); // Determine proper og:type based on context $og_type = 'website'; if (is_singular('post')) { $og_type = 'article'; } elseif (is_singular('page')) { $og_type = 'website'; } elseif (is_home() || is_front_page()) { $og_type = 'website'; } echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; // Add featured image if available (only for singular posts/pages) if (is_singular() && $this->current_post_id) { if (has_post_thumbnail($this->current_post_id)) { $image_url = get_the_post_thumbnail_url($this->current_post_id, 'large'); echo "\n"; echo "\n"; // Get image dimensions and alt text $image_id = get_post_thumbnail_id($this->current_post_id); $image_meta = wp_get_attachment_metadata($image_id); if ($image_meta) { echo "\n"; echo "\n"; echo "\n"; } // Add image alt text $image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true); if ($image_alt) { echo "\n"; } } // Add article specific tags for posts only if ($og_type === 'article') { echo '' . "\n"; echo '' . "\n"; // Add author $author_id = get_post_field('post_author', $this->current_post_id); $author_name = get_the_author_meta('display_name', $author_id); echo "\n"; // Add categories as article:section if (is_single()) { $categories = get_the_category($this->current_post_id); if (!empty($categories)) { echo "name) . "\" />\n"; } } } } echo "\n"; } /** * Output Twitter Card meta tags (HIGH PRIORITY) * Uses Social Meta Manager with fallback to Site Identity templates * * @return void */ public function output_twitter_card_tags(): void { // Priority 1: Try Social Meta Manager (Social Media tab settings) if ($this->social_manager) { // Map context for Social Meta Manager (homepage -> site for site-wide settings) $social_context = $this->current_context === 'homepage' ? 'site' : $this->current_context; $social_data = $this->social_manager->get_output_data( $social_context, $this->current_post_id ); if ($social_data['enabled'] && !empty($social_data['twitter_tags'])) { $this->output_social_twitter_tags($social_data['twitter_tags']); return; } } // Priority 2: Fallback to current implementation $this->output_basic_twitter_tags(); } /** * Output basic Twitter Card tags (fallback implementation) * * @return void */ private function output_basic_twitter_tags(): void { // Get title using priority system: post-specific > Site Identity > default $title = ''; if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['title'])) { $title = $this->current_metadata['title']; } else { $title = $this->generate_context_title(); } if (!$title) { $title = is_singular() ? get_the_title() : get_bloginfo('name'); } $description = $this->get_meta_description(); if (!$description) { $description = is_singular() ? wp_trim_words(get_the_excerpt(), 30) : get_bloginfo('description'); } // Determine card type based on image availability $card_type = 'summary'; if (is_singular() && $this->current_post_id && has_post_thumbnail($this->current_post_id)) { $card_type = 'summary_large_image'; } echo "\n"; echo '' . "\n"; echo "\n"; echo "\n"; // Add Twitter image with proper fallback priority $twitter_image_url = $this->get_twitter_image_with_fallback(); if ($twitter_image_url) { echo "\n"; // Add image alt text for accessibility (if it's a featured image) if (is_singular() && $this->current_post_id && has_post_thumbnail($this->current_post_id)) { $featured_image_url = get_the_post_thumbnail_url($this->current_post_id, 'large'); if ($twitter_image_url === $featured_image_url) { $image_id = get_post_thumbnail_id($this->current_post_id); $image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true); if ($image_alt) { echo "\n"; } } } } // Add site Twitter handle if configured if ($this->site_identity_data && !empty($this->site_identity_data['social']['twitter_username'])) { $twitter_handle = $this->site_identity_data['social']['twitter_username']; // Ensure handle starts with @ if (strpos($twitter_handle, '@') !== 0) { $twitter_handle = '@' . $twitter_handle; } echo "\n"; } echo "\n"; } /** * Output canonical URL * * @return void */ public function output_canonical_url(): void { if (!is_singular()) { return; } $canonical_url = $this->current_post_id ? get_permalink($this->current_post_id) : get_permalink(); echo "\n"; echo "\n"; echo "\n"; } /** * Check if ThinkRank has metadata for current post * * @return bool True if has ThinkRank metadata */ private function has_thinkrank_metadata(): bool { if (!is_singular()) { return false; } return !empty($this->current_metadata['title']) || !empty($this->current_metadata['description']); } /** * Check if current page has SEO data (public method for template functions) * * @return bool True if has SEO data */ public function has_seo_data(): bool { // Check if Site Identity is enabled and active if ($this->site_identity_data && $this->site_identity_data['enabled']) { return true; } // Check if post has ThinkRank metadata return $this->has_thinkrank_metadata(); } /** * Get current breadcrumbs data (public method for template functions) * * @return array|null Breadcrumb data or null if not available */ public function get_current_breadcrumbs(): ?array { if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return null; } $settings = $this->site_identity_manager->get_settings('site'); if (empty($settings['breadcrumbs_enabled'])) { return null; } return $this->generate_breadcrumbs($settings); } /** * Get current SEO metadata * * @return array Current metadata */ public function get_current_metadata(): array { return $this->current_metadata; } /** * Generate title based on current context using Site Identity templates * * @return string|null Generated title or null if no template available */ private function generate_context_title(): ?string { if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return null; } $template = $this->get_title_template_for_context(); if (!$template) { return null; } $placeholders = $this->get_title_placeholders(); return $this->process_title_template($template, $placeholders); } /** * Get title template for current context * * @return string|null Template string or null if not found */ private function get_title_template_for_context(): ?string { $settings = $this->site_identity_manager->get_settings('site'); switch ($this->current_context) { case 'homepage': return $settings['homepage_title'] ?? null; case 'post': return $settings['post_title'] ?? null; case 'page': return $settings['page_title'] ?? null; case 'category': return $settings['category_title'] ?? null; case 'tag': return $settings['tag_title'] ?? null; case 'author': return $settings['author_title'] ?? null; case 'search': return $settings['search_title'] ?? null; case 'archive': return $settings['archive_title'] ?? null; default: return null; } } /** * Get title placeholders for current context * * @return array Placeholder values */ private function get_title_placeholders(): array { global $post, $wp_query; $settings = $this->site_identity_manager->get_settings('site'); $separator = $this->get_title_separator($settings['title_separator'] ?? 'pipe'); $placeholders = [ '%site_title%' => $settings['site_name'] ?? get_bloginfo('name'), '%site_name%' => $settings['site_name'] ?? get_bloginfo('name'), '%site_description%' => $settings['site_description'] ?? get_bloginfo('description'), '%tagline%' => $settings['tagline'] ?? get_bloginfo('description'), '%separator%' => ' ' . $separator . ' ', '%date%' => gmdate('F Y'), ]; // Context-specific placeholders switch ($this->current_context) { case 'post': case 'page': if ($this->current_post_id) { $placeholders['%post_title%'] = get_the_title($this->current_post_id); $placeholders['%page_title%'] = get_the_title($this->current_post_id); $post_author = get_post_field('post_author', $this->current_post_id); $placeholders['%author%'] = get_the_author_meta('display_name', $post_author); $placeholders['%author_name%'] = get_the_author_meta('display_name', $post_author); // Get categories for posts $post_type = get_post_type($this->current_post_id); if ($post_type === 'post') { $categories = get_the_category($this->current_post_id); $placeholders['%category%'] = !empty($categories) ? $categories[0]->name : ''; } } break; case 'category': $category = get_queried_object(); if ($category) { $placeholders['%category_title%'] = $category->name; $placeholders['%category%'] = $category->name; } break; case 'tag': $tag = get_queried_object(); if ($tag) { $placeholders['%tag_title%'] = $tag->name; $placeholders['%tag%'] = $tag->name; } break; case 'author': $author = get_queried_object(); if ($author) { $placeholders['%author_name%'] = $author->display_name; $placeholders['%author%'] = $author->display_name; } break; case 'search': $placeholders['%search_term%'] = get_search_query(); break; case 'archive': $placeholders['%archive_title%'] = get_the_archive_title(); break; } return $placeholders; } /** * Process title template with placeholders * * @param string $template Template string * @param array $placeholders Placeholder values * @return string Processed title */ private function process_title_template(string $template, array $placeholders): string { $title = str_replace(array_keys($placeholders), array_values($placeholders), $template); // Clean up multiple separators and extra spaces $separator = $placeholders['%separator%'] ?? ' | '; $title = preg_replace('/\s*' . preg_quote(trim($separator), '/') . '\s*' . preg_quote(trim($separator), '/') . '\s*/', $separator, $title); $title = preg_replace('/\s+/', ' ', $title); $title = trim($title); // Remove trailing separator $separator_trimmed = trim($separator); if (substr($title, -strlen($separator_trimmed)) === $separator_trimmed) { $title = trim(substr($title, 0, -strlen($separator_trimmed))); } return $title; } /** * Get title separator symbol * * @param string $separator_type Separator type * @return string Separator symbol */ private function get_title_separator(string $separator_type): string { $separators = [ 'pipe' => '|', 'dash' => '-', 'ndash' => '–', 'mdash' => '—', 'middot' => '·', 'bull' => '•', 'star' => '*', 'smstar' => '⋆', 'gt' => '>', 'raquo' => '»', ]; return $separators[$separator_type] ?? '|'; } /** * Get meta description with fallback system * * @return string|null Meta description or null if none available */ private function get_meta_description(): ?string { // First priority: Post-specific ThinkRank metadata if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['description'])) { return $this->current_metadata['description']; } // Second priority: Site Identity default meta description if ($this->site_identity_data && $this->site_identity_data['enabled']) { $settings = $this->site_identity_manager->get_settings('site'); $default_description = $settings['default_meta_description'] ?? ''; if (!empty($default_description)) { return $default_description; } } // Third priority: Generate from content for posts/pages if (is_singular() && $this->current_post_id) { $post_content = get_post_field('post_content', $this->current_post_id); if ($post_content) { $excerpt = wp_trim_words(wp_strip_all_tags($post_content), 25, '...'); if (!empty($excerpt)) { return $excerpt; } } } // Fourth priority: Site description for homepage if (is_home() || is_front_page()) { $site_description = get_bloginfo('description'); if (!empty($site_description)) { return $site_description; } } return null; } /** * Output site-wide schema markup with priority system * * Priority: Schema Manager > Site Identity (like Twitter Cards approach) * * @return void */ public function output_site_schema_markup(): void { $has_schema_manager_output = false; // PRIORITY 1: Always output site-wide schemas (Organization, Website, LocalBusiness, Person) if ($this->schema_manager) { $site_wide_schemas = $this->schema_manager->get_deployed_schemas('site', null); if (!empty($site_wide_schemas)) { foreach ($site_wide_schemas as $schema_type => $schema_info) { $this->output_schema_markup($schema_info['data'], $schema_type, 'Schema Manager'); } $has_schema_manager_output = true; } } // PRIORITY 2: Also output page-specific schemas (Article, HowTo, FAQ, etc.) on individual posts/pages if ($this->schema_manager && (is_single() || is_page())) { $context_type = is_page() ? 'page' : 'post'; $context_id = get_the_ID(); $page_specific_schemas = $this->schema_manager->get_deployed_schemas($context_type, $context_id); if (!empty($page_specific_schemas)) { foreach ($page_specific_schemas as $schema_type => $schema_info) { $this->output_schema_markup($schema_info['data'], $schema_type, 'Schema Manager'); } $has_schema_manager_output = true; } } // Skip Site Identity fallback if any Schema Manager schemas were output if ($has_schema_manager_output) { return; } // PRIORITY 2: Fall back to Site Identity schemas (like basic Twitter Cards) if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return; } $settings = $this->site_identity_manager->get_settings('site'); // Only output on homepage or if organization schema is enabled if (!is_home() && !is_front_page() && empty($settings['organization_schema'])) { return; } $schema = $this->generate_organization_schema($settings); if ($schema) { $this->output_schema_markup($schema, 'Organization', 'Site Identity'); } } /** * Output schema markup with consistent formatting * * @param array $schema_data Schema data * @param string $schema_type Schema type name * @param string $source Source of schema (Schema Manager, Site Identity, etc.) * @return void */ private function output_schema_markup(array $schema_data, string $schema_type, string $source): void { if (empty($schema_data)) { return; } echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; } /** * Generate organization schema markup * * @param array $settings Site identity settings * @return array|null Schema data or null if insufficient data */ private function generate_organization_schema(array $settings): ?array { $site_name = $settings['site_name'] ?? get_bloginfo('name'); $site_description = $settings['site_description'] ?? get_bloginfo('description'); if (empty($site_name)) { return null; } $schema = [ '@context' => 'https://schema.org', '@type' => 'Organization', '@id' => home_url() . '#organization', 'name' => $site_name, 'url' => home_url(), ]; // Add description if available if (!empty($site_description)) { $schema['description'] = $site_description; } // Add logo if available with proper ImageObject structure if (!empty($settings['logo_url'])) { $schema['logo'] = [ '@type' => 'ImageObject', '@id' => home_url() . '#logo', 'url' => $settings['logo_url'], 'contentUrl' => $settings['logo_url'], 'caption' => $site_name . ' Logo' ]; // Also add as image property $schema['image'] = $schema['logo']; } // Add social media accounts if available $social_urls = []; if (!empty($this->site_identity_data['social'])) { $social_data = $this->site_identity_data['social']; if (!empty($social_data['facebook_url'])) { $social_urls[] = $social_data['facebook_url']; } if (!empty($social_data['twitter_username'])) { $twitter_url = 'https://twitter.com/' . ltrim($social_data['twitter_username'], '@'); $social_urls[] = $twitter_url; } if (!empty($social_data['linkedin_url'])) { $social_urls[] = $social_data['linkedin_url']; } if (!empty($social_data['instagram_url'])) { $social_urls[] = $social_data['instagram_url']; } if (!empty($social_data['youtube_url'])) { $social_urls[] = $social_data['youtube_url']; } } if (!empty($social_urls)) { $schema['sameAs'] = $social_urls; } // Add contact information if available if (!empty($settings['contact_email'])) { $schema['email'] = $settings['contact_email']; } return $schema; } /** * Output breadcrumb schema markup * * @return void */ public function output_breadcrumb_schema(): void { if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return; } $settings = $this->site_identity_manager->get_settings('site'); // Only output if breadcrumbs are enabled if (empty($settings['breadcrumbs_enabled'])) { return; } $breadcrumbs = $this->generate_breadcrumbs($settings); if (!empty($breadcrumbs['schema'])) { echo "\n"; echo '' . "\n"; echo "\n"; } } /** * Output closing comment for ThinkRank SEO * * @return void */ public function output_closing_comment(): void { // Only output if we've output any SEO content static $header_output = false; if ($header_output || $this->has_seo_output()) { echo "\n"; } } /** * Check if any SEO content has been output * * @return bool True if SEO content was output */ private function has_seo_output(): bool { // Check if we have meta description or any other SEO data return !empty($this->get_meta_description()) || $this->has_thinkrank_metadata() || ($this->site_identity_data && $this->site_identity_data['enabled']); } /** * Display breadcrumbs HTML * * @return void */ public function display_breadcrumbs(): void { if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return; } $settings = $this->site_identity_manager->get_settings('site'); // Only display if breadcrumbs are enabled if (empty($settings['breadcrumbs_enabled'])) { return; } $breadcrumbs = $this->generate_breadcrumbs($settings); if (!empty($breadcrumbs['html'])) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is properly escaped in generate_breadcrumb_html method echo $breadcrumbs['html']; } } /** * Generate breadcrumbs data * * @param array $settings Breadcrumb settings * @return array Breadcrumb data with HTML and schema */ private function generate_breadcrumbs(array $settings): array { $breadcrumbs = [ 'items' => [], 'html' => '', 'schema' => null ]; // Get breadcrumb items $items = $this->get_breadcrumb_items($settings); if (empty($items)) { return $breadcrumbs; } $breadcrumbs['items'] = $items; // Generate HTML $breadcrumbs['html'] = $this->generate_breadcrumb_html($items, $settings); // Generate schema $breadcrumbs['schema'] = $this->generate_breadcrumb_schema($items); return $breadcrumbs; } /** * Filter WordPress robots.txt output * * @param string $output The default robots.txt output * @param string $public Whether the site is public * @return string Modified robots.txt content */ public function filter_robots_txt(string $output, string $public): string { // Only override if Site Identity is enabled and robots.txt management is enabled if (!$this->site_identity_data || !$this->site_identity_data['enabled']) { return $output; } $settings = $this->site_identity_manager->get_settings('site'); if (empty($settings['robots_txt_enabled'])) { return $output; } // Generate ThinkRank robots.txt content try { $robots_data = $this->site_identity_manager->generate_robots_txt(); if (!empty($robots_data['content'])) { return $robots_data['content']; } } catch (\Exception $e) { // Robots.txt generation failed - fallback to default output } // Fallback to default output if generation fails return $output; } /** * Get breadcrumb items for current page * * @param array $settings Breadcrumb settings * @return array Breadcrumb items */ private function get_breadcrumb_items(array $settings): array { $items = []; // Always start with home $home_text = $settings['breadcrumb_home_text'] ?? 'Home'; $items[] = [ 'title' => $home_text, 'url' => home_url(), 'position' => 1 ]; $position = 2; if (is_single()) { $current_post_id = get_the_ID(); if ($current_post_id) { // Add categories for posts $post_type = get_post_type($current_post_id); if ($post_type === 'post') { $categories = get_the_category($current_post_id); if (!empty($categories)) { $category = $categories[0]; $items[] = [ 'title' => $category->name, 'url' => get_category_link($category->term_id), 'position' => $position++ ]; } } // Add current post if (empty($settings['show_current_page']) || $settings['show_current_page']) { $items[] = [ 'title' => get_the_title($current_post_id), 'url' => get_permalink($current_post_id), 'position' => $position, 'current' => true ]; } } } elseif (is_page()) { $current_post_id = get_the_ID(); if ($current_post_id) { // Add parent pages $parents = []; $parent_id = wp_get_post_parent_id($current_post_id); while ($parent_id) { $parent = get_post($parent_id); if ($parent) { $parents[] = [ 'title' => get_the_title($parent->ID), 'url' => get_permalink($parent->ID), 'position' => 0 // Will be set later ]; $parent_id = $parent->post_parent; } else { break; } } // Reverse to get correct order $parents = array_reverse($parents); // Add parents with correct positions foreach ($parents as $parent) { $parent['position'] = $position++; $items[] = $parent; } // Add current page if (empty($settings['show_current_page']) || $settings['show_current_page']) { $items[] = [ 'title' => get_the_title($current_post_id), 'url' => get_permalink($current_post_id), 'position' => $position, 'current' => true ]; } } } elseif (is_category()) { $category = get_queried_object(); // Add parent categories $parents = []; $parent_id = $category->parent; while ($parent_id) { $parent = get_category($parent_id); if ($parent && !is_wp_error($parent)) { $parents[] = [ 'title' => $parent->name, 'url' => get_category_link($parent->term_id), 'position' => 0 // Will be set later ]; $parent_id = $parent->parent; } else { break; } } // Reverse to get correct order $parents = array_reverse($parents); // Add parents with correct positions foreach ($parents as $parent) { $parent['position'] = $position++; $items[] = $parent; } // Add current category if (empty($settings['show_current_page']) || $settings['show_current_page']) { $items[] = [ 'title' => $category->name, 'url' => get_category_link($category->term_id), 'position' => $position, 'current' => true ]; } } return $items; } /** * Generate breadcrumb HTML * * @param array $items Breadcrumb items * @param array $settings Breadcrumb settings * @return string HTML output */ private function generate_breadcrumb_html(array $items, array $settings): string { if (empty($items)) { return ''; } $separator = $settings['breadcrumb_separator'] ?? '>'; $prefix = $settings['breadcrumb_prefix'] ?? ''; $html = ''; return $html; } /** * Generate breadcrumb schema markup * * @param array $items Breadcrumb items * @return array Schema data */ private function generate_breadcrumb_schema(array $items): array { if (empty($items)) { return []; } $schema_items = []; foreach ($items as $item) { $schema_items[] = [ '@type' => 'ListItem', 'position' => $item['position'], 'name' => $item['title'], 'item' => $item['url'] ]; } return [ '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $schema_items ]; } /** * Debug method to verify priority system (remove in production) * * @return array Debug information about current page SEO data */ public function debug_seo_priority(): array { if (!defined('WP_DEBUG') || !WP_DEBUG) { return []; } $debug = [ 'context' => $this->current_context, 'is_singular' => is_singular(), 'post_id' => $this->current_post_id, 'has_post_metadata' => $this->has_thinkrank_metadata(), 'post_metadata' => $this->current_metadata, 'site_identity_enabled' => $this->site_identity_data && $this->site_identity_data['enabled'], 'final_title' => '', 'final_description' => '', 'title_source' => '', 'description_source' => '' ]; // Determine title source if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['title'])) { $debug['final_title'] = $this->current_metadata['title']; $debug['title_source'] = 'post_metadata'; } else { $generated_title = $this->generate_context_title(); if ($generated_title) { $debug['final_title'] = $generated_title; $debug['title_source'] = 'site_identity_template'; } else { $debug['final_title'] = is_singular() ? get_the_title() : get_bloginfo('name'); $debug['title_source'] = 'wordpress_default'; } } // Determine description source $description = $this->get_meta_description(); if ($description) { $debug['final_description'] = $description; if ($this->has_thinkrank_metadata() && !empty($this->current_metadata['description'])) { $debug['description_source'] = 'post_metadata'; } elseif ($this->site_identity_data && $this->site_identity_data['enabled']) { $settings = $this->site_identity_manager->get_settings('site'); if (!empty($settings['default_meta_description'])) { $debug['description_source'] = 'site_identity_default'; } else { $debug['description_source'] = 'auto_generated'; } } else { $debug['description_source'] = 'auto_generated_or_site_description'; } } else { $debug['description_source'] = 'none'; } return $debug; } /** * Get Twitter image with proper fallback priority * * @since 1.0.0 * * @return string|null Twitter image URL or null if none available */ private function get_twitter_image_with_fallback(): ?string { // For posts, check for post-specific Twitter image meta first (future enhancement) if (is_singular() && $this->current_post_id) { $post_twitter_image = get_post_meta($this->current_post_id, '_thinkrank_twitter_image', true); if (!empty($post_twitter_image)) { return $post_twitter_image; } // Check featured image as fallback for posts if (has_post_thumbnail($this->current_post_id)) { $featured_image_url = get_the_post_thumbnail_url($this->current_post_id, 'large'); if ($featured_image_url) { return $featured_image_url; } } } // Check Social Meta Manager settings for Twitter-specific default image if ($this->social_manager) { $social_context = $this->current_context === 'homepage' ? 'site' : $this->current_context; $social_settings = $this->social_manager->get_settings($social_context, $this->current_post_id); // Prioritize Twitter-specific default image if (!empty($social_settings['default_twitter_image'])) { return $social_settings['default_twitter_image']; } // Fallback to Open Graph default image if (!empty($social_settings['default_og_image'])) { return $social_settings['default_og_image']; } // Final fallback to generic default image if (!empty($social_settings['default_image'])) { return $social_settings['default_image']; } } // Check Site Identity data for social images if ($this->site_identity_data && !empty($this->site_identity_data['social'])) { $social_data = $this->site_identity_data['social']; // Check for any configured social image if (!empty($social_data['default_image'])) { return $social_data['default_image']; } } return null; } }