post_type, ['page','post'], true)) return; if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return; self::$finalizing = true; self::finalize($post_id, (string)$post->post_content, ['post_type'=>$post->post_type,'post_title'=>$post->post_title]); self::$finalizing = false; } public static function prompt_contract($context = []) { $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); return "SEO AND GEO CONTRACT (mandatory):\n" . "- Satisfy the page's real user intent with accurate, useful, non-commodity content.\n" . "- Write a concise introduction, then use a logical H2/H3 hierarchy. Never add an H1 inside page content.\n" . "- Produce a truthful SEO title and meta description that match the visible content; avoid keyword stuffing.\n" . "- Make entities, product names, relationships, dates and definitions explicit and internally consistent.\n" . "- Make important answers easy to identify through descriptive headings, steps, lists, tables or FAQ when genuinely useful.\n" . "- Never invent statistics, quotations, customer claims, citations or external sources.\n" . "- Internal links must use verified existing URLs on {$host}; never use chatgpt.com, localhost or example.com as internal destinations.\n" . "- Structured information must agree with the visible content and the administrator's project data.\n" . "- Optimize for people first; do not create repetitive query variations or search-engine-only text."; } public static function normalize($post_id, $content, $context = []) { $title = (string)($context['post_title'] ?? ($post_id ? get_the_title($post_id) : '')); $post_type = (string)($context['post_type'] ?? ($post_id ? get_post_type($post_id) : 'page')); $normalized = ['content' => (string)$content, 'report' => []]; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { $normalized = AIWEST_Page_Title_Heading_Normalizer::normalize($post_id, (string)$content, [ 'post_title' => $title, 'post_type' => $post_type, 'mode' => !empty($context['custom_hero']) ? 'custom_hero' : 'standard', 'theme_title_visible' => empty($context['theme_title_hidden']), ]); } $normalized['report'] = self::analyze($title, $normalized['content'], $context, $normalized['report']); return $normalized; } public static function finalize($post_id, $content = null, $context = [], $provided = []) { $post = get_post($post_id); if (!$post) return []; $content = $content === null ? (string)$post->post_content : (string)$content; $title = (string)($context['post_title'] ?? $post->post_title); $report = self::analyze($title, $content, $context); $meta_title = sanitize_text_field($provided['meta_title'] ?? get_post_meta($post_id, '_aiwest_meta_title', true)); if ($meta_title === '') $meta_title = $title; $description = sanitize_textarea_field($provided['meta_description'] ?? get_post_meta($post_id, '_aiwest_meta_description', true)); if (preg_match('/\[(?:\/)?aiwest_[a-z0-9_-]*/i', $description)) $description = ''; if ($description === '') $description = self::description($post->post_excerpt ?: $content); update_post_meta($post_id, '_aiwest_meta_title', $meta_title); update_post_meta($post_id, '_aiwest_meta_description', $description); update_post_meta($post_id, self::REPORT_META, $report); update_post_meta($post_id, '_aiwest_search_optimized_at', current_time('mysql')); return $report; } public static function analyze($title, $content, $context = [], $heading_report = []) { $plain = trim(preg_replace('/\s+/u', ' ', html_entity_decode(wp_strip_all_tags((string)$content, true), ENT_QUOTES, 'UTF-8'))); preg_match_all('/]*>(.*?)<\/h\1>/is', $content, $headings, PREG_SET_ORDER); preg_match_all('/]*\bhref\s*=\s*(["\'])(.*?)\1/is', $content, $links, PREG_SET_ORDER); $internal = 0; $external = 0; $invalid = []; $home_host = strtolower((string)wp_parse_url(home_url('/'), PHP_URL_HOST)); foreach ($links as $link) { $url = html_entity_decode(trim($link[2]), ENT_QUOTES, 'UTF-8'); if ($url === '' || $url[0] === '#' || stripos($url, 'mailto:') === 0 || stripos($url, 'tel:') === 0) continue; $host = strtolower((string)wp_parse_url($url, PHP_URL_HOST)); if ($host === '' || $host === $home_host) $internal++; else $external++; if ($home_host === 'aiwebmaster.cloud' && in_array($host, ['chatgpt.com','www.chatgpt.com','localhost','example.com','www.example.com'], true)) $invalid[] = $url; } $h1_count = preg_match_all('/]*>[^<]*(?:\?|faq|how|what|why|when|where|who)\b.*?<\/h[2-4]>/iu', $content); $structured = preg_match('/<(?:ul|ol|table|details)\b/i', $content) === 1; $word_count = self::word_count($plain); $warnings = []; if ($word_count < 120) $warnings[] = 'Content may be too short to satisfy the topic completely.'; if ($h1_count > 0) $warnings[] = 'Page content contains an H1; the WordPress title should normally remain the only H1.'; if ($h2_count < 1 && $word_count > 250) $warnings[] = 'Long content has no H2 sections.'; if ($internal < 1 && $word_count > 250) $warnings[] = 'No internal contextual link was detected.'; if ($invalid) $warnings[] = 'A prohibited internal-link destination was detected.'; $seo = 35 + min(20, intval($word_count / 50)) + ($h2_count ? 15 : 0) + ($internal ? 15 : 0) + ($h1_count === 0 ? 15 : 0); $geo = 30 + min(20, intval($word_count / 60)) + ($structured ? 15 : 0) + ($question_count ? 10 : 0) + ($external ? 10 : 0) + ($h2_count ? 15 : 0); if ($invalid) { $seo -= 20; $geo -= 20; } return [ 'contract' => 'ADL-028', 'checked_at' => current_time('mysql'), 'seo_score' => max(0, min(100, $seo)), 'geo_readiness_score' => max(0, min(100, $geo)), 'word_count' => $word_count, 'h1_count' => $h1_count, 'h2_count' => $h2_count, 'internal_links' => $internal, 'external_sources' => $external, 'question_headings' => $question_count, 'structured_content' => $structured, 'schema_type' => self::schema_type($context, $content), 'indexability' => 'review WordPress and SEO-plugin settings', 'prohibited_link_targets' => array_values(array_unique($invalid)), 'duplicate_title_heading' => $heading_report, 'warnings' => $warnings, 'passed' => empty($warnings), ]; } public static function output_search_metadata() { if (!is_singular() || self::seo_plugin_active()) return; $post_id = get_queried_object_id(); if (!$post_id) return; $description = trim((string)get_post_meta($post_id, '_aiwest_meta_description', true)); if ($description !== '') echo '' . "\n"; $schema = self::schema($post_id, $description); if ($schema) { $json = wp_json_encode( $schema, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if (is_string($json)) { wp_print_inline_script_tag($json, ['type' => 'application/ld+json']); } } } protected static function schema($post_id, $description) { $post = get_post($post_id); if (!$post) return []; $report = get_post_meta($post_id, self::REPORT_META, true); $type = is_array($report) ? (string)($report['schema_type'] ?? 'WebPage') : 'WebPage'; $schema = [ '@context' => 'https://schema.org', '@type' => in_array($type, ['WebPage','Article','FAQPage'], true) ? $type : 'WebPage', 'name' => (string)$post->post_title, 'url' => get_permalink($post_id), ]; if ($description !== '') $schema['description'] = $description; if ($schema['@type'] === 'Article') { $schema['datePublished'] = get_post_time('c', true, $post); $schema['dateModified'] = get_post_modified_time('c', true, $post); } elseif ($schema['@type'] === 'FAQPage') { $entities = self::faq_entities((string)$post->post_content); if ($entities) $schema['mainEntity'] = $entities; else $schema['@type'] = 'WebPage'; } return $schema; } protected static function faq_entities($content) { preg_match_all('/]*>\s*]*>(.*?)<\/summary>(.*?)<\/details>/is', $content, $matches, PREG_SET_ORDER); $entities = []; foreach ($matches as $match) { $question = trim(wp_strip_all_tags($match[1])); $answer = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags($match[2]))); if ($question === '' || $answer === '') continue; $entities[] = ['@type'=>'Question','name'=>$question,'acceptedAnswer'=>['@type'=>'Answer','text'=>$answer]]; } return array_slice($entities, 0, 50); } protected static function schema_type($context, $content) { if (preg_match('/