prefix . 'aiforya_sm_gsc_errors'; $errors = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE status = 'open' LIMIT 50", $table ) ); if ( empty( $errors ) ) return; foreach ( $errors as $err ) { $this->apply_auto_fix_for_error( $err ); } } private function apply_auto_fix_for_error( $err ): void { global $wpdb; $table = $wpdb->prefix . 'aiforya_sm_gsc_errors'; $post_id = (int) $err->post_id; if ( $post_id <= 0 ) return; // Stratégie de fix par type d'erreur Schema GSC : // - "Missing field X" -> re-run the Generator with AI context to add X // - "Invalid value" → strip le champ invalide, re-run // - "Duplicate" → keep first only $err_msg = strtolower( (string) ( $err->error_message ?? '' ) ); $fixed = false; if ( strpos( $err_msg, 'missing' ) !== false || strpos( $err_msg, 'required' ) !== false || strpos( $err_msg, 'invalid' ) !== false ) { // Force régénération via Generator (qui passera par maybe_enrich_with_ai). if ( class_exists( 'Aiforya_Schema_Markup_Generator' ) ) { $gen = Aiforya_Schema_Markup_Generator::get_instance(); $db = Aiforya_Schema_Markup_Db::get_instance(); $sch = $db->get_schema_by_post_id( $post_id ); if ( $sch && ! empty( $sch->schema_type ) ) { $json = $gen->generate_json_ld( $post_id, (string) $sch->schema_type ); if ( ! is_wp_error( $json ) ) { $db->save_or_update_schema( array( 'post_id' => $post_id, 'schema_type' => (string) $sch->schema_type, 'schema_json' => $json, ) ); $gen->clear_cache_for_post( $post_id ); $fixed = true; } } } } if ( $fixed ) { $wpdb->update( $table, array( 'status' => 'fixed', 'fixed_at' => current_time( 'mysql' ), ), array( 'id' => (int) $err->id ) ); } } /** * Manual import : permet à l'utilisateur d'ajouter une erreur GSC à la main (en attendant l'OAuth). */ public function record_gsc_error( int $post_id, string $error_type, string $error_message, string $severity = 'warning' ): int { global $wpdb; $table = $wpdb->prefix . 'aiforya_sm_gsc_errors'; $wpdb->insert( $table, array( 'post_id' => $post_id, 'error_type' => $error_type, 'error_message' => $error_message, 'severity' => $severity, 'status' => 'open', 'detected_at' => current_time( 'mysql' ), ) ); return (int) $wpdb->insert_id; } /** * Liste des erreurs GSC (open ou tous). */ public function list_gsc_errors( string $status = 'open', int $limit = 100 ): array { global $wpdb; $table = $wpdb->prefix . 'aiforya_sm_gsc_errors'; $limit = max( 1, min( 500, $limit ) ); $rows = 'all' === $status ? $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i ORDER BY detected_at DESC LIMIT %d", $table, $limit ) ) : $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE status = %s ORDER BY detected_at DESC LIMIT %d", $table, $status, $limit ) ); return is_array( $rows ) ? $rows : array(); } /* ======================================================== * F16 — One-Click Schema Optimizer 2026 * ======================================================== */ /** * Runs the AI again over EVERY schema, with a prompt enriched by the 2026 Google specs. * Returns stats { ok, fail, modified } plus the first diffs for the admin preview. */ public function one_click_optimize( int $batch_size = 25 ): array { global $wpdb; $table = $wpdb->prefix . 'aiforya_sm_schemas'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE status = 'active' ORDER BY updated_at ASC LIMIT %d", $table, $batch_size ) ); if ( empty( $rows ) ) return array( 'ok' => 0, 'fail' => 0, 'remaining' => 0 ); $generator = Aiforya_Schema_Markup_Generator::get_instance(); $stats = array( 'ok' => 0, 'fail' => 0, 'errors' => array() ); foreach ( $rows as $r ) { $pid = (int) $r->post_id; $type = (string) $r->schema_type; $json = $generator->generate_json_ld( $pid, $type ); if ( is_wp_error( $json ) ) { $stats['fail']++; if ( count( $stats['errors'] ) < 5 ) $stats['errors'][] = '#' . $pid . ' ' . $json->get_error_message(); continue; } // Optionnel : ajout speakable selectors (F18 Voice Search) si Article/BlogPosting. $json = $this->maybe_add_speakable( $json, $type ); Aiforya_Schema_Markup_Db::get_instance()->save_or_update_schema( array( 'post_id' => $pid, 'schema_type' => $type, 'schema_json' => $json, ) ); $generator->clear_cache_for_post( $pid ); $stats['ok']++; } $remaining = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status = 'active'", $table ) ) - $stats['ok']; $stats['remaining'] = max( 0, $remaining ); return $stats; } /* ======================================================== * F18 — Voice Search Optimizer * ======================================================== */ /** * Ajoute speakable selectors Google Assistant/Alexa/Siri sur les schemas Article-like. * Cible : .entry-title (titre), .entry-content > p:first-of-type (premier paragraphe lisible). */ public function maybe_add_speakable( string $json, string $type ): string { $voice_compatible = array( 'Article', 'BlogPosting', 'NewsArticle', 'TechArticle', 'WebPage', 'AboutPage' ); if ( ! in_array( $type, $voice_compatible, true ) ) return $json; $data = json_decode( $json, true ); if ( ! is_array( $data ) ) return $json; if ( isset( $data['speakable'] ) ) return $json; // déjà présent $data['speakable'] = array( '@type' => 'SpeakableSpecification', 'cssSelector' => array( '.entry-title', '.entry-content > p:first-of-type', 'h1', 'article p:first-of-type' ), ); return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } /* ======================================================== * Schema Deduplication * ======================================================== */ /** * Détecte les autres plugins qui injectent leur propre Schema dans . * Retourne la liste des conflits potentiels avec recommandations. */ public function detect_dedup_conflicts(): array { $conflicts = array(); if ( defined( 'WPSEO_VERSION' ) ) { // Yoast injecte automatiquement. On suggère de désactiver son output via filtre. $disabled = (bool) get_option( 'aiforya_sm_dedup_yoast_disabled', false ); $conflicts[] = array( 'plugin' => 'yoast', 'name' => 'Yoast SEO', 'severity' => 'high', 'issue' => __( 'Yoast injects its own @graph into on every page — duplication guaranteed.', 'aiforya-schema-markup' ), 'fix' => __( 'Disable Yoast Schema output via the wpseo_json_ld_output filter. AIFORYA takes over.', 'aiforya-schema-markup' ), 'disabled' => $disabled, ); } if ( defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath' ) ) { $disabled = (bool) get_option( 'aiforya_sm_dedup_rankmath_disabled', false ); $conflicts[] = array( 'plugin' => 'rankmath', 'name' => 'Rank Math', 'severity' => 'high', 'issue' => __( 'Rank Math automatically injects Schema on every page — duplication guaranteed.', 'aiforya-schema-markup' ), 'fix' => __( 'Disable the Rank Math Schema module via Dashboard → Modules → Schema. AIFORYA takes over.', 'aiforya-schema-markup' ), 'disabled' => $disabled, ); } if ( defined( 'AIOSEO_VERSION' ) ) { $disabled = (bool) get_option( 'aiforya_sm_dedup_aioseo_disabled', false ); $conflicts[] = array( 'plugin' => 'aioseo', 'name' => 'All in One SEO', 'severity' => 'high', 'issue' => __( 'AIOSEO injects its own structured data — potential duplication.', 'aiforya-schema-markup' ), 'fix' => __( 'Disable Schema in AIOSEO → Search Appearance → Schema Markup.', 'aiforya-schema-markup' ), 'disabled' => $disabled, ); } if ( defined( 'WP_SCHEMA_PRO_VER' ) ) { $disabled = (bool) get_option( 'aiforya_sm_dedup_schema_pro_disabled', false ); $conflicts[] = array( 'plugin' => 'schema_pro', 'name' => 'Schema Pro', 'severity' => 'medium', 'issue' => __( 'Schema Pro injects its global schemas — possible overlap.', 'aiforya-schema-markup' ), 'fix' => __( 'Disable Schema Pro global schemas after import via Migration.', 'aiforya-schema-markup' ), 'disabled' => $disabled, ); } return $conflicts; } /** * Désactive l'output Schema d'un plugin concurrent via nos filtres natifs (Yoast/RankMath). */ public function disable_competitor_output( string $plugin ): bool { $key = 'aiforya_sm_dedup_' . sanitize_key( $plugin ) . '_disabled'; return update_option( $key, true ); } } /** * Filtres anti-duplication appliqués automatiquement si l'utilisateur a activé le toggle. */ add_action( 'plugins_loaded', static function (): void { if ( get_option( 'aiforya_sm_dedup_yoast_disabled', false ) ) { add_filter( 'wpseo_json_ld_output', '__return_empty_array', 99 ); add_filter( 'wpseo_schema_graph', '__return_empty_array', 99 ); } if ( get_option( 'aiforya_sm_dedup_rankmath_disabled', false ) ) { add_filter( 'rank_math/json_ld', '__return_empty_array', 99 ); } if ( get_option( 'aiforya_sm_dedup_aioseo_disabled', false ) ) { add_filter( 'aioseo_schema_disable', '__return_true', 99 ); } }, 20 );