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;
}
}