> */ private const KNOWN_PLUGINS = array( 'yoast' => array( 'detection_class' => 'WPSEO_Schema', 'detection_filter' => 'wpseo_schema_graph', 'label' => 'Yoast SEO', ), 'rankmath' => array( 'detection_class' => 'RankMath\\JsonLD', 'detection_filter' => 'rank_math/json_ld', 'label' => 'Rank Math', ), 'aioseo' => array( 'detection_class' => 'AIOSEO\\Plugin\\Schema\\Schema', 'detection_filter' => 'aioseo_schema_output', 'label' => 'All in One SEO', ), ); /** * Detected active SEO plugins. * * @var array Plugin key => label. */ private array $detected = array(); /** * Detect which SEO plugins are active. * * Should be called early (e.g., on plugins_loaded or init) before * the suppression hooks are registered. * * @return array Detected plugin key => label pairs. */ public function detect(): array { $this->detected = array(); foreach ( self::KNOWN_PLUGINS as $key => $config ) { if ( class_exists( $config['detection_class'] ) ) { $this->detected[ $key ] = $config['label']; } } /** * Filters the detected SEO plugins. * * Third-party integrations can add their own detected plugins. * * @param array $detected Plugin key => label pairs. */ $this->detected = apply_filters( 'aicommerce_detected_seo_plugins', $this->detected ); return $this->detected; } /** * Suppress schema output from detected SEO plugins. * * Only suppresses Product-related schema — leaves general website/organization * schema from SEO plugins intact when possible. * * @return void */ public function suppress(): void { $should_suppress = get_option( 'aicommerce_conflict_suppress', true ); if ( ! $should_suppress ) { return; } if ( empty( $this->detected ) ) { $this->detect(); } foreach ( $this->detected as $key => $label ) { $method = "suppress_{$key}"; if ( method_exists( $this, $method ) ) { $this->{$method}(); } } } /** * Suppress Yoast SEO Product schema. * * Removes the Product piece from Yoast's schema graph while preserving * WebSite, Organization, and other pieces. * * @return void */ private function suppress_yoast(): void { add_filter( 'wpseo_schema_graph_pieces', static function ( array $pieces ): array { if ( ! is_product() ) { return $pieces; } return array_filter( $pieces, static function ( $piece ): bool { $class_name = get_class( $piece ); // Remove Yoast's Product and Offer schema pieces. return ! str_contains( $class_name, 'Product' ) && ! str_contains( $class_name, 'Offer' ); } ); }, 100 ); } /** * Suppress Rank Math Product schema. * * @return void */ private function suppress_rankmath(): void { add_filter( 'rank_math/json_ld', static function ( array $data ): array { if ( ! is_product() ) { return $data; } // Remove Product-type entries from RankMath's JSON-LD. foreach ( $data as $key => $entry ) { if ( isset( $entry['@type'] ) && ( 'Product' === $entry['@type'] || ( is_array( $entry['@type'] ) && in_array( 'Product', $entry['@type'], true ) ) ) ) { unset( $data[ $key ] ); } } return $data; }, 100 ); } /** * Suppress All in One SEO Product schema. * * @return void */ private function suppress_aioseo(): void { add_filter( 'aioseo_schema_output', static function ( array $graphs ): array { if ( ! is_product() ) { return $graphs; } return array_filter( $graphs, static function ( array $graph ): bool { return ! isset( $graph['@type'] ) || 'Product' !== $graph['@type']; } ); }, 100 ); } /** * Get list of detected SEO plugins. * * @return array Plugin key => label pairs. */ public function get_detected(): array { if ( empty( $this->detected ) ) { $this->detect(); } return $this->detected; } /** * Check if any conflicting SEO plugins are detected. * * @return bool */ public function has_conflicts(): bool { if ( empty( $this->detected ) ) { $this->detect(); } return ! empty( $this->detected ); } }