post_type, ['page', 'post'], true)) return; if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return; if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (function_exists('current_user_can') && !current_user_can('edit_post', $post_id)) return; $mode = get_post_meta($post_id, '_aiwest_custom_hero_mode', true) === 'yes' ? 'custom_hero' : 'standard'; $result = self::normalize($post_id, (string)$post->post_content, [ 'post_title' => (string)$post->post_title, 'post_type' => (string)$post->post_type, 'mode' => $mode, ]); self::record_report($post_id, $result['report']); if ($result['content'] === (string)$post->post_content) return; self::$saving = true; wp_update_post([ 'ID' => $post_id, 'post_content' => $result['content'], ]); self::$saving = false; } public static function normalize($post_id, $content, $context = []) { $post_id = absint($post_id); $context = is_array($context) ? $context : []; $post = $post_id ? get_post($post_id) : null; $title = trim((string)($context['post_title'] ?? ($post ? $post->post_title : ''))); $post_type = sanitize_key((string)($context['post_type'] ?? ($post ? $post->post_type : 'page'))); $mode = sanitize_key((string)($context['mode'] ?? 'standard')); if (!in_array($mode, ['standard', 'custom_hero'], true)) $mode = 'standard'; $theme_title_visible = self::theme_title_is_visible($post_id, $context); $content = (string)$content; $original = $content; $duplicate = false; $removed = ''; $downgraded = 0; $first_before = self::first_heading($content); if ($title !== '' && $first_before && self::titles_match($title, $first_before['text'])) { $duplicate = true; if ($theme_title_visible) { $removed = $first_before['text']; $content = substr_replace($content, '', $first_before['offset'], $first_before['length']); } elseif ($mode === 'custom_hero' && intval($first_before['level']) !== 1) { $content = self::replace_heading_level_at($content, $first_before, 1); } } if ($theme_title_visible && $mode === 'standard') { $content = self::downgrade_body_h1s($content, $downgraded); } $first_after = self::first_heading($content); $manual_action = ($mode === 'custom_hero' && $theme_title_visible && self::contains_h1($content)); $report = [ 'duplicate_title_detected' => $duplicate, 'page_title' => wp_strip_all_tags($title), 'removed_duplicated_heading' => $removed, 'first_content_heading_before_normalization' => $first_before ? $first_before['text'] : '', 'first_content_heading_after_normalization' => $first_after ? $first_after['text'] : '', 'theme_title_visible' => $theme_title_visible, 'theme_title_should_be_hidden' => $manual_action, 'custom_hero_mode' => $mode === 'custom_hero', 'downgraded_h1_count' => $downgraded, 'changed' => $content !== $original, 'message' => $removed !== '' ? 'Duplicate page title heading was removed to prevent repeated H1 output.' : ($manual_action ? 'This page uses a custom hero H1. Hide the automatic theme page title to avoid duplicate headings.' : ''), 'post_type' => $post_type, 'checked_at' => function_exists('current_time') ? current_time('mysql') : gmdate('Y-m-d H:i:s'), ]; return ['content' => $content, 'report' => $report]; } public static function normalize_content($post_id, $content, $context = []) { $result = self::normalize($post_id, $content, $context); return $result['content']; } public static function record_report($post_id, $report) { $post_id = absint($post_id); if (!$post_id || !is_array($report)) return false; return update_post_meta($post_id, self::META_REPORT, $report); } public static function titles_match($page_title, $heading) { $title = self::normalize_title($page_title); $heading = self::normalize_title($heading); if ($title === '' || $heading === '') return false; if ($title === $heading) return true; $aliases = [ 'frequently asked questions' => 'faq', 'frequently asked question' => 'faq', 'questions and answers' => 'faq', ]; $title = $aliases[$title] ?? $title; $heading = $aliases[$heading] ?? $heading; if ($title === $heading) return true; similar_text($title, $heading, $percent); if ($percent >= 86) return true; $shorter = strlen($title) <= strlen($heading) ? $title : $heading; $longer = $shorter === $title ? $heading : $title; if (strlen($shorter) >= 5 && strpos($longer, $shorter) !== false) { $extra = trim(str_replace($shorter, '', $longer)); if ($extra === '' || preg_match('/^(?:page|guide|overview|wordpress|website)$/', $extra)) return true; } return false; } protected static function normalize_title($text) { $text = html_entity_decode(wp_strip_all_tags((string)$text), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = self::lower($text); $text = str_replace(['&', '+'], ' and ', $text); $text = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $text); $text = trim(preg_replace('/\s+/u', ' ', $text)); $brands = ['ai webmaster studio', 'ai webmaster foundation', 'ai webmaster', 'aiwos cloud', 'aiwos']; $site = function_exists('get_bloginfo') ? self::lower(trim(wp_strip_all_tags((string)get_bloginfo('name')))) : ''; if ($site !== '') $brands[] = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $site); $brands = array_unique($brands); usort($brands, function($left, $right) { return strlen($right) <=> strlen($left); }); foreach ($brands as $brand) { $brand = trim($brand); if ($brand === '') continue; $text = preg_replace('/^(?:' . preg_quote($brand, '/') . ')\s+/u', '', $text); $text = preg_replace('/\s+(?:' . preg_quote($brand, '/') . ')$/u', '', $text); } return trim(preg_replace('/\s+/u', ' ', $text)); } protected static function first_heading($content) { $pattern = '/(?:\s*)?]*)>(.*?)<\/h\1>\s*(?:)?/is'; if (!preg_match($pattern, (string)$content, $match, PREG_OFFSET_CAPTURE)) return null; return [ 'level' => intval($match[1][0]), 'attributes' => $match[2][0], 'inner_html' => $match[3][0], 'text' => trim(wp_strip_all_tags($match[3][0])), 'offset' => intval($match[0][1]), 'length' => strlen($match[0][0]), ]; } protected static function replace_heading_level_at($content, $heading, $level) { $replacement = '' . $heading['inner_html'] . ''; return substr_replace($content, $replacement, $heading['offset'], $heading['length']); } protected static function downgrade_body_h1s($content, &$count) { $content = preg_replace_callback( '/(\s*)]*)>(.*?)<\/h1>(\s*)/is', function($match) use (&$count) { $count++; $opening = preg_replace('/"level"\s*:\s*1/', '"level":2', $match[1]); return $opening . '' . $match[3] . '' . $match[4]; }, (string)$content ); return preg_replace_callback('/]*)>(.*?)<\/h1>/is', function($match) use (&$count) { $count++; return '' . $match[2] . ''; }, $content); } protected static function contains_h1($content) { return preg_match('/