*/ public static function detect(): array { $found = array(); // ── Yoast SEO / Yoast SEO Premium ──────────────────────────────── if ( defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Frontend' ) ) { $conflicts = self::detect_yoast_conflicts(); if ( ! empty( $conflicts ) ) { $found['yoast'] = array( 'name' => 'Yoast SEO', 'conflicts' => $conflicts, 'doc_url' => 'https://yoast.com/features/schema/', ); } } // ── Rank Math ───────────────────────────────────────────────────── if ( defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath' ) ) { $conflicts = self::detect_rankmath_conflicts(); if ( ! empty( $conflicts ) ) { $found['rankmath'] = array( 'name' => 'Rank Math', 'conflicts' => $conflicts, 'doc_url' => 'https://rankmath.com/kb/schema-markup/', ); } } // ── All in One SEO ──────────────────────────────────────────────── if ( defined( 'AIOSEO_VERSION' ) || class_exists( 'AIOSEO\Plugin\AIOSEO' ) ) { $found['aioseo'] = array( 'name' => 'All in One SEO', 'conflicts' => array( 'article', 'breadcrumb', 'organization', 'author' ), 'doc_url' => 'https://aioseo.com/docs/schema-markup/', ); } // ── SEOPress ────────────────────────────────────────────────────── if ( defined( 'SEOPRESS_VERSION' ) || function_exists( 'seopress_activation' ) ) { $found['seopress'] = array( 'name' => 'SEOPress', 'conflicts' => array( 'article', 'breadcrumb', 'organization' ), 'doc_url' => 'https://www.seopress.org/support/guides/schema-markup/', ); } // ── The SEO Framework ───────────────────────────────────────────── if ( function_exists( 'the_seo_framework' ) ) { $found['tsf'] = array( 'name' => 'The SEO Framework', 'conflicts' => array( 'article', 'breadcrumb', 'organization', 'author' ), 'doc_url' => 'https://theseoframework.com/?p=2582', ); } /** * Filter the detected SEO plugin conflicts. * * Allows sites with unusual setups to drop or adjust entries when * the detector's heuristics don't match their reality (e.g., a * custom filter that suppresses Yoast's schema output). * * Return an empty array (or unset a plugin key) to suppress its * conflict entirely. * * @param array $found */ $found = (array) apply_filters( 'aeomatic_seo_conflicts_detected', $found ); // Drop plugin entries whose 'conflicts' list was emptied by the filter. foreach ( $found as $slug => $entry ) { if ( empty( $entry['conflicts'] ) || ! is_array( $entry['conflicts'] ) ) { unset( $found[ $slug ] ); } } return $found; } /** * Build the Yoast conflict list based on its actual settings. * * Yoast has no global schema off switch, but per-CPT you can set the * page-type / article-type to "None", and breadcrumbs have their own * toggle. We treat each schema key independently: * • breadcrumb — only if breadcrumbs-enable is truthy * • article — only if any schema-article-type-* is set to a real type * • organization — only if company_or_person resolves to company/person * AND at least one schema-page-type-* is a real type * • author — same trigger as article * * @return string[] */ protected static function detect_yoast_conflicts(): array { // Yoast's master schema toggle (introduced in the Schema Framework // feature). When explicitly false, Yoast emits no JSON-LD regardless // of the per-CPT schema-*-type-* settings below. $main = (array) get_option( 'wpseo', array() ); if ( array_key_exists( 'enable_schema', $main ) && false === (bool) $main['enable_schema'] ) { return array(); } $titles = (array) get_option( 'wpseo_titles', array() ); $conflicts = array(); $has_real_type = static function ( array $opts, string $prefix ): bool { foreach ( $opts as $key => $val ) { if ( 0 !== strpos( (string) $key, $prefix ) ) { continue; } $val = is_string( $val ) ? trim( $val ) : ''; if ( '' !== $val && 'None' !== $val && 'none' !== $val ) { return true; } } return false; }; $article_on = $has_real_type( $titles, 'schema-article-type-' ); $page_on = $has_real_type( $titles, 'schema-page-type-' ); if ( $article_on ) { $conflicts[] = 'article'; $conflicts[] = 'author'; } if ( $page_on ) { $org_type = isset( $titles['company_or_person'] ) ? (string) $titles['company_or_person'] : ''; if ( 'company' === $org_type || 'person' === $org_type ) { $conflicts[] = 'organization'; } } if ( ! empty( $titles['breadcrumbs-enable'] ) ) { $conflicts[] = 'breadcrumb'; } return array_values( array_unique( $conflicts ) ); } /** * Build the Rank Math conflict list based on which modules are active. * * Rank Math ships a "Schema (Structured Data)" module with ID * `rich-snippet` that users can toggle from Dashboard → Modules. When * it's off Rank Math emits no schema, so we drop the entire conflict * list for that plugin. * * @return string[] */ protected static function detect_rankmath_conflicts(): array { $modules = (array) get_option( 'rank_math_modules', array() ); $schema_on = in_array( 'rich-snippet', $modules, true ); if ( ! $schema_on ) { return array(); } return array( 'article', 'breadcrumb', 'organization', 'author' ); } /** * Return a flat list of schema setting keys that are conflicted by * any currently-active SEO plugin. * * @return string[] e.g. ['article', 'breadcrumb', 'organization'] */ public static function conflicted_keys(): array { $keys = array(); foreach ( self::detect() as $plugin ) { $keys = array_merge( $keys, $plugin['conflicts'] ); } return array_values( array_unique( $keys ) ); } /** * Whether AEOmatic currently has any conflicting schema types enabled. */ public static function has_active_conflicts(): bool { $settings = AEOMATIC_Settings::get( 'schema' ); $conflicts = self::conflicted_keys(); foreach ( $conflicts as $key ) { if ( ! empty( $settings[ $key ] ) ) { return true; } } return false; } /** * Show the conflict notice in the admin — only on AEOmatic pages, * and only if not already dismissed. */ public function maybe_show_notice(): void { // Only on our admin pages. Reading $_GET['page'] for routing display only — no form processing, no nonce needed. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $aeomatic_current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( false === strpos( $aeomatic_current_page, AEOMATIC_Admin::MENU_SLUG ) ) { return; } if ( get_option( self::NOTICE_DISMISSED_KEY ) ) { return; } $plugins = self::detect(); if ( empty( $plugins ) || ! self::has_active_conflicts() ) { return; } $names = implode( ' and ', array_column( $plugins, 'name' ) ); $fix_url = wp_nonce_url( admin_url( 'admin-post.php?action=aeomatic_fix_conflicts' ), 'aeomatic_fix_conflicts' ); $dismiss_url = wp_nonce_url( admin_url( 'admin-post.php?action=aeomatic_fix_conflicts&dismiss=1' ), 'aeomatic_fix_conflicts' ); $conflicted = self::conflicted_keys(); $labels = array( 'article' => 'Article', 'breadcrumb' => 'Breadcrumb', 'organization' => 'Organization', 'author' => 'Author', ); $conflict_labels = array_filter( array_map( static fn( $k ) => $labels[ $k ] ?? null, $conflicted ) ); ?>

⚠️

' . esc_html( $names ) . '', '' . esc_html( implode( ', ', $conflict_labels ) ) . '' ); ?>

✓ ' . esc_html__( 'No conflicting SEO plugins detected.', 'aeomatic' ) . ''; } $names = array_column( $plugins, 'name' ); if ( self::has_active_conflicts() ) { $fix_url = wp_nonce_url( admin_url( 'admin-post.php?action=aeomatic_fix_conflicts' ), 'aeomatic_fix_conflicts' ); // translators: %s: comma-separated list of detected SEO plugin names. $aeomatic_conflict_msg = esc_html__( '%s detected — some schema types overlap.', 'aeomatic' ); return sprintf( '⚠ ' . $aeomatic_conflict_msg . ' %s', esc_html( implode( ', ', $names ) ), esc_url( $fix_url ), esc_html__( 'Fix conflicts', 'aeomatic' ) ); } // translators: %s: comma-separated list of detected SEO plugin names. $aeomatic_resolved_msg = esc_html__( '%s detected — conflicts already resolved.', 'aeomatic' ); return sprintf( '✓ ' . $aeomatic_resolved_msg . '', esc_html( implode( ', ', $names ) ) ); } }