mapping * type de contenu -> heuristique par mots-clés. Aucun appel réseau. * * @package aiforya-schema-markup */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'Aiforya_Schema_Markup_Detector', false ) ) { return; } class Aiforya_Schema_Markup_Detector { private static ?self $instance = null; public static function get_instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } public function __construct() {} /** * Détermine le type de schéma d'un contenu : règles -> mapping -> heuristique. * * @param int $post_id * @return string|WP_Error Type Schema.org (ex: 'Article') ou erreur. */ public function detect_schema_type_for_post( int $post_id = 0 ) { $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'invalid_post', __( 'Invalid content, or content not found.', 'aiforya-schema-markup' ) ); } // 1. Règles avancées (priorité) : la première règle qui match impose le type. if ( class_exists( 'Aiforya_Schema_Markup_Rules' ) ) { $rule_type = Aiforya_Schema_Markup_Rules::get_instance()->resolve_for_post( $post_id ); if ( ! empty( $rule_type ) ) { $this->log_detection_safe( $post_id, (string) $rule_type, 1.0, 'rule' ); return (string) $rule_type; } } // 2. Mapping type de contenu -> type de schéma (configuré dans l'onglet Types). $type_mappings = get_option( 'aiforya_sm_type_mappings', array() ); $mapping_for_pt = ( is_array( $type_mappings ) && ! empty( $type_mappings[ $post->post_type ] ) ) ? (string) $type_mappings[ $post->post_type ] : ''; if ( $mapping_for_pt !== '' ) { $this->log_detection_safe( $post_id, $mapping_for_pt, 1.0, 'mapping' ); return $mapping_for_pt; } // 3. Heuristique par mots-clés (sans IA). $content = wp_strip_all_tags( (string) $post->post_content ); $title = wp_strip_all_tags( (string) $post->post_title ); $type = $this->fallback_detection( $content, $title ); $this->log_detection_safe( $post_id, $type, 0.6, 'heuristic' ); return $type; } /** * Détermine un type de schéma depuis un contenu brut (heuristique par mots-clés). * * @return string */ public function detect_schema_type_for_content( string $content = '', string $title = '' ): string { return $this->fallback_detection( $content, $title ); } private function log_detection_safe( int $post_id, string $type, float $confidence, string $method ): void { if ( ! class_exists( 'Aiforya_Schema_Markup_Db' ) ) { return; } $db = Aiforya_Schema_Markup_Db::get_instance(); if ( ! method_exists( $db, 'log_detection' ) ) { return; } $db->log_detection( array( 'post_id' => $post_id, 'detected_type' => $type, 'confidence' => $confidence, 'method' => $method, ) ); } /** * Détection heuristique par mots-clés (titre + contenu). 100% locale. */ public function fallback_detection( string $content = '', string $title = '' ): string { $text = strtolower( $title . ' ' . $content ); if ( preg_match( '/\b(recette|recipe|ingredient|cooking time|prep time)\b/u', $text ) ) return 'Recipe'; if ( preg_match( '/\b(comment|how to|step by step|tutorial|étape|guide)\b/u', $text ) ) return 'HowTo'; if ( preg_match( '/\b(faq|question|réponse|frequently asked)\b/u', $text ) ) return 'FAQPage'; if ( preg_match( '/\b(event|événement|conférence|festival|webinar)\b/u', $text ) ) return 'Event'; if ( preg_match( '/\b(produit|product|prix|price|stock|sku)\b/u', $text ) ) return 'Product'; if ( preg_match( '/\b(review|avis|note|étoiles|stars|rating)\b/u', $text ) ) return 'Review'; if ( preg_match( '/\b(news|actualité|breaking|press release)\b/u', $text ) ) return 'NewsArticle'; if ( preg_match( '/\b(course|cours|formation|module|leçon|chapter)\b/u', $text ) ) return 'Course'; if ( preg_match( '/\b(video|vidéo|youtube|vimeo|embed)\b/u', $text ) ) return 'VideoObject'; if ( preg_match( '/\b(adresse|address|téléphone|phone|horaires|hours|local)\b/u', $text ) ) return 'LocalBusiness'; if ( strlen( $content ) > 800 ) return 'Article'; if ( strlen( $content ) > 200 ) return 'BlogPosting'; return 'WebPage'; } /** * Bibliothèque des types Schema.org supportés. */ public static function supported_schema_types(): array { return array( 'Article', 'BlogPosting', 'NewsArticle', 'TechArticle', 'ScholarlyArticle', 'WebPage', 'AboutPage', 'ContactPage', 'CollectionPage', 'ProfilePage', 'Product', 'Offer', 'AggregateOffer', 'Review', 'AggregateRating', 'Recipe', 'NutritionInformation', 'Restaurant', 'MenuItem', 'HowTo', 'HowToStep', 'HowToSection', 'FAQPage', 'QAPage', 'Question', 'Answer', 'Event', 'EventSeries', 'BusinessEvent', 'EducationEvent', 'MusicEvent', 'SportsEvent', 'Course', 'CourseInstance', 'EducationalOccupationalCredential', 'LearningResource', 'VideoObject', 'AudioObject', 'ImageObject', 'PodcastEpisode', 'PodcastSeries', 'Organization', 'Corporation', 'NGO', 'EducationalOrganization', 'GovernmentOrganization', 'Person', 'Author', 'LocalBusiness', 'MedicalBusiness', 'AutomotiveBusiness', 'FinancialService', 'HomeAndConstructionBusiness', 'LegalService', 'LodgingBusiness', 'ProfessionalService', 'Place', 'TouristAttraction', 'TouristDestination', 'JobPosting', 'MedicalCondition', 'Drug', 'MedicalProcedure', 'BreadcrumbList', 'ItemList', 'SiteNavigationElement', 'SoftwareApplication', 'MobileApplication', 'WebApplication', 'Book', 'CreativeWork', 'Dataset', ); } }