check_title_issues($title, $clean_content); $issues = array_merge($issues, $title_issues); // Intro issues (early answer) $intro_issues = $this->check_intro_issues($content, $clean_content); $issues = array_merge($issues, $intro_issues); // Header issues $header_issues = $this->check_header_issues($content); $issues = array_merge($issues, $header_issues); // Body content issues $body_issues = $this->check_body_issues($content, $clean_content); $issues = array_merge($issues, $body_issues); // Link issues $link_issues = $this->check_link_issues($content); $issues = array_merge($issues, $link_issues); // Schema opportunities $schema_issues = $this->check_schema_opportunities($content, $clean_content, $title); $issues = array_merge($issues, $schema_issues); // Meta description issues if (!empty($meta_description)) { $meta_issues = $this->check_meta_issues($meta_description, $title); $issues = array_merge($issues, $meta_issues); } // Calculate signals $signals = $this->calculate_signals($content, $clean_content, $breakdown); return array( 'issues' => $issues, 'signals' => $signals, ); } /** * Check title issues * * @param string $title Post title * @param string $content Clean content * @return array Issues */ private function check_title_issues($title, $content) { $issues = array(); $title_length = strlen($title); $title_words = str_word_count($title); // Too short or too long if ($title_length < 35) { $issues[] = array( 'id' => 'title_too_short', 'severity' => 'medium', 'why' => 'Short titles miss opportunities for entities and context', 'hint' => 'Expand to 40-65 characters with specific entities or qualifiers', ); } elseif ($title_length > 70) { $issues[] = array( 'id' => 'title_too_long', 'severity' => 'medium', 'why' => 'Long titles get truncated in search results', 'hint' => 'Trim to 60-65 characters, removing filler words', ); } // No question mark when content suggests it should be if (substr_count($content, '?') > 5 && strpos($title, '?') === false) { $issues[] = array( 'id' => 'title_lacks_question', 'severity' => 'low', 'why' => 'FAQ-style content benefits from question titles', 'hint' => 'Consider reformatting as "How to..." or "What is..." question', ); } // Lacks specific entities $has_entity = preg_match('/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\b/', $title); if (!$has_entity && $title_words > 3) { $issues[] = array( 'id' => 'title_lacks_entity', 'severity' => 'high', 'why' => 'Titles without specific entities are less discoverable', 'hint' => 'Add a specific tool, product, concept, or brand name', ); } // Late answer pattern (answer buried in title) if (preg_match('/\b(the answer|here\'s how|learn|discover)\b/i', $title)) { $issues[] = array( 'id' => 'title_late_answer', 'severity' => 'medium', 'why' => 'Title promises answer but doesn\'t deliver it immediately', 'hint' => 'Front-load the key insight or answer in the title', ); } return $issues; } /** * Check intro/early answer issues * * @param string $content HTML content * @param string $clean_content Clean text * @return array Issues */ private function check_intro_issues($content, $clean_content) { $issues = array(); // Extract first 2-3 paragraphs preg_match_all('/]*>(.*?)<\/p>/is', $content, $paragraphs); $first_paras = array_slice($paragraphs[1], 0, 3); $intro_text = implode(' ', array_map('wp_strip_all_tags', $first_paras)); $intro_words = str_word_count($intro_text); // No early answer summary if ($intro_words < 30) { $issues[] = array( 'id' => 'no_early_answer', 'severity' => 'high', 'why' => 'AEO requires a concise answer in the opening 2-3 sentences', 'hint' => 'Add a 2-sentence summary answering the core query at the very top', ); } // First sentence is too long (reduces scannability) $sentences = preg_split('/[.!?]+/', $intro_text, -1, PREG_SPLIT_NO_EMPTY); if (!empty($sentences)) { $first_sentence_words = str_word_count($sentences[0]); if ($first_sentence_words > 25) { $issues[] = array( 'id' => 'first_sentence_too_long', 'severity' => 'medium', 'why' => 'Long opening sentences reduce scannability for LLMs', 'hint' => 'Break first sentence into 2 shorter sentences (15-20 words each)', ); } } // Lacks definition in intro $has_definition = preg_match('/\b(?:is|means|refers to|defined as)\b/i', $intro_text); if (!$has_definition && strlen($intro_text) > 100) { $issues[] = array( 'id' => 'intro_lacks_definition', 'severity' => 'medium', 'why' => 'Early definitions help LLMs categorize and cite content', 'hint' => 'Add a simple "X is..." definition in first 2 sentences', ); } return $issues; } /** * Check header structure issues * * @param string $content HTML content * @return array Issues */ private function check_header_issues($content) { $issues = array(); // Extract all headers preg_match_all('/]*>(.*?)<\/h\1>/is', $content, $headers); $header_count = count($headers[0]); // Too few headers if ($header_count < 3) { $issues[] = array( 'id' => 'insufficient_headers', 'severity' => 'high', 'why' => 'Headers create scannability and semantic structure', 'hint' => 'Add 3-5 H2/H3 headers to break content into sections', ); } // No FAQ-style headers $has_faq_header = false; foreach ($headers[2] as $header_text) { if (preg_match('/\b(how|what|why|when|where|can|should|which)\b/i', $header_text)) { $has_faq_header = true; break; } } if (!$has_faq_header && $header_count > 0) { $issues[] = array( 'id' => 'no_faq_headers', 'severity' => 'medium', 'why' => 'FAQ-style headers target buyer intent queries', 'hint' => 'Convert 1-2 headers to question format (How, What, Why)', ); } // Headers too similar if ($header_count > 2) { $header_texts = array_map('strtolower', array_map('wp_strip_all_tags', $headers[2])); $unique_words = array(); foreach ($header_texts as $ht) { $words = str_word_count($ht, 1); $unique_words = array_merge($unique_words, $words); } $unique_ratio = count(array_unique($unique_words)) / max(1, count($unique_words)); if ($unique_ratio < 0.6) { $issues[] = array( 'id' => 'headers_too_similar', 'severity' => 'low', 'why' => 'Repetitive headers reduce topical diversity signals', 'hint' => 'Vary header vocabulary to cover different query angles', ); } } return $issues; } /** * Check body content issues * * @param string $content HTML content * @param string $clean_content Clean text * @return array Issues */ private function check_body_issues($content, $clean_content) { $issues = array(); // Missing lists (bullets or numbered) $has_lists = preg_match('/<(?:ul|ol)[^>]*>/i', $content); if (!$has_lists) { $issues[] = array( 'id' => 'no_lists', 'severity' => 'medium', 'why' => 'Lists improve scannability and LLM parsing', 'hint' => 'Convert 1-2 dense paragraphs into bullet or numbered lists', ); } // No comparisons $has_comparison = preg_match('/\b(vs|versus|compared to|better than|difference between)\b/i', $clean_content); if (!$has_comparison && str_word_count($clean_content) > 500) { $issues[] = array( 'id' => 'no_comparisons', 'severity' => 'low', 'why' => 'Comparisons address common user queries', 'hint' => 'Add a comparison to alternative or related concepts', ); } // No step-by-step content (when appropriate) $has_steps = preg_match('/\b(step\s+\d+|first|second|third|finally)\b/i', $clean_content); $suggests_howto = preg_match('/\b(how to|guide|tutorial)\b/i', $clean_content); if ($suggests_howto && !$has_steps) { $issues[] = array( 'id' => 'missing_steps', 'severity' => 'high', 'why' => 'How-to content without steps misses HowTo schema opportunity', 'hint' => 'Break process into numbered steps with clear actions', ); } // Dense paragraphs preg_match_all('/]*>(.*?)<\/p>/is', $content, $paragraphs); $dense_para_count = 0; foreach ($paragraphs[1] as $para) { $para_words = str_word_count(wp_strip_all_tags($para)); if ($para_words > 100) { $dense_para_count++; } } if ($dense_para_count > 2) { $issues[] = array( 'id' => 'dense_paragraphs', 'severity' => 'medium', 'why' => 'Paragraphs over 100 words reduce scannability', 'hint' => 'Break dense paragraphs into 2-3 shorter ones (40-80 words)', ); } // Missing citations/sources $has_citations = preg_match('/\b(according to|research shows|study found|source:|via)\b/i', $clean_content); if (!$has_citations && str_word_count($clean_content) > 800) { $issues[] = array( 'id' => 'no_citations', 'severity' => 'low', 'why' => 'Citations improve trustworthiness for LLMs', 'hint' => 'Add 1-2 references to authoritative sources or studies', ); } return $issues; } /** * Check link issues * * @param string $content HTML content * @return array Issues */ private function check_link_issues($content) { $issues = array(); // Extract all links preg_match_all('/]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', $content, $links); $link_count = count($links[0]); if ($link_count === 0) { $issues[] = array( 'id' => 'no_links', 'severity' => 'high', 'why' => 'Links signal topical context and authority to LLMs', 'hint' => 'Add 2-3 contextual links (1 internal, 1-2 external authority)', ); return $issues; } // Categorize links $internal_links = 0; $external_links = 0; $current_domain = wp_parse_url(home_url(), PHP_URL_HOST); foreach ($links[1] as $url) { $link_domain = wp_parse_url($url, PHP_URL_HOST); if ($link_domain === $current_domain || strpos($url, '/') === 0) { $internal_links++; } else { $external_links++; } } // No internal deep links if ($internal_links === 0) { $issues[] = array( 'id' => 'no_internal_links', 'severity' => 'medium', 'why' => 'Internal links to cornerstone content strengthen topical authority', 'hint' => 'Link to 1-2 related internal posts or pillar pages', ); } // No external authority links if ($external_links === 0) { $issues[] = array( 'id' => 'no_external_links', 'severity' => 'medium', 'why' => 'External links to authority sites signal research depth', 'hint' => 'Add 1-2 links to authoritative sources (.edu, .gov, industry leaders)', ); } return $issues; } /** * Check schema opportunities * * @param string $content HTML content * @param string $clean_content Clean text * @param string $title Post title * @return array Issues */ private function check_schema_opportunities($content, $clean_content, $title) { $issues = array(); // FAQ opportunity $question_count = substr_count($clean_content, '?'); if ($question_count > 2) { preg_match_all('/]*>.*?\?.*?<\/h[3-6]>/is', $content, $faq_headers); if (count($faq_headers[0]) > 1) { $issues[] = array( 'id' => 'faq_schema_opportunity', 'severity' => 'low', 'why' => 'Q&A patterns can be structured as FAQPage schema', 'hint' => 'Ensure questions are in H3 tags with answers in following paragraphs', ); } } // HowTo opportunity $has_steps = preg_match('/\b(step\s+\d+|first,?\s+|then,?\s+|finally,?\s+)/i', $clean_content); $is_howto_title = preg_match('/\b(how to|guide|tutorial)\b/i', $title); if ($has_steps || $is_howto_title) { $issues[] = array( 'id' => 'howto_schema_opportunity', 'severity' => 'low', 'why' => 'Step-by-step content can be structured as HowTo schema', 'hint' => 'Use numbered lists or clear step markers for HowTo schema', ); } // Product review opportunity $has_product_signals = preg_match('/\b(review|rating|price|buy|purchase|features|specs)\b/i', $clean_content); if ($has_product_signals) { $issues[] = array( 'id' => 'product_schema_opportunity', 'severity' => 'low', 'why' => 'Product-related content benefits from Product/Review schema', 'hint' => 'Add explicit rating, price, or feature sections for schema', ); } return $issues; } /** * Check meta description issues * * @param string $meta_description Meta description * @param string $title Post title * @return array Issues */ private function check_meta_issues($meta_description, $title) { $issues = array(); $meta_length = strlen($meta_description); // Too short or too long if ($meta_length < 120) { $issues[] = array( 'id' => 'meta_too_short', 'severity' => 'medium', 'why' => 'Short meta descriptions miss opportunity for context', 'hint' => 'Expand to 140-160 characters with key entities and benefit', ); } elseif ($meta_length > 165) { $issues[] = array( 'id' => 'meta_too_long', 'severity' => 'medium', 'why' => 'Long meta descriptions get truncated in search results', 'hint' => 'Trim to 155-160 characters, front-loading key information', ); } // Doesn't end with period if (!preg_match('/[.!]$/', trim($meta_description))) { $issues[] = array( 'id' => 'meta_no_period', 'severity' => 'low', 'why' => 'Meta descriptions should be complete sentences', 'hint' => 'End with a period for grammatical completeness', ); } // Missing topic/entity from title $title_words = str_word_count($title, 1); $important_words = array_filter($title_words, function($word) { return strlen($word) > 3 && !in_array(strtolower($word), array('the', 'and', 'for', 'with')); }); $has_title_entity = false; foreach ($important_words as $word) { if (stripos($meta_description, $word) !== false) { $has_title_entity = true; break; } } if (!$has_title_entity) { $issues[] = array( 'id' => 'meta_missing_topic', 'severity' => 'high', 'why' => 'Meta should reference main topic/entity from title', 'hint' => 'Include primary topic or entity from title in meta', ); } return $issues; } /** * Calculate content signals * * @param string $content HTML content * @param string $clean_content Clean text * @param array $breakdown AEO breakdown scores * @return array Signals */ private function calculate_signals($content, $clean_content, $breakdown = array()) { $word_count = str_word_count($clean_content); $read_time_min = ceil($word_count / 200); // Check for dense paragraphs preg_match_all('/]*>(.*?)<\/p>/is', $content, $paragraphs); $dense_paras = false; foreach ($paragraphs[1] as $para) { $para_words = str_word_count(wp_strip_all_tags($para)); if ($para_words > 100) { $dense_paras = true; break; } } // Extract header count preg_match_all('/]*>/i', $content, $headers); $header_count = count($headers[0]); // Link count preg_match_all('/]+href=/i', $content, $links); $link_count = count($links[0]); // List count preg_match_all('/<(?:ul|ol)[^>]*>/i', $content, $lists); $list_count = count($lists[0]); $signals = array( 'read_time_min' => $read_time_min, 'words' => $word_count, 'dense_paras' => $dense_paras, 'header_count' => $header_count, 'link_count' => $link_count, 'list_count' => $list_count, 'avg_para_length' => count($paragraphs[1]) > 0 ? $word_count / count($paragraphs[1]) : 0, ); // Add AEO scores if available if (!empty($breakdown)) { $signals['aeo_breakdown'] = $breakdown; } return $signals; } }