post_content; $title = $post->post_title; $meta_description = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); // 1. Q&A/AEO blocks present (+15) $qa_score = $this->check_qa_blocks($content); $score += $qa_score; $details['qa_blocks'] = $qa_score; // 2. Structured answers & clarity (+10) $structured_score = $this->check_structured_answers($content, $title); $score += $structured_score; $details['structured_answers'] = $structured_score; // 3. Schema JSON-LD valid (+15) $schema_score = $this->check_schema_json_ld($post_id); $score += $schema_score; $details['schema_json_ld'] = $schema_score; // 4. Citations & support (+10) $citations_score = $this->check_citations($content); $score += $citations_score; $details['citations'] = $citations_score; // 5. Internal linking (+10) $internal_links_score = $this->check_internal_links($content); $score += $internal_links_score; $details['internal_links'] = $internal_links_score; // 6. Readability (+10) $readability_score = $this->check_readability($content); $score += $readability_score; $details['readability'] = $readability_score; // 7. Entity coverage (+10) $entity_score = $this->check_entity_coverage($content, $title); $score += $entity_score; $details['entity_coverage'] = $entity_score; // 8. llms.txt + engine prompt discoverable (+10) $discoverability_score = $this->check_discoverability($post_id); $score += $discoverability_score; $details['discoverability'] = $discoverability_score; // 9. Safety/brand constraints met (+10) $safety_score = $this->check_safety_brand($content); $score += $safety_score; $details['safety_brand'] = $safety_score; // Ensure score is between 0-100 $score = max(0, min(100, $score)); // Save score and details update_post_meta($post_id, '_aeo_bob_aeo_score', $score); update_post_meta($post_id, '_aeo_bob_aeo_details', $details); return $score; } /** * Check for Q&A/AEO blocks */ private function check_qa_blocks($content) { $qa_patterns = array( '/]*>.*?(?:faq|question|q&a|q and a|frequently asked).*?<\/h[2-6]>/i', '/]*class[^>]*faq[^>]*>/i', '/]*class[^>]*faq[^>]*>/i', '/\b(?:what|how|why|when|where|who)\b.*?\?/i', '/]*>.*?]*>.*?<\/dt>.*?]*>.*?<\/dd>.*?<\/dl>/s' ); foreach ($qa_patterns as $pattern) { if (preg_match($pattern, $content)) { return self::WEIGHT_QA_BLOCKS; } } return 0; } /** * Check for structured answers within first 180 words */ private function check_structured_answers($content, $title) { $clean_content = wp_strip_all_tags($content); $first_180_words = implode(' ', array_slice(explode(' ', $clean_content), 0, 180)); // Check if there's a clear answer structure $answer_indicators = array( 'answer is', 'solution is', 'result is', 'conclusion is', 'the answer', 'the solution', 'the result', 'the conclusion', 'here\'s how', 'here is how', 'this means', 'this indicates' ); foreach ($answer_indicators as $indicator) { if (stripos($first_180_words, $indicator) !== false) { return self::WEIGHT_STRUCTURED_ANSWERS; } } return 0; } /** * Check for valid Schema JSON-LD */ private function check_schema_json_ld($post_id) { // Check if we have stored schema $schema = get_post_meta($post_id, '_aeobob_engine_prompt', true); if ($schema) { $schema_data = json_decode($schema, true); if ($schema_data && isset($schema_data['schema_ld'])) { return self::WEIGHT_SCHEMA_JSON_LD; } } // Check for existing schema in content $content = get_post_field('post_content', $post_id); if (preg_match('/]*type[^>]*application\/ld\+json[^>]*>(.*?)<\/script>/s', $content, $matches)) { $json_ld = json_decode($matches[1], true); if ($json_ld && (isset($json_ld['@type']) && in_array($json_ld['@type'], array('Article', 'FAQPage')))) { return self::WEIGHT_SCHEMA_JSON_LD; } } return 0; } /** * Check for citations and external links */ private function check_citations($content) { // Count external links preg_match_all('/]*href[^>]*>/i', $content, $links); $external_links = 0; if (isset($links[0])) { foreach ($links[0] as $link) { if (preg_match('/href[^>]*https?:\/\/(?!' . preg_quote(wp_parse_url(home_url(), PHP_URL_HOST)) . ')/i', $link)) { $external_links++; } } } return $external_links > 0 ? self::WEIGHT_CITATIONS : 0; } /** * Check for internal links */ private function check_internal_links($content) { preg_match_all('/]*href[^>]*>/i', $content, $links); $internal_links = 0; if (isset($links[0])) { foreach ($links[0] as $link) { if (preg_match('/href[^>]*https?:\/\/' . preg_quote(wp_parse_url(home_url(), PHP_URL_HOST)) . '/i', $link)) { $internal_links++; } } } return $internal_links >= 2 ? self::WEIGHT_INTERNAL_LINKS : 0; } /** * Check readability (target grade 8-10) */ private function check_readability($content) { $clean_content = wp_strip_all_tags($content); $sentences = preg_split('/[.!?]+/', $clean_content); $words = str_word_count($clean_content); $syllables = $this->count_syllables($clean_content); if (count($sentences) > 0 && $words > 0) { $avg_sentence_length = $words / count($sentences); $avg_syllables_per_word = $syllables / $words; // Flesch Reading Ease calculation $flesch_score = 206.835 - (1.015 * $avg_sentence_length) - (84.6 * $avg_syllables_per_word); // Grade 8-10 corresponds to Flesch scores of 60-70 if ($flesch_score >= 60 && $flesch_score <= 70) { return self::WEIGHT_READABILITY; } elseif ($flesch_score >= 50 && $flesch_score <= 80) { return self::WEIGHT_READABILITY / 2; // Partial credit } } return 0; } /** * Count syllables in text */ private function count_syllables($text) { $text = strtolower($text); $text = preg_replace('/[^a-z]/', '', $text); $syllables = 0; $previous_was_vowel = false; for ($i = 0; $i < strlen($text); $i++) { $is_vowel = in_array($text[$i], array('a', 'e', 'i', 'o', 'u', 'y')); if ($is_vowel && !$previous_was_vowel) { $syllables++; } $previous_was_vowel = $is_vowel; } return max(1, $syllables); } /** * Check entity coverage */ private function check_entity_coverage($content, $title) { // Extract primary keyword $keyword = get_post_meta(get_the_ID(), '_aeo_bob_target_keyword', true); if (!$keyword) { return 0; } // Check if keyword appears in headings and body $headings = array(); preg_match_all('/]*>(.*?)<\/h[1-6]>/i', $content, $headings); $keyword_in_headings = false; $keyword_in_body = false; if (isset($headings[1])) { foreach ($headings[1] as $heading) { if (stripos($heading, $keyword) !== false) { $keyword_in_headings = true; break; } } } if (stripos($content, $keyword) !== false) { $keyword_in_body = true; } return ($keyword_in_headings && $keyword_in_body) ? self::WEIGHT_ENTITY_COVERAGE : 0; } /** * Check discoverability (llms.txt + engine prompt) */ private function check_discoverability($post_id) { // Check if engine prompt exists $engine_prompt = get_post_meta($post_id, '_aeobob_engine_prompt', true); if (!$engine_prompt) { return 0; } // Check if llms.txt is accessible $upload_dir = wp_upload_dir(); $llms_file = $upload_dir['basedir'] . '/llms.txt'; if (!file_exists($llms_file)) { return self::WEIGHT_DISCOVERABILITY / 2; // Partial credit for engine prompt only } return self::WEIGHT_DISCOVERABILITY; } /** * Check safety and brand constraints */ private function check_safety_brand($content) { $forbidden_phrases = array( 'guaranteed results', '100% success', 'overnight success', 'get rich quick', 'make money fast', 'secret formula', 'limited time offer', 'act now', 'don\'t miss out' ); $overclaiming_phrases = array( 'best in the world', 'number one', 'revolutionary', 'breakthrough', 'miracle', 'cure all' ); $content_lower = strtolower($content); foreach ($forbidden_phrases as $phrase) { if (stripos($content_lower, $phrase) !== false) { return 0; // Fail if forbidden phrases found } } $overclaiming_count = 0; foreach ($overclaiming_phrases as $phrase) { if (stripos($content_lower, $phrase) !== false) { $overclaiming_count++; } } // Penalize for overclaiming but don't fail completely if ($overclaiming_count > 2) { return self::WEIGHT_SAFETY_BRAND / 2; } return self::WEIGHT_SAFETY_BRAND; } /** * Get AEO score for a post */ public function get_aeo_score($post_id) { $score = get_post_meta($post_id, '_aeo_bob_aeo_score', true); return $score ? intval($score) : 0; } /** * Get AEO details for a post */ public function get_aeo_details($post_id) { $details = get_post_meta($post_id, '_aeo_bob_aeo_details', true); return $details ? $details : array(); } }