settings = $settings; } /** * Register hooks. * * @return void */ public function init() { add_filter( 'agizoai_agentic_is_noindex', array( $this, 'filter_is_noindex' ), 10, 2 ); } /** * Return supported integration sources with detection status. * * @return array> */ public function get_sources() { return array( 'real_seo_master' => array( 'label' => __( 'Real SEO Master', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_real_seo_master_enabled', 'detected' => $this->detect_real_seo_master(), 'priority' => 10, 'description' => __( 'Reads Real SEO Master-style title, description, robots, canonical and social-image metadata when available.', 'agizoai-agentic-browsing-optimizer' ), ), 'yoast' => array( 'label' => __( 'Yoast SEO', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_yoast_enabled', 'detected' => $this->detect_yoast(), 'priority' => 20, 'description' => __( 'Reads Yoast SEO metadata including SEO title, meta description, canonical URL, focus keyphrase, robots and Open Graph image.', 'agizoai-agentic-browsing-optimizer' ), ), 'rank_math' => array( 'label' => __( 'Rank Math', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_rankmath_enabled', 'detected' => $this->detect_rank_math(), 'priority' => 30, 'description' => __( 'Reads Rank Math metadata including title, description, canonical URL, focus keyword, robots and social images.', 'agizoai-agentic-browsing-optimizer' ), ), 'aioseo' => array( 'label' => __( 'All in One SEO', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_aioseo_enabled', 'detected' => $this->detect_aioseo(), 'priority' => 40, 'description' => __( 'Reads common AIOSEO metadata keys for public title, description, canonical and robots hints.', 'agizoai-agentic-browsing-optimizer' ), ), 'seopress' => array( 'label' => __( 'SEOPress', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_seopress_enabled', 'detected' => $this->detect_seopress(), 'priority' => 50, 'description' => __( 'Reads SEOPress title, description, canonical, target keywords, robots and social-image metadata.', 'agizoai-agentic-browsing-optimizer' ), ), 'wordpress_core' => array( 'label' => __( 'WordPress Core Fallback', 'agizoai-agentic-browsing-optimizer' ), 'option' => 'integration_wordpress_core_enabled', 'detected' => true, 'priority' => 90, 'description' => __( 'Uses public post titles, excerpts, permalinks and featured-image data when no SEO plugin metadata exists.', 'agizoai-agentic-browsing-optimizer' ), ), ); } /** * Return enabled and detected source keys in priority order. * * @return array */ public function get_enabled_source_keys() { if ( ! $this->settings->get( 'integrations_enabled', 1 ) ) { return array(); } $sources = $this->get_sources(); uasort( $sources, static function ( $a, $b ) { return absint( $a['priority'] ) <=> absint( $b['priority'] ); } ); $primary = sanitize_key( (string) $this->settings->get( 'integration_primary_source', 'auto' ) ); if ( 'auto' !== $primary && isset( $sources[ $primary ] ) && $this->source_is_enabled( $primary, $sources[ $primary ] ) ) { return array_values( array_unique( array_merge( array( $primary ), array_keys( array_filter( $sources, array( $this, 'source_is_enabled_callback' ) ) ) ) ) ); } $enabled = array(); foreach ( $sources as $key => $source ) { if ( $this->source_is_enabled( $key, $source ) ) { $enabled[] = $key; } } return $enabled; } /** * Callback wrapper for source filtering. * * @param array $source Source data. * @return bool */ private function source_is_enabled_callback( $source ) { $key = ''; foreach ( $this->get_sources() as $source_key => $source_data ) { if ( isset( $source_data['option'], $source['option'] ) && $source_data['option'] === $source['option'] ) { $key = $source_key; break; } } return '' !== $key && $this->source_is_enabled( $key, $source ); } /** * Check source enabled. * * @param string $key Source key. * @param array $source Source data. * @return bool */ public function source_is_enabled( $key, array $source ) { unset( $key ); $option = isset( $source['option'] ) ? (string) $source['option'] : ''; if ( '' === $option || ! $this->settings->get( $option, 1 ) ) { return false; } return ! empty( $source['detected'] ); } /** * Return a compact detection summary. * * @return array */ public function summarize_integrations() { $sources = $this->get_sources(); $detected = array(); $enabled = array(); foreach ( $sources as $key => $source ) { if ( ! empty( $source['detected'] ) ) { $detected[] = $key; } if ( $this->source_is_enabled( $key, $source ) ) { $enabled[] = $key; } } return array( 'enabled' => (bool) $this->settings->get( 'integrations_enabled', 1 ), 'primary_source' => sanitize_key( (string) $this->settings->get( 'integration_primary_source', 'auto' ) ), 'detected_count' => count( $detected ), 'enabled_count' => count( $enabled ), 'detected_sources' => $detected, 'enabled_sources' => $enabled, 'sources' => $sources, 'use_seo_llms' => (bool) $this->settings->get( 'integration_use_seo_descriptions_in_llms', 1 ), 'expose_seo_tools' => (bool) $this->settings->get( 'integration_expose_seo_metadata_in_tools', 1 ), 'add_notes_to_llms' => (bool) $this->settings->get( 'integration_add_seo_notes_to_llms', 1 ), ); } /** * Scan public content for metadata coverage. * * @param int $limit Limit. * @return array */ public function scan( $limit = 25 ) { $options = $this->settings->get_options(); $summary = $this->summarize_integrations(); $result = array( 'enabled' => ! empty( $summary['enabled'] ), 'sources' => $summary['sources'], 'detected_sources' => $summary['detected_sources'], 'enabled_sources' => $summary['enabled_sources'], 'items_scanned' => 0, 'items_with_seo_title' => 0, 'items_with_description' => 0, 'items_with_canonical' => 0, 'items_with_focus_keyword' => 0, 'items_with_og_image' => 0, 'items_noindex' => 0, 'source_breakdown' => array(), 'issues' => array(), 'issue_count' => 0, 'critical_count' => 0, 'warning_count' => 0, 'score' => 0, 'preview' => array(), ); if ( empty( $summary['enabled'] ) ) { $result['issues'][] = $this->issue( __( 'SEO integrations disabled', 'agizoai-agentic-browsing-optimizer' ), '', __( 'Metadata extraction is disabled.', 'agizoai-agentic-browsing-optimizer' ), __( 'Enable SEO metadata integrations to improve agent summaries.', 'agizoai-agentic-browsing-optimizer' ), 'critical' ); $result['issue_count'] = 1; $result['critical_count'] = 1; return $result; } $post_types = ! empty( $options['include_post_types'] ) ? (array) $options['include_post_types'] : array( 'post', 'page' ); $posts = get_posts( array( 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => $this->bound_limit( $limit ), 'orderby' => 'modified', 'order' => 'DESC', 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_meta_cache' => true, 'update_post_term_cache' => false, 'suppress_filters' => false, ) ); foreach ( $posts as $post ) { if ( ! $post instanceof WP_Post || '' !== (string) $post->post_password ) { continue; } $metadata = $this->get_post_metadata( $post->ID ); $result['items_scanned']++; if ( ! empty( $metadata['has_seo_title'] ) ) { $result['items_with_seo_title']++; } if ( ! empty( $metadata['has_meta_description'] ) ) { $result['items_with_description']++; } else { $result['issues'][] = $this->issue( __( 'Missing meta description', 'agizoai-agentic-browsing-optimizer' ), get_permalink( $post ), __( 'This public item has no detected SEO meta description.', 'agizoai-agentic-browsing-optimizer' ), __( 'Add a concise SEO description in your SEO plugin or WordPress excerpt.', 'agizoai-agentic-browsing-optimizer' ), 'warning' ); } if ( ! empty( $metadata['has_canonical'] ) ) { $result['items_with_canonical']++; } if ( ! empty( $metadata['focus_keyphrase'] ) ) { $result['items_with_focus_keyword']++; } if ( ! empty( $metadata['og_image'] ) ) { $result['items_with_og_image']++; } if ( ! empty( $metadata['is_noindex'] ) ) { $result['items_noindex']++; } $source = isset( $metadata['source'] ) ? sanitize_key( $metadata['source'] ) : 'wordpress_core'; if ( ! isset( $result['source_breakdown'][ $source ] ) ) { $result['source_breakdown'][ $source ] = 0; } $result['source_breakdown'][ $source ]++; if ( count( $result['preview'] ) < 8 ) { $result['preview'][] = array( 'id' => absint( $post->ID ), 'title' => $metadata['title'], 'url' => get_permalink( $post ), 'source' => $source, 'description' => $metadata['description'], 'canonical' => $metadata['canonical'], 'robots' => $metadata['robots'], ); } } if ( 0 === absint( $result['items_scanned'] ) ) { $result['issues'][] = $this->issue( __( 'No public content scanned', 'agizoai-agentic-browsing-optimizer' ), '', __( 'No public posts, pages or selected content types were available for metadata scanning.', 'agizoai-agentic-browsing-optimizer' ), __( 'Publish content or adjust the selected public post types.', 'agizoai-agentic-browsing-optimizer' ), 'critical' ); } $issues = $result['issues']; $result['issue_count'] = count( $issues ); $result['critical_count'] = count( array_filter( $issues, static function ( $issue ) { return isset( $issue['severity'] ) && 'critical' === $issue['severity']; } ) ); $result['warning_count'] = max( 0, $result['issue_count'] - $result['critical_count'] ); $result['score'] = $this->calculate_score( $result ); return $result; } /** * Get metadata for one public content item. * * @param int $post_id Post ID. * @return array */ public function get_post_metadata( $post_id ) { $post_id = absint( $post_id ); $post = get_post( $post_id ); if ( ! $post instanceof WP_Post ) { return array(); } $sources = $this->get_enabled_source_keys(); if ( empty( $sources ) ) { $sources = array( 'wordpress_core' ); } $merged = array( 'source' => 'wordpress_core', 'title' => '', 'description' => '', 'canonical' => '', 'robots' => '', 'focus_keyphrase' => '', 'og_image' => '', 'has_seo_title' => false, 'has_meta_description' => false, 'has_canonical' => false, 'is_noindex' => false, ); foreach ( $sources as $source ) { $data = $this->get_source_metadata( $post_id, $source ); if ( empty( $data ) ) { continue; } foreach ( array( 'title', 'description', 'canonical', 'robots', 'focus_keyphrase', 'og_image' ) as $field ) { if ( '' === (string) $merged[ $field ] && ! empty( $data[ $field ] ) ) { $merged[ $field ] = $data[ $field ]; $merged['source'] = $source; } } if ( ! empty( $data['has_seo_title'] ) ) { $merged['has_seo_title'] = true; } if ( ! empty( $data['has_meta_description'] ) ) { $merged['has_meta_description'] = true; } if ( ! empty( $data['has_canonical'] ) ) { $merged['has_canonical'] = true; } if ( ! empty( $data['is_noindex'] ) ) { $merged['is_noindex'] = true; } } if ( '' === $merged['title'] ) { $merged['title'] = get_the_title( $post ); } if ( '' === $merged['description'] ) { $merged['description'] = $this->core_description( $post ); } if ( '' === $merged['canonical'] ) { $merged['canonical'] = get_permalink( $post ); } if ( '' === $merged['robots'] ) { $merged['robots'] = $merged['is_noindex'] ? 'noindex,follow' : 'index,follow'; } $merged['title'] = $this->clean_text( $merged['title'] ); $merged['description'] = $this->clean_text( $merged['description'] ); $merged['canonical'] = esc_url_raw( $merged['canonical'] ); $merged['robots'] = sanitize_text_field( $merged['robots'] ); $merged['og_image'] = esc_url_raw( $merged['og_image'] ); return (array) apply_filters( 'agizoai_agentic_post_metadata', $merged, $post_id ); } /** * Noindex filter. * * @param bool $is_noindex Existing value. * @param int $post_id Post ID. * @return bool */ public function filter_is_noindex( $is_noindex, $post_id ) { return (bool) $is_noindex || $this->metadata_is_noindex( absint( $post_id ) ); } /** * Source-specific metadata. * * @param int $post_id Post ID. * @param string $source Source key. * @return array */ private function get_source_metadata( $post_id, $source ) { $post = get_post( $post_id ); if ( ! $post instanceof WP_Post ) { return array(); } $data = array( 'title' => '', 'description' => '', 'canonical' => '', 'robots' => '', 'focus_keyphrase' => '', 'og_image' => '', 'has_seo_title' => false, 'has_meta_description' => false, 'has_canonical' => false, 'is_noindex' => false, ); switch ( $source ) { case 'real_seo_master': $data['title'] = $this->first_meta( $post_id, array( '_rsm_seo_title', '_rsm_meta_title', '_rsm_title', 'rsm_seo_title', 'rsm_title', '_real_seo_master_title', '_real_seo_title' ) ); $data['description'] = $this->first_meta( $post_id, array( '_rsm_meta_description', '_rsm_description', '_rsm_seo_description', 'rsm_meta_description', 'rsm_description', '_real_seo_master_description', '_real_seo_description' ) ); $data['canonical'] = $this->first_meta( $post_id, array( '_rsm_canonical', '_rsm_canonical_url', 'rsm_canonical', '_real_seo_master_canonical' ) ); $data['focus_keyphrase'] = $this->first_meta( $post_id, array( '_rsm_focus_keyphrase', '_rsm_focus_keyword', 'rsm_focus_keyphrase', '_real_seo_master_focus_keyphrase' ) ); $data['og_image'] = $this->first_meta( $post_id, array( '_rsm_og_image', '_rsm_social_image', 'rsm_og_image', '_real_seo_master_og_image' ) ); break; case 'yoast': $data['title'] = $this->first_meta( $post_id, array( '_yoast_wpseo_title' ) ); $data['description'] = $this->first_meta( $post_id, array( '_yoast_wpseo_metadesc' ) ); $data['canonical'] = $this->first_meta( $post_id, array( '_yoast_wpseo_canonical' ) ); $data['focus_keyphrase'] = $this->first_meta( $post_id, array( '_yoast_wpseo_focuskw' ) ); $data['og_image'] = $this->first_meta( $post_id, array( '_yoast_wpseo_opengraph-image' ) ); break; case 'rank_math': $data['title'] = $this->first_meta( $post_id, array( 'rank_math_title' ) ); $data['description'] = $this->first_meta( $post_id, array( 'rank_math_description' ) ); $data['canonical'] = $this->first_meta( $post_id, array( 'rank_math_canonical_url', 'rank_math_canonical' ) ); $data['focus_keyphrase'] = $this->first_meta( $post_id, array( 'rank_math_focus_keyword' ) ); $data['og_image'] = $this->first_meta( $post_id, array( 'rank_math_facebook_image', 'rank_math_twitter_image' ) ); break; case 'aioseo': $data['title'] = $this->first_meta( $post_id, array( '_aioseo_title', '_aioseo_og_title' ) ); $data['description'] = $this->first_meta( $post_id, array( '_aioseo_description', '_aioseo_og_description' ) ); $data['canonical'] = $this->first_meta( $post_id, array( '_aioseo_canonical_url', '_aioseo_custom_link' ) ); $data['focus_keyphrase'] = $this->first_meta( $post_id, array( '_aioseo_focus_keyphrase', '_aioseo_keywords' ) ); $data['og_image'] = $this->first_meta( $post_id, array( '_aioseo_og_image_custom_url', '_aioseo_twitter_image_custom_url' ) ); break; case 'seopress': $data['title'] = $this->first_meta( $post_id, array( '_seopress_titles_title' ) ); $data['description'] = $this->first_meta( $post_id, array( '_seopress_titles_desc' ) ); $data['canonical'] = $this->first_meta( $post_id, array( '_seopress_robots_canonical' ) ); $data['focus_keyphrase'] = $this->first_meta( $post_id, array( '_seopress_analysis_target_kw' ) ); $data['og_image'] = $this->first_meta( $post_id, array( '_seopress_social_fb_img', '_seopress_social_twitter_img' ) ); break; case 'wordpress_core': default: $data['title'] = get_the_title( $post ); $data['description'] = $this->core_description( $post ); $data['canonical'] = get_permalink( $post ); $data['og_image'] = get_the_post_thumbnail_url( $post, 'full' ); break; } $data['title'] = $this->replace_vars( $data['title'], $post ); $data['description'] = $this->replace_vars( $data['description'], $post ); $data['canonical'] = $this->replace_vars( $data['canonical'], $post ); $data['has_seo_title'] = '' !== trim( (string) $data['title'] ) && 'wordpress_core' !== $source; $data['has_meta_description'] = '' !== trim( (string) $data['description'] ) && ( 'wordpress_core' !== $source || has_excerpt( $post ) ); $data['has_canonical'] = '' !== trim( (string) $data['canonical'] ) && 'wordpress_core' !== $source; $data['is_noindex'] = $this->metadata_is_noindex( $post_id ); $data['robots'] = $data['is_noindex'] ? 'noindex,follow' : 'index,follow'; return $data; } /** * Read first non-empty post meta value. * * @param int $post_id Post ID. * @param array $keys Meta keys. * @return string */ private function first_meta( $post_id, array $keys ) { foreach ( $keys as $key ) { $value = get_post_meta( $post_id, $key, true ); if ( is_array( $value ) ) { $value = implode( ', ', array_filter( array_map( 'sanitize_text_field', $value ) ) ); } $value = trim( (string) $value ); if ( '' !== $value ) { return $value; } } return ''; } /** * Detect noindex metadata. * * @param int $post_id Post ID. * @return bool */ private function metadata_is_noindex( $post_id ) { $yoast = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true ); if ( '1' === (string) $yoast || 'yes' === strtolower( (string) $yoast ) ) { return true; } $rank_math = get_post_meta( $post_id, 'rank_math_robots', true ); if ( is_array( $rank_math ) && in_array( 'noindex', $rank_math, true ) ) { return true; } if ( is_string( $rank_math ) && false !== strpos( strtolower( $rank_math ), 'noindex' ) ) { return true; } foreach ( array( '_aioseo_robots_noindex', '_rsm_robots_noindex', '_real_seo_master_noindex' ) as $key ) { $value = get_post_meta( $post_id, $key, true ); if ( '1' === (string) $value || 'yes' === strtolower( (string) $value ) || 'on' === strtolower( (string) $value ) ) { return true; } } $seopress = get_post_meta( $post_id, '_seopress_robots_index', true ); if ( 'yes' === strtolower( (string) $seopress ) || '1' === (string) $seopress ) { return true; } return false; } /** * Replace common SEO plugin variables. * * @param string $value Raw value. * @param WP_Post $post Post. * @return string */ private function replace_vars( $value, WP_Post $post ) { $value = (string) $value; if ( '' === trim( $value ) ) { return ''; } $replacements = array( '%%title%%' => get_the_title( $post ), '%title%' => get_the_title( $post ), '%%sitename%%' => get_bloginfo( 'name' ), '%sitename%' => get_bloginfo( 'name' ), '%%sep%%' => '-', '%sep%' => '-', '%%excerpt%%' => $this->core_description( $post ), '%excerpt%' => $this->core_description( $post ), '%%post_date%%' => get_the_date( '', $post ), '%date%' => get_the_date( '', $post ), ); return strtr( $value, $replacements ); } /** * Core summary fallback. * * @param WP_Post $post Post. * @return string */ private function core_description( WP_Post $post ) { $excerpt = has_excerpt( $post ) ? get_the_excerpt( $post ) : ''; if ( '' === trim( $excerpt ) ) { $excerpt = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post->post_content ), true ), 32, '…' ); } return $this->clean_text( $excerpt ); } /** * Clean short text. * * @param string $value Value. * @return string */ private function clean_text( $value ) { $value = wp_strip_all_tags( (string) $value, true ); $value = html_entity_decode( $value, ENT_QUOTES, get_bloginfo( 'charset' ) ); $value = preg_replace( '/\s+/', ' ', $value ); return trim( (string) $value ); } /** * Calculate coverage score. * * @param array $result Scan result. * @return int */ private function calculate_score( array $result ) { if ( empty( $result['enabled'] ) ) { return 0; } $items = max( 1, absint( $result['items_scanned'] ) ); $score = 0; $score += ! empty( $result['enabled_sources'] ) ? 20 : 0; $score += in_array( 'wordpress_core', (array) $result['enabled_sources'], true ) ? 10 : 0; $score += (int) round( ( absint( $result['items_with_description'] ) / $items ) * 25 ); $score += (int) round( ( absint( $result['items_with_canonical'] ) / $items ) * 15 ); $score += (int) round( ( absint( $result['items_with_og_image'] ) / $items ) * 10 ); $score += (int) round( ( absint( $result['items_with_focus_keyword'] ) / $items ) * 5 ); $score += ! empty( $result['items_scanned'] ) ? 15 : 0; return min( 100, max( 0, $score ) ); } /** * Return issue row. * * @param string $title Title. * @param string $url URL. * @param string $problem Problem. * @param string $fix Fix. * @param string $severity Severity. * @return array */ private function issue( $title, $url, $problem, $fix, $severity ) { return array( 'title' => $title, 'url' => esc_url_raw( $url ), 'problem' => $problem, 'fix' => $fix, 'severity' => sanitize_key( $severity ), ); } /** * Bound scan limit. * * @param mixed $limit Limit. * @return int */ private function bound_limit( $limit ) { $limit = absint( $limit ); return max( 1, min( 50, $limit ) ); } /** * Detect Real SEO Master-like plugins. * * @return bool */ private function detect_real_seo_master() { return defined( 'REAL_SEO_MASTER_VERSION' ) || defined( 'RSM_VERSION' ) || class_exists( 'Real_SEO_Master' ) || class_exists( 'RealSEOMaster' ) || function_exists( 'rsm_get_meta' ) || $this->active_plugin_like( array( 'real-seo-master', 'real_seo_master' ) ); } /** * Detect Yoast. * * @return bool */ private function detect_yoast() { return defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Frontend' ) || $this->active_plugin_like( array( 'wordpress-seo', 'wp-seo.php' ) ); } /** * Detect Rank Math. * * @return bool */ private function detect_rank_math() { return defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath' ) || $this->active_plugin_like( array( 'seo-by-rank-math', 'rank-math' ) ); } /** * Detect AIOSEO. * * @return bool */ private function detect_aioseo() { return defined( 'AIOSEO_VERSION' ) || function_exists( 'aioseo' ) || class_exists( 'AIOSEO\\Plugin\\AIOSEO' ) || $this->active_plugin_like( array( 'all-in-one-seo-pack', 'aioseo' ) ); } /** * Detect SEOPress. * * @return bool */ private function detect_seopress() { return defined( 'SEOPRESS_VERSION' ) || function_exists( 'seopress_get_service' ) || $this->active_plugin_like( array( 'wp-seopress', 'seopress' ) ); } /** * Active plugin helper. * * @param array $needles Plugin path fragments. * @return bool */ private function active_plugin_like( array $needles ) { $active = (array) get_option( 'active_plugins', array() ); $network = (array) get_site_option( 'active_sitewide_plugins', array() ); $plugins = array_merge( $active, array_keys( $network ) ); foreach ( $plugins as $plugin ) { $plugin = strtolower( (string) $plugin ); foreach ( $needles as $needle ) { if ( false !== strpos( $plugin, strtolower( $needle ) ) ) { return true; } } } return false; } }