15, 'question_intent' => 15, 'content_architecture' => 10, 'schema_structured' => 15, 'semantic_nlp' => 10, 'technical_seo' => 10, 'answer_extraction' => 10, 'ai_llm' => 5, 'eeat_trust' => 5, 'analytics_future' => 5, ); /** * Calculate comprehensive AEO score. * * @param int $post_id Post ID. * @return array Score data with detailed breakdown. */ public static function calculate_ultimate_score( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return self::get_empty_score(); } // Check if content is truly empty $content = trim( wp_strip_all_tags( $post->post_content ) ); $title = trim( $post->post_title ); $word_count = str_word_count( $content ); // If post has no title and no content, return 0% score if ( empty( $title ) && $word_count === 0 ) { return self::get_empty_score(); } $breakdown = array(); $total_score = 0; $max_possible = 0; // Phase 1 Categories $breakdown['summary_answer'] = self::score_summary_answer( $post_id, $post ); $breakdown['question_intent'] = self::score_question_intent( $post_id, $post ); $breakdown['content_architecture'] = self::score_content_architecture( $post_id, $post ); $breakdown['schema_structured'] = self::score_schema_structured( $post_id, $post ); // Phase 2 Categories $breakdown['semantic_nlp'] = self::score_semantic_nlp( $post_id, $post ); $breakdown['technical_seo'] = self::score_technical_seo( $post_id, $post ); $breakdown['answer_extraction'] = self::score_answer_extraction( $post_id, $post ); $breakdown['ai_llm'] = self::score_ai_llm( $post_id, $post ); $breakdown['eeat_trust'] = self::score_eeat_trust( $post_id, $post ); $breakdown['analytics_future'] = self::score_analytics_future( $post_id, $post ); // Calculate weighted total foreach ( self::$category_weights as $category => $weight ) { if ( isset( $breakdown[ $category ] ) ) { $category_score = $breakdown[ $category ]['score']; $total_score += ( $category_score / 100 ) * $weight; $max_possible += $weight; } } // Normalize to 0-100 scale $final_score = $max_possible > 0 ? round( ( $total_score / $max_possible ) * 100 ) : 0; return array( 'score' => $final_score, 'breakdown' => $breakdown, 'color' => self::get_score_color( $final_score ), 'label' => self::get_score_label( $final_score ), 'phase' => 2, 'categories' => self::$category_weights, ); } /** * Category 1: Summary & Answer Optimization (15% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_summary_answer( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 7; // Check 1: Manual summary provided (not auto-generated) $manual_summary = get_post_meta( $post_id, '_aitk_summary', true ); $checks['manual_summary'] = array( 'label' => __( 'Manual summary provided (not auto-generated)', 'ai-answer-toolkit' ), 'passed' => ! empty( $manual_summary ), 'weight' => 1, ); if ( $checks['manual_summary']['passed'] ) { $total_points += 1; } // Check 2: Summary length between 300-500 characters $summary = AI_Answer_Toolkit_Summary::get_final_summary( $post_id ); $summary_length = strlen( wp_strip_all_tags( $summary ) ); $checks['summary_length'] = array( 'label' => __( 'Summary length between 300–500 characters', 'ai-answer-toolkit' ), 'passed' => ( $summary_length >= 300 && $summary_length <= 500 ), 'weight' => 1, 'value' => $summary_length, ); if ( $checks['summary_length']['passed'] ) { $total_points += 1; } // Check 3: First 1-2 sentences contain a direct answer $first_sentences = self::get_first_sentences( $summary, 2 ); $has_direct_answer = self::contains_direct_answer( $first_sentences ); $checks['direct_answer'] = array( 'label' => __( 'First 1–2 sentences contain a direct answer', 'ai-answer-toolkit' ), 'passed' => $has_direct_answer, 'weight' => 1, ); if ( $checks['direct_answer']['passed'] ) { $total_points += 1; } // Check 4: Summary is information-dense and fluff-free $fluff_density = self::calculate_fluff_density( $summary ); $checks['fluff_free'] = array( 'label' => __( 'Summary is information-dense and fluff-free', 'ai-answer-toolkit' ), 'passed' => ( $fluff_density < 0.15 ), 'weight' => 1, 'value' => round( $fluff_density * 100, 1 ) . '%', ); if ( $checks['fluff_free']['passed'] ) { $total_points += 1; } // Check 5: Key entities included $has_entities = self::has_key_entities( $summary, $post ); $checks['key_entities'] = array( 'label' => __( 'Key entities (brand, topic, product, service) included', 'ai-answer-toolkit' ), 'passed' => $has_entities, 'weight' => 1, ); if ( $checks['key_entities']['passed'] ) { $total_points += 1; } // Check 6: No shortcodes, HTML, or promotional language $is_clean = self::is_clean_summary( $summary ); $checks['clean_summary'] = array( 'label' => __( 'No shortcodes, HTML, or promotional language', 'ai-answer-toolkit' ), 'passed' => $is_clean, 'weight' => 1, ); if ( $checks['clean_summary']['passed'] ) { $total_points += 1; } // Check 7: Suitable for zero-click & AI answer extraction $is_extractable = ( $summary_length >= 40 && $summary_length <= 500 && $has_direct_answer ); $checks['ai_extractable'] = array( 'label' => __( 'Suitable for zero-click & AI answer extraction', 'ai-answer-toolkit' ), 'passed' => $is_extractable, 'weight' => 1, ); if ( $checks['ai_extractable']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 2: Question & Intent Coverage (15% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_question_intent( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 7; $content = get_post_field( 'post_content', $post_id ); $blocks = parse_blocks( $content ); // Check 1: Question-based headings present $question_headings = self::find_question_headings( $blocks ); $checks['question_headings'] = array( 'label' => __( 'Question-based headings present (What, How, Why, When, Where, Which)', 'ai-answer-toolkit' ), 'passed' => count( $question_headings ) > 0, 'weight' => 1, 'value' => count( $question_headings ), ); if ( $checks['question_headings']['passed'] ) { $total_points += 1; } // Check 2: One-question → one-answer structure $has_qa_structure = self::has_qa_structure( $blocks ); $checks['qa_structure'] = array( 'label' => __( 'One-question → one-answer structure', 'ai-answer-toolkit' ), 'passed' => $has_qa_structure, 'weight' => 1, ); if ( $checks['qa_structure']['passed'] ) { $total_points += 1; } // Check 3: Answers placed immediately after questions $answers_follow_questions = self::answers_follow_questions( $blocks ); $checks['answers_placement'] = array( 'label' => __( 'Answers placed immediately after questions', 'ai-answer-toolkit' ), 'passed' => $answers_follow_questions, 'weight' => 1, ); if ( $checks['answers_placement']['passed'] ) { $total_points += 1; } // Check 4: Follow-up & conversational query support $has_followup = self::has_followup_questions( $question_headings ); $checks['followup_queries'] = array( 'label' => __( 'Follow-up & conversational query support', 'ai-answer-toolkit' ), 'passed' => $has_followup, 'weight' => 1, ); if ( $checks['followup_queries']['passed'] ) { $total_points += 1; } // Check 5: Long-tail and semantic question coverage $has_longtail = count( $question_headings ) >= 3; $checks['longtail_coverage'] = array( 'label' => __( 'Long-tail and semantic question coverage', 'ai-answer-toolkit' ), 'passed' => $has_longtail, 'weight' => 1, ); if ( $checks['longtail_coverage']['passed'] ) { $total_points += 1; } // Check 6: Search journey mapping (informational → transactional) $has_journey = self::has_search_journey( $blocks ); $checks['search_journey'] = array( 'label' => __( 'Search journey mapping (informational → transactional)', 'ai-answer-toolkit' ), 'passed' => $has_journey, 'weight' => 1, ); if ( $checks['search_journey']['passed'] ) { $total_points += 1; } // Check 7: Answer ownership clarity (no cannibalization) $has_clear_ownership = count( $question_headings ) > 0 && $has_qa_structure; $checks['answer_ownership'] = array( 'label' => __( 'Cannibalization risk avoided (answer ownership clarity)', 'ai-answer-toolkit' ), 'passed' => $has_clear_ownership, 'weight' => 1, ); if ( $checks['answer_ownership']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 3: Content Architecture & Formatting (10% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_content_architecture( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 7; $content = get_post_field( 'post_content', $post_id ); $blocks = parse_blocks( $content ); // Use existing content guidance analysis $guidance = class_exists( 'AI_Answer_Toolkit_Content_Guidance' ) ? AI_Answer_Toolkit_Content_Guidance::analyze_content( $post_id ) : array(); // Check 1: Single H1 (main entity/topic) $h1_check = isset( $guidance['heading_structure']['h1_count'] ) ? ( $guidance['heading_structure']['h1_count'] === 1 ) : false; $checks['single_h1'] = array( 'label' => __( 'Single H1 (main entity/topic)', 'ai-answer-toolkit' ), 'passed' => $h1_check, 'weight' => 1, ); if ( $checks['single_h1']['passed'] ) { $total_points += 1; } // Check 2: Logical H2–H6 hierarchy (no skipped levels) $hierarchy_check = isset( $guidance['heading_structure']['hierarchy_valid'] ) ? $guidance['heading_structure']['hierarchy_valid'] : false; $checks['heading_hierarchy'] = array( 'label' => __( 'Logical H2–H6 hierarchy (no skipped levels)', 'ai-answer-toolkit' ), 'passed' => $hierarchy_check, 'weight' => 1, ); if ( $checks['heading_hierarchy']['passed'] ) { $total_points += 1; } // Check 3: Direct answers above the fold $first_para_check = isset( $guidance['first_paragraph']['is_optimized'] ) ? $guidance['first_paragraph']['is_optimized'] : false; $checks['above_fold'] = array( 'label' => __( 'Direct answers above the fold', 'ai-answer-toolkit' ), 'passed' => $first_para_check, 'weight' => 1, ); if ( $checks['above_fold']['passed'] ) { $total_points += 1; } // Check 4: TL;DR or quick-answer blocks present $has_tldr = self::has_tldr_block( $blocks ); $checks['tldr_present'] = array( 'label' => __( 'TL;DR or quick-answer blocks present', 'ai-answer-toolkit' ), 'passed' => $has_tldr, 'weight' => 1, ); if ( $checks['tldr_present']['passed'] ) { $total_points += 1; } // Check 5: Short paragraphs, bullets, tables used $has_formatting = self::has_good_formatting( $blocks ); $checks['formatting'] = array( 'label' => __( 'Short paragraphs, bullets, tables used', 'ai-answer-toolkit' ), 'passed' => $has_formatting, 'weight' => 1, ); if ( $checks['formatting']['passed'] ) { $total_points += 1; } // Check 6: Definition, process, benefits, conclusion sections present $section_coverage = isset( $guidance['section_coverage'] ) ? $guidance['section_coverage'] : array(); // Require actual content - empty array or all false values should fail $section_count = is_array( $section_coverage ) ? count( array_filter( $section_coverage ) ) : 0; $has_sections = $section_count >= 2; $checks['section_coverage'] = array( 'label' => __( 'Definition, process, benefits, conclusion sections present', 'ai-answer-toolkit' ), 'passed' => $has_sections, 'weight' => 1, ); if ( $checks['section_coverage']['passed'] ) { $total_points += 1; } // Check 7: Modular content blocks reusable by AI $is_modular = self::is_modular_content( $blocks ); $checks['modular_blocks'] = array( 'label' => __( 'Modular content blocks reusable by AI', 'ai-answer-toolkit' ), 'passed' => $is_modular, 'weight' => 1, ); if ( $checks['modular_blocks']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 4: Structured Data & Schema (15% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_schema_structured( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 9; // Use existing visibility analysis for schema checks $visibility = class_exists( 'AI_Answer_Toolkit_Visibility' ) ? AI_Answer_Toolkit_Visibility::check_visibility( $post_id ) : array(); // Check 1: Article / BlogPosting schema present $has_article_schema = isset( $visibility['article_schema'] ) && $visibility['article_schema']; $checks['article_schema'] = array( 'label' => __( 'Article / BlogPosting schema present', 'ai-answer-toolkit' ), 'passed' => $has_article_schema, 'weight' => 1, ); if ( $checks['article_schema']['passed'] ) { $total_points += 1; } // Check 2: FAQPage schema (if FAQs exist) $faq_count = AI_Answer_Toolkit_Score::get_faq_count( $post_id ); $has_faq_schema = isset( $visibility['faq_schema'] ) && $visibility['faq_schema']; // Only pass if FAQs exist AND schema is present, or if content exists but no FAQs needed $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $faq_schema_appropriate = $content_exists && ( ( $faq_count > 0 && $has_faq_schema ) || $faq_count === 0 ); $checks['faq_schema'] = array( 'label' => __( 'FAQPage schema (if FAQs exist)', 'ai-answer-toolkit' ), 'passed' => $faq_schema_appropriate, 'weight' => 1, ); if ( $checks['faq_schema']['passed'] ) { $total_points += 1; } // Check 3: HowTo / QAPage schema when applicable $has_howto = self::has_howto_content( $post_id ); $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; // Only pass if content exists and either no how-to needed or schema is present $howto_check = $content_exists && ( ! $has_howto || self::has_howto_schema( $post_id ) ); $checks['howto_schema'] = array( 'label' => __( 'HowTo / QAPage schema when applicable', 'ai-answer-toolkit' ), 'passed' => $howto_check, 'weight' => 1, ); if ( $checks['howto_schema']['passed'] ) { $total_points += 1; } // Check 4: Product, Review, Service schema when relevant $post_type = get_post_type( $post_id ); $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $has_type_schema = ( $post_type === 'service' ) ? self::has_service_schema( $post_id ) : $content_exists; $checks['type_schema'] = array( 'label' => __( 'Product, Review, Service schema when relevant', 'ai-answer-toolkit' ), 'passed' => $has_type_schema, 'weight' => 1, ); if ( $checks['type_schema']['passed'] ) { $total_points += 1; } // Check 5: Organization & Person schema defined $has_org_schema = self::has_organization_schema(); $checks['org_schema'] = array( 'label' => __( 'Organization & Person schema defined', 'ai-answer-toolkit' ), 'passed' => $has_org_schema, 'weight' => 1, ); if ( $checks['org_schema']['passed'] ) { $total_points += 1; } // Check 6: Breadcrumb schema enabled $has_breadcrumb = self::has_breadcrumb_schema( $post_id ); $checks['breadcrumb_schema'] = array( 'label' => __( 'Breadcrumb schema enabled', 'ai-answer-toolkit' ), 'passed' => $has_breadcrumb, 'weight' => 1, ); if ( $checks['breadcrumb_schema']['passed'] ) { $total_points += 1; } // Check 7: Speakable schema for voice readiness $has_speakable = self::has_speakable_schema( $post_id ); $checks['speakable_schema'] = array( 'label' => __( 'Speakable schema for voice readiness', 'ai-answer-toolkit' ), 'passed' => $has_speakable, 'weight' => 1, ); if ( $checks['speakable_schema']['passed'] ) { $total_points += 1; } // Check 8: Schema nesting valid and error-free $schema_valid = self::validate_schema_structure( $post_id ); $checks['schema_valid'] = array( 'label' => __( 'Schema nesting valid and error-free', 'ai-answer-toolkit' ), 'passed' => $schema_valid, 'weight' => 1, ); if ( $checks['schema_valid']['passed'] ) { $total_points += 1; } // Check 9: AI-friendly, low-entropy schema output $schema_quality = self::check_schema_quality( $post_id ); $checks['schema_quality'] = array( 'label' => __( 'AI-friendly, low-entropy schema output', 'ai-answer-toolkit' ), 'passed' => $schema_quality, 'weight' => 1, ); if ( $checks['schema_quality']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } // ======================================== // Helper Methods // ======================================== /** * Get first N sentences from text. */ private static function get_first_sentences( $text, $count = 2 ) { $text = wp_strip_all_tags( $text ); preg_match_all( '/[^.!?]+[.!?]+/', $text, $matches ); $sentences = isset( $matches[0] ) ? $matches[0] : array(); return implode( ' ', array_slice( $sentences, 0, $count ) ); } /** * Check if text contains a direct answer. */ private static function contains_direct_answer( $text ) { $text = strtolower( $text ); $answer_patterns = array( '/^(yes|no),/', '/is (a|an|the)/', '/refers to/', '/means/', '/defined as/', '/can be/', ); foreach ( $answer_patterns as $pattern ) { if ( preg_match( $pattern, $text ) ) { return true; } } return strlen( $text ) >= 40 && strlen( $text ) <= 200; } /** * Calculate fluff density in text. */ private static function calculate_fluff_density( $text ) { $text = strtolower( wp_strip_all_tags( $text ) ); $word_count = str_word_count( $text ); // Empty content should fail, not pass if ( $word_count === 0 ) { return 1.0; // 100% fluff (empty = all fluff) } $fluff_words = array( 'welcome', 'today', 'in this post', 'in this article', 'we will', 'let\'s dive', 'let us', 'you might be wondering', 'have you ever', 'in today\'s', 'in this guide', 'comprehensive guide', ); $fluff_count = 0; foreach ( $fluff_words as $fluff ) { $fluff_count += substr_count( $text, $fluff ); } return ( $fluff_count / $word_count ); } /** * Check if summary has key entities. */ private static function has_key_entities( $summary, $post ) { $summary_lower = strtolower( wp_strip_all_tags( $summary ) ); $title_words = explode( ' ', strtolower( $post->post_title ) ); $significant_words = array_filter( $title_words, function( $word ) { return strlen( $word ) > 3; }); foreach ( $significant_words as $word ) { if ( strpos( $summary_lower, $word ) !== false ) { return true; } } return false; } /** * Check if summary is clean (no shortcodes, HTML, promo). */ private static function is_clean_summary( $summary ) { // Empty content should fail, not pass $summary_stripped = trim( wp_strip_all_tags( $summary ) ); if ( empty( $summary_stripped ) ) { return false; } if ( has_shortcode( $summary, '' ) || strpos( $summary, '[' ) !== false ) { return false; } if ( $summary !== wp_strip_all_tags( $summary ) ) { return false; } $promo_words = array( 'buy now', 'click here', 'limited time', 'special offer', 'discount' ); $summary_lower = strtolower( $summary ); foreach ( $promo_words as $promo ) { if ( strpos( $summary_lower, $promo ) !== false ) { return false; } } return true; } /** * Find question headings in blocks. */ private static function find_question_headings( $blocks ) { $questions = array(); foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) && strpos( $block['blockName'], 'heading' ) !== false ) { $text = isset( $block['innerHTML'] ) ? wp_strip_all_tags( $block['innerHTML'] ) : ''; if ( self::is_question( $text ) ) { $questions[] = $text; } } } return $questions; } /** * Check if text is a question. */ private static function is_question( $text ) { if ( substr( trim( $text ), -1 ) === '?' ) { return true; } $question_starters = array( 'what', 'how', 'why', 'when', 'where', 'which', 'who', 'can', 'should', 'is', 'are', 'do', 'does' ); $first_word = strtolower( strtok( trim( $text ), ' ' ) ); return in_array( $first_word, $question_starters, true ); } /** * Check if content has Q&A structure. */ private static function has_qa_structure( $blocks ) { $question_count = 0; $answer_count = 0; for ( $i = 0; $i < count( $blocks ); $i++ ) { $block = $blocks[ $i ]; if ( isset( $block['blockName'] ) && strpos( $block['blockName'], 'heading' ) !== false ) { $text = isset( $block['innerHTML'] ) ? wp_strip_all_tags( $block['innerHTML'] ) : ''; if ( self::is_question( $text ) ) { $question_count++; if ( isset( $blocks[ $i + 1 ] ) && $blocks[ $i + 1 ]['blockName'] === 'core/paragraph' ) { $answer_count++; } } } } return $question_count > 0 && $answer_count >= $question_count; } /** * Check if answers follow questions. */ private static function answers_follow_questions( $blocks ) { return self::has_qa_structure( $blocks ); } /** * Check if content has follow-up questions. */ private static function has_followup_questions( $questions ) { if ( count( $questions ) < 2 ) { return false; } $followup_words = array( 'also', 'additionally', 'furthermore', 'moreover', 'another', 'related' ); foreach ( $questions as $q ) { $q_lower = strtolower( $q ); foreach ( $followup_words as $word ) { if ( strpos( $q_lower, $word ) !== false ) { return true; } } } return count( $questions ) >= 3; } /** * Check if content has search journey mapping. */ private static function has_search_journey( $blocks ) { $has_informational = false; $has_transactional = false; foreach ( $blocks as $block ) { $text = isset( $block['innerHTML'] ) ? strtolower( wp_strip_all_tags( $block['innerHTML'] ) ) : ''; if ( preg_match( '/(what|why|how|guide|tutorial|learn)/', $text ) ) { $has_informational = true; } if ( preg_match( '/(buy|purchase|price|cost|service|contact)/', $text ) ) { $has_transactional = true; } } return $has_informational && $has_transactional; } /** * Check if content has TL;DR block. */ private static function has_tldr_block( $blocks ) { foreach ( $blocks as $block ) { $text = isset( $block['innerHTML'] ) ? strtolower( wp_strip_all_tags( $block['innerHTML'] ) ) : ''; if ( preg_match( '/(tl;dr|tldr|quick answer|in short|summary)/', $text ) ) { return true; } } return false; } /** * Check if content has good formatting. */ private static function has_good_formatting( $blocks ) { $has_list = false; $has_table = false; foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) ) { if ( strpos( $block['blockName'], 'list' ) !== false ) { $has_list = true; } if ( strpos( $block['blockName'], 'table' ) !== false ) { $has_table = true; } } } return $has_list || $has_table; } /** * Check if content is modular. */ private static function is_modular_content( $blocks ) { return count( $blocks ) >= 5; } /** * Check if content has HowTo structure. */ private static function has_howto_content( $post_id ) { $content = get_post_field( 'post_content', $post_id ); $content_lower = strtolower( $content ); return preg_match( '/(step|how to|tutorial|guide|instructions)/', $content_lower ); } /** * Schema detection helpers (placeholder - implement based on your schema setup). */ private static function has_howto_schema( $post_id ) { return apply_filters( 'aitk_has_howto_schema', false, $post_id ); } private static function has_service_schema( $post_id ) { return apply_filters( 'aitk_has_service_schema', false, $post_id ); } private static function has_organization_schema() { return apply_filters( 'aitk_has_organization_schema', false ); } private static function has_breadcrumb_schema( $post_id ) { return apply_filters( 'aitk_has_breadcrumb_schema', false, $post_id ); } private static function has_speakable_schema( $post_id ) { return apply_filters( 'aitk_has_speakable_schema', false, $post_id ); } private static function validate_schema_structure( $post_id ) { $post = get_post( $post_id ); $content_exists = $post && str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; return apply_filters( 'aitk_validate_schema_structure', $content_exists, $post_id ); } private static function check_schema_quality( $post_id ) { $post = get_post( $post_id ); $content_exists = $post && str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; return apply_filters( 'aitk_check_schema_quality', $content_exists, $post_id ); } /** * Category 5: Semantic & NLP Optimization (10% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_semantic_nlp( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 8; $content = $post->post_content; $blocks = parse_blocks( $content ); $text_content = wp_strip_all_tags( $content ); $word_count = str_word_count( $text_content ); // Check 1: Entity-rich content (proper nouns, specific terms) $has_entities = preg_match_all( '/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/', $text_content ) > 5; $checks['entity_rich'] = array( 'label' => __( 'Entity-rich content (proper nouns, specific terms)', 'ai-answer-toolkit' ), 'passed' => $has_entities, 'weight' => 1, ); if ( $checks['entity_rich']['passed'] ) { $total_points += 1; } // Check 2: Natural language patterns (conversational tone) $has_natural_language = preg_match( '/\b(you|your|we|our|how|why|what|when|where)\b/i', $text_content ); $checks['natural_language'] = array( 'label' => __( 'Natural language patterns (conversational tone)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_natural_language, 'weight' => 1, ); if ( $checks['natural_language']['passed'] ) { $total_points += 1; } // Check 3: Synonym & related term coverage $has_synonyms = $word_count > 300; $checks['synonym_coverage'] = array( 'label' => __( 'Synonym & related term coverage', 'ai-answer-toolkit' ), 'passed' => $has_synonyms, 'weight' => 1, ); if ( $checks['synonym_coverage']['passed'] ) { $total_points += 1; } // Check 4: Contextual relevance (topic consistency) $title = get_the_title( $post_id ); $title_words = explode( ' ', strtolower( $title ) ); $content_lower = strtolower( $text_content ); $topic_mentions = 0; foreach ( $title_words as $word ) { if ( strlen( $word ) > 3 ) { $topic_mentions += substr_count( $content_lower, $word ); } } $has_context = $topic_mentions > 3; $checks['contextual_relevance'] = array( 'label' => __( 'Contextual relevance (topic consistency)', 'ai-answer-toolkit' ), 'passed' => $has_context, 'weight' => 1, ); if ( $checks['contextual_relevance']['passed'] ) { $total_points += 1; } // Check 5: Semantic HTML tags (strong, em, mark) $has_semantic_html = preg_match( '/<(strong|em|mark|b|i)>/i', $content ); $checks['semantic_html'] = array( 'label' => __( 'Semantic HTML tags (strong, em, mark)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_semantic_html, 'weight' => 1, ); if ( $checks['semantic_html']['passed'] ) { $total_points += 1; } // Check 6: Topical depth (comprehensive coverage) $has_depth = $word_count >= 800; $checks['topical_depth'] = array( 'label' => __( 'Topical depth (comprehensive coverage)', 'ai-answer-toolkit' ), 'passed' => $has_depth, 'weight' => 1, ); if ( $checks['topical_depth']['passed'] ) { $total_points += 1; } // Check 7: Co-occurrence patterns (related concepts together) $paragraphs = explode( "\n\n", $text_content ); $has_cooccurrence = count( $paragraphs ) >= 5; $checks['cooccurrence'] = array( 'label' => __( 'Co-occurrence patterns (related concepts together)', 'ai-answer-toolkit' ), 'passed' => $has_cooccurrence, 'weight' => 1, ); if ( $checks['cooccurrence']['passed'] ) { $total_points += 1; } // Check 8: Avoid keyword stuffing (natural density) $keyword_density = 0; if ( ! empty( $title_words ) && $word_count > 0 ) { $main_keyword = $title_words[0]; if ( strlen( $main_keyword ) > 3 ) { $keyword_count = substr_count( $content_lower, $main_keyword ); $keyword_density = ( $keyword_count / $word_count ) * 100; } } $natural_density = $keyword_density < 3; $checks['natural_density'] = array( 'label' => __( 'Avoid keyword stuffing (natural density)', 'ai-answer-toolkit' ), 'passed' => $natural_density, 'weight' => 1, ); if ( $checks['natural_density']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 6: Technical AEO & SEO Foundations (10% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_technical_seo( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 8; // Check 1: Clean, descriptive URL slug $slug = $post->post_name; $clean_slug = strlen( $slug ) > 3 && ! preg_match( '/[0-9]{4,}/', $slug ); $checks['clean_url'] = array( 'label' => __( 'Clean, descriptive URL slug', 'ai-answer-toolkit' ), 'passed' => $clean_slug, 'weight' => 1, ); if ( $checks['clean_url']['passed'] ) { $total_points += 1; } // Check 2: Meta description present $meta_desc = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ); if ( empty( $meta_desc ) ) { $meta_desc = get_post_meta( $post_id, '_aioseop_description', true ); } $has_meta_desc = ! empty( $meta_desc ) && strlen( $meta_desc ) >= 120; $checks['meta_description'] = array( 'label' => __( 'Meta description present (120+ chars)', 'ai-answer-toolkit' ), 'passed' => $has_meta_desc, 'weight' => 1, ); if ( $checks['meta_description']['passed'] ) { $total_points += 1; } // Check 3: Mobile-friendly content $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $has_mobile_friendly = $content_exists; // Only pass if content exists $checks['mobile_friendly'] = array( 'label' => __( 'Mobile-friendly content structure', 'ai-answer-toolkit' ), 'passed' => $has_mobile_friendly, 'weight' => 1, ); if ( $checks['mobile_friendly']['passed'] ) { $total_points += 1; } // Check 4: Fast load time indicators $content_size = strlen( $post->post_content ); $fast_load = $content_size < 50000; $checks['fast_load'] = array( 'label' => __( 'Fast load time (optimized content size)', 'ai-answer-toolkit' ), 'passed' => $fast_load, 'weight' => 1, ); if ( $checks['fast_load']['passed'] ) { $total_points += 1; } // Check 5: Internal linking present $has_internal_links = preg_match_all( '/]+href=["\'](?!http|\/\/|mailto:|tel:)[^"\']*["\'][^>]*>/i', $post->post_content ) > 0; $checks['internal_linking'] = array( 'label' => __( 'Internal linking present', 'ai-answer-toolkit' ), 'passed' => $has_internal_links, 'weight' => 1, ); if ( $checks['internal_linking']['passed'] ) { $total_points += 1; } // Check 6: Image optimization (alt text) preg_match_all( '/]+>/i', $post->post_content, $images ); $total_images = count( $images[0] ); $images_with_alt = 0; foreach ( $images[0] as $img ) { if ( preg_match( '/alt=["\'][^"\']+["\']/', $img ) ) { $images_with_alt++; } } // Only pass if: (1) no images but content exists, OR (2) images exist with 80%+ alt text $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $images_optimized = $content_exists && ( $total_images === 0 || ( $total_images > 0 && $images_with_alt / $total_images >= 0.8 ) ); $checks['image_optimization'] = array( 'label' => __( 'Image optimization (alt text present)', 'ai-answer-toolkit' ), 'passed' => $images_optimized, 'weight' => 1, ); if ( $checks['image_optimization']['passed'] ) { $total_points += 1; } // Check 7: Canonical URL set $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $has_canonical = $content_exists; // Only pass if content exists $checks['canonical_url'] = array( 'label' => __( 'Canonical URL properly set', 'ai-answer-toolkit' ), 'passed' => $has_canonical, 'weight' => 1, ); if ( $checks['canonical_url']['passed'] ) { $total_points += 1; } // Check 8: No broken formatting $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $no_broken_format = $content_exists && ! preg_match( '/<(p|div|span)[^>]*>\s*<\/\1>/', $post->post_content ); $checks['no_broken_format'] = array( 'label' => __( 'No broken formatting (empty tags)', 'ai-answer-toolkit' ), 'passed' => $no_broken_format, 'weight' => 1, ); if ( $checks['no_broken_format']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 7: Answer Extraction Readiness (10% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_answer_extraction( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 7; $content = $post->post_content; $blocks = parse_blocks( $content ); $text_content = wp_strip_all_tags( $content ); // Check 1: Clear topic sentences in paragraphs $paragraphs = explode( "\n\n", $text_content ); $has_topic_sentences = count( $paragraphs ) >= 3; $checks['topic_sentences'] = array( 'label' => __( 'Clear topic sentences in paragraphs', 'ai-answer-toolkit' ), 'passed' => $has_topic_sentences, 'weight' => 1, ); if ( $checks['topic_sentences']['passed'] ) { $total_points += 1; } // Check 2: Scannable formatting (lists, bold, headings) $has_lists = preg_match( '/<(ul|ol)>/i', $content ); $has_bold = preg_match( '/<(strong|b)>/i', $content ); $has_headings = preg_match( '//i', $content ); $is_scannable = $has_lists || ( $has_bold && $has_headings ); $checks['scannable_format'] = array( 'label' => __( 'Scannable formatting (lists, bold, headings)', 'ai-answer-toolkit' ), 'passed' => $is_scannable, 'weight' => 1, ); if ( $checks['scannable_format']['passed'] ) { $total_points += 1; } // Check 3: Fact-based statements (numbers, data) $has_facts = preg_match( '/\b\d+(\.\d+)?%?\b/', $text_content ); $checks['fact_based'] = array( 'label' => __( 'Fact-based statements (numbers, data)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_facts, 'weight' => 1, ); if ( $checks['fact_based']['passed'] ) { $total_points += 1; } // Check 4: Quotable snippets (concise, standalone) $sentences = preg_split( '/[.!?]+/', $text_content ); $quotable_count = 0; foreach ( $sentences as $sentence ) { $word_count = str_word_count( trim( $sentence ) ); if ( $word_count >= 10 && $word_count <= 30 ) { $quotable_count++; } } $has_quotable = $quotable_count >= 3; $checks['quotable_snippets'] = array( 'label' => __( 'Quotable snippets (concise, standalone)', 'ai-answer-toolkit' ), 'passed' => $has_quotable, 'weight' => 1, ); if ( $checks['quotable_snippets']['passed'] ) { $total_points += 1; } // Check 5: Definition-style content $has_definition = preg_match( '/\b(is|are|means|refers to|defined as)\b/i', $text_content ); $checks['definition_style'] = array( 'label' => __( 'Definition-style content present', 'ai-answer-toolkit' ), 'passed' => (bool) $has_definition, 'weight' => 1, ); if ( $checks['definition_style']['passed'] ) { $total_points += 1; } // Check 6: Step-by-step instructions (if applicable) $has_steps = preg_match( '/\b(step|first|second|third|next|then|finally)\b/i', $text_content ); $checks['step_by_step'] = array( 'label' => __( 'Step-by-step instructions (if applicable)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_steps, 'weight' => 1, ); if ( $checks['step_by_step']['passed'] ) { $total_points += 1; } // Check 7: Comparison tables or structured data $has_table = preg_match( '/ __( 'Comparison tables or structured data', 'ai-answer-toolkit' ), 'passed' => (bool) $has_table, 'weight' => 1, ); if ( $checks['comparison_tables']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 8: AI & LLM Optimization Signals (5% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_ai_llm( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 6; $content = $post->post_content; $text_content = wp_strip_all_tags( $content ); $word_count = str_word_count( $text_content ); // Check 1: Prompt-friendly language $has_prompt_friendly = preg_match( '/\b(how to|what is|why|when|where|guide|tutorial|explained)\b/i', $text_content ); $checks['prompt_friendly'] = array( 'label' => __( 'Prompt-friendly language (how-to, what-is)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_prompt_friendly, 'weight' => 1, ); if ( $checks['prompt_friendly']['passed'] ) { $total_points += 1; } // Check 2: Context-complete paragraphs $paragraphs = explode( "\n\n", $text_content ); $complete_paragraphs = 0; foreach ( $paragraphs as $para ) { if ( str_word_count( $para ) >= 50 ) { $complete_paragraphs++; } } $has_context_complete = $complete_paragraphs >= 3; $checks['context_complete'] = array( 'label' => __( 'Context-complete paragraphs (50+ words)', 'ai-answer-toolkit' ), 'passed' => $has_context_complete, 'weight' => 1, ); if ( $checks['context_complete']['passed'] ) { $total_points += 1; } // Check 3: Avoid ambiguous pronouns $pronoun_count = preg_match_all( '/\b(it|this|that|they|them)\b/i', $text_content ); $pronoun_density = $word_count > 0 ? ( $pronoun_count / $word_count ) * 100 : 0; $low_ambiguity = $pronoun_density < 5; $checks['low_ambiguity'] = array( 'label' => __( 'Avoid ambiguous pronouns (low it/this/that)', 'ai-answer-toolkit' ), 'passed' => $low_ambiguity, 'weight' => 1, ); if ( $checks['low_ambiguity']['passed'] ) { $total_points += 1; } // Check 4: Explicit subject-verb-object structure $has_explicit_structure = $word_count >= 300; $checks['explicit_structure'] = array( 'label' => __( 'Explicit subject-verb-object structure', 'ai-answer-toolkit' ), 'passed' => $has_explicit_structure, 'weight' => 1, ); if ( $checks['explicit_structure']['passed'] ) { $total_points += 1; } // Check 5: Minimal jargon (or explained jargon) $has_explanations = preg_match( '/\(([^)]+)\)/', $text_content ); $checks['minimal_jargon'] = array( 'label' => __( 'Minimal jargon (or explained jargon)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_explanations, 'weight' => 1, ); if ( $checks['minimal_jargon']['passed'] ) { $total_points += 1; } // Check 6: Consistent terminology $has_consistency = $word_count >= 500; $checks['consistent_terminology'] = array( 'label' => __( 'Consistent terminology throughout', 'ai-answer-toolkit' ), 'passed' => $has_consistency, 'weight' => 1, ); if ( $checks['consistent_terminology']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 9: E-E-A-T & Trust Signals (5% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_eeat_trust( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 6; $content = $post->post_content; $text_content = wp_strip_all_tags( $content ); // Check 1: Author bio/credentials visible $author_id = $post->post_author; $author_bio = get_the_author_meta( 'description', $author_id ); $has_author_bio = ! empty( $author_bio ) && strlen( $author_bio ) > 50; $checks['author_credentials'] = array( 'label' => __( 'Author bio/credentials visible', 'ai-answer-toolkit' ), 'passed' => $has_author_bio, 'weight' => 1, ); if ( $checks['author_credentials']['passed'] ) { $total_points += 1; } // Check 2: Publication/update dates shown $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $has_dates = $content_exists && ! empty( $post->post_date ) && ! empty( $post->post_modified ); $checks['publication_dates'] = array( 'label' => __( 'Publication/update dates shown', 'ai-answer-toolkit' ), 'passed' => $has_dates, 'weight' => 1, ); if ( $checks['publication_dates']['passed'] ) { $total_points += 1; } // Check 3: External citations/sources $has_external_links = preg_match_all( '/]+href=["\']https?:\/\/[^"\']+["\'][^>]*>/i', $content ) > 0; $checks['external_citations'] = array( 'label' => __( 'External citations/sources linked', 'ai-answer-toolkit' ), 'passed' => $has_external_links, 'weight' => 1, ); if ( $checks['external_citations']['passed'] ) { $total_points += 1; } // Check 4: Original research/insights $has_original_content = str_word_count( $text_content ) >= 800; $checks['original_research'] = array( 'label' => __( 'Original research/insights (800+ words)', 'ai-answer-toolkit' ), 'passed' => $has_original_content, 'weight' => 1, ); if ( $checks['original_research']['passed'] ) { $total_points += 1; } // Check 5: Contact/about information $has_contact_info = has_nav_menu( 'primary' ) || has_nav_menu( 'footer' ); $checks['contact_info'] = array( 'label' => __( 'Contact/about information accessible', 'ai-answer-toolkit' ), 'passed' => $has_contact_info, 'weight' => 1, ); if ( $checks['contact_info']['passed'] ) { $total_points += 1; } // Check 6: Transparent methodology $has_methodology = preg_match( '/\b(research|study|analysis|tested|reviewed|compared)\b/i', $text_content ); $checks['transparent_methodology'] = array( 'label' => __( 'Transparent methodology mentioned', 'ai-answer-toolkit' ), 'passed' => (bool) $has_methodology, 'weight' => 1, ); if ( $checks['transparent_methodology']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Category 10: Analytics, Testing & Future Readiness (5% weight). * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @return array Category score data. */ private static function score_analytics_future( $post_id, $post ) { $checks = array(); $total_points = 0; $max_points = 5; $content = $post->post_content; // Check 1: Content freshness (updated recently) $content_exists = str_word_count( wp_strip_all_tags( $post->post_content ) ) > 0; $last_modified = strtotime( $post->post_modified ); $days_since_update = ( time() - $last_modified ) / ( 60 * 60 * 24 ); $is_fresh = $content_exists && $days_since_update < 180; $checks['content_freshness'] = array( 'label' => __( 'Content freshness (updated within 6 months)', 'ai-answer-toolkit' ), 'passed' => $is_fresh, 'weight' => 1, ); if ( $checks['content_freshness']['passed'] ) { $total_points += 1; } // Check 2: Engagement potential (CTA, comments enabled) $comments_open = comments_open( $post_id ); $has_cta = preg_match( '/\b(learn more|read more|subscribe|download|contact|get started)\b/i', $content ); $has_engagement = $comments_open || $has_cta; $checks['engagement_potential'] = array( 'label' => __( 'Engagement potential (CTA, comments enabled)', 'ai-answer-toolkit' ), 'passed' => $has_engagement, 'weight' => 1, ); if ( $checks['engagement_potential']['passed'] ) { $total_points += 1; } // Check 3: Multi-format content (text + images/video) $has_images = preg_match( '/ __( 'Multi-format content (text + images/video)', 'ai-answer-toolkit' ), 'passed' => $is_multiformat, 'weight' => 1, ); if ( $checks['multiformat_content']['passed'] ) { $total_points += 1; } // Check 4: Voice search optimization $has_voice_optimization = preg_match( '/\b(how|what|why|when|where|who)\b/i', get_the_title( $post_id ) ); $checks['voice_optimization'] = array( 'label' => __( 'Voice search optimization (question-based title)', 'ai-answer-toolkit' ), 'passed' => (bool) $has_voice_optimization, 'weight' => 1, ); if ( $checks['voice_optimization']['passed'] ) { $total_points += 1; } // Check 5: Future-proof structure (modular, reusable) $blocks = parse_blocks( $content ); $is_future_proof = count( $blocks ) >= 5; $checks['future_proof'] = array( 'label' => __( 'Future-proof structure (modular blocks)', 'ai-answer-toolkit' ), 'passed' => $is_future_proof, 'weight' => 1, ); if ( $checks['future_proof']['passed'] ) { $total_points += 1; } $score = $max_points > 0 ? round( ( $total_points / $max_points ) * 100 ) : 0; return array( 'score' => $score, 'max' => 100, 'checks' => $checks, 'points' => $total_points, 'total' => $max_points, ); } /** * Get score color. */ private static function get_score_color( $score ) { if ( $score >= 80 ) { return 'green'; } elseif ( $score >= 50 ) { return 'yellow'; } return 'red'; } /** * Get score label. */ private static function get_score_label( $score ) { if ( $score >= 80 ) { return __( 'Excellent - AI Answer Ready', 'ai-answer-toolkit' ); } elseif ( $score >= 50 ) { return __( 'Good - Optimization Recommended', 'ai-answer-toolkit' ); } return __( 'Poor - High Risk / Low AI Visibility', 'ai-answer-toolkit' ); } /** * Get empty score data. */ private static function get_empty_score() { return array( 'score' => 0, 'breakdown' => array(), 'color' => 'red', 'label' => __( 'No Data', 'ai-answer-toolkit' ), 'phase' => 1, 'categories' => self::$category_weights, ); } }