post_type, ['page', 'post'], true)) { return new WP_Error('aiwest_links_no_post', __('The page or post to link was not found.', 'ai-webmaster-studio')); } $active_header_id = intval(get_option('aiwest_active_header_page_id', 0)); $active_footer_id = intval(get_option('aiwest_active_footer_page_id', 0)); if ($post_id === $active_header_id || $post_id === $active_footer_id) { return new WP_Error( 'aiwest_links_structural_area', __('This is the active AI Header or AI Footer. Use the dedicated header or footer linking action so structural navigation is not mixed with main-content linking.', 'ai-webmaster-studio') ); } $content = $post->post_content; if (trim(wp_strip_all_tags($content)) === '') { return new WP_Error('aiwest_links_empty', __('The page content is empty.', 'ai-webmaster-studio')); } $targets = self::collect_targets($post_id); if (!$targets) { return new WP_Error('aiwest_links_no_targets', __('No published pages or posts are available as link targets.', 'ai-webmaster-studio')); } $linked = []; $new_content = $content; foreach ($targets as $target) { if (count($linked) >= $limit) break; foreach ($target['phrases'] as $phrase) { if (count($linked) >= $limit) break; $phrase = trim($phrase); if (mb_strlen($phrase) < 4) continue; if (self::content_already_links_to($new_content, $target['url'])) continue; $updated = self::link_first_plain_occurrence($new_content, $phrase, $target['url'], $target['title']); if ($updated !== $new_content) { $new_content = $updated; $linked[] = [ 'phrase' => $phrase, 'post_id' => $target['id'], 'title' => $target['title'], 'url' => $target['url'], ]; break; } } } if ($new_content === $content) { return [ 'changed' => false, 'message' => __('No new safe link locations were found, or the links already exist.', 'ai-webmaster-studio'), 'links' => [], 'post_id' => $post_id, ]; } if (class_exists('AIWEST_Undo')) { AIWEST_Undo::snapshot($post_id, 'internal_links', __('Snapshot before automatic AI Webmaster Studio internal linking.', 'ai-webmaster-studio')); } $heading_normalization = ['content' => $new_content, 'report' => []]; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { $heading_normalization = AIWEST_Page_Title_Heading_Normalizer::normalize($post_id, $new_content, [ 'post_title' => (string)$post->post_title, 'post_type' => (string)$post->post_type, 'mode' => 'standard', ]); $new_content = $heading_normalization['content']; } $result = wp_update_post([ 'ID' => $post_id, 'post_content' => $new_content, ], true); if (is_wp_error($result)) return $result; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { AIWEST_Page_Title_Heading_Normalizer::record_report($post_id, $heading_normalization['report']); } update_post_meta($post_id, '_aiwest_last_internal_links_at', current_time('mysql')); update_post_meta($post_id, '_aiwest_last_internal_links', $linked); if (class_exists('AIWEST_Logger')) { AIWEST_Logger::info('Internal links added.', [ 'post_id' => $post_id, 'count' => count($linked), ]); } return [ 'changed' => true, 'message' => __('Internal links were added without changing the content structure.', 'ai-webmaster-studio'), 'links' => $linked, 'post_id' => $post_id, ]; } public static function collect_targets($exclude_id = 0) { $posts = get_posts([ 'numberposts' => 81, 'post_type' => ['page', 'post'], 'post_status' => 'publish', 'orderby' => 'modified', 'order' => 'DESC', ]); $targets = []; foreach ($posts as $p) { if (intval($p->ID) === intval($exclude_id)) continue; $title = trim(wp_strip_all_tags($p->post_title)); if ($title === '') continue; $phrases = self::phrases_for_post($p); if (!$phrases) continue; $targets[] = [ 'id' => intval($p->ID), 'title' => $title, 'url' => get_permalink($p->ID), 'phrases' => $phrases, ]; } return $targets; } protected static function phrases_for_post($post) { $title = trim(wp_strip_all_tags($post->post_title)); $phrases = [$title]; $normalized = self::normalize_title($title); if ($normalized && $normalized !== $title) $phrases[] = $normalized; if ($post->post_type === 'post') { $cats = wp_get_post_categories($post->ID, ['fields' => 'names']); $tags = wp_get_post_tags($post->ID, ['fields' => 'names']); foreach (array_merge((array)$cats, (array)$tags) as $term) { if (mb_strlen($term) >= 4) $phrases[] = $term; } } $phrases = array_map('trim', $phrases); $phrases = array_filter($phrases, function($v) { return mb_strlen($v) >= 4; }); $phrases = array_values(array_unique($phrases)); usort($phrases, function($a, $b) { return mb_strlen($b) <=> mb_strlen($a); }); return $phrases; } protected static function normalize_title($title) { $title = preg_replace('/\s*[–—-]\s*.*$/u', '', $title); $title = preg_replace('/\b(current|practical|guide|overview|introduction|in 2026|in 2025|in 2024)\b/iu', '', $title); $title = trim(preg_replace('/\s+/', ' ', $title)); return $title; } protected static function content_already_links_to($content, $url) { return stripos($content, 'href="' . esc_url($url) . '"') !== false || stripos($content, "href='" . esc_url($url) . "'") !== false; } protected static function link_first_plain_occurrence($content, $phrase, $url, $title) { $parts = preg_split('/(]*>.*?<\/a>|<[^>]+>)/isu', $content, -1, PREG_SPLIT_DELIM_CAPTURE); if (!$parts || count($parts) === 1) { return self::replace_first_in_text($content, $phrase, $url, $title); } $out = ''; $changed = false; foreach ($parts as $part) { if ($changed) { $out .= $part; continue; } // Skip tags and existing links. if (preg_match('/^]+>$/s', $part)) { $out .= $part; continue; } $new = self::replace_first_in_text($part, $phrase, $url, $title); if ($new !== $part) { $changed = true; $out .= $new; } else { $out .= $part; } } return $changed ? $out : $content; } protected static function replace_first_in_text($text, $phrase, $url, $title) { $pattern = '/(?$1'; return preg_replace($pattern, $replacement, $text, 1); } public static function link_active_footer($create_missing = true) { $post_id = intval(get_option('aiwest_active_footer_page_id', 0)); if (!$post_id) { return new WP_Error('aiwest_footer_no_active', __('No active AI Footer is available for linking.', 'ai-webmaster-studio')); } $post = get_post($post_id); if (!$post || $post->post_type !== 'page') { return new WP_Error('aiwest_footer_no_post', __('The active AI Footer page was not found.', 'ai-webmaster-studio')); } $content = $post->post_content; $linked = []; $created = []; $new_content = preg_replace_callback('/
' . esc_html(__('This page was created as an AI Webmaster Studio draft to complete internal linking. Expand it in Content Development before publishing.', 'ai-webmaster-studio')) . '
'; $heading_normalization = ['content' => $placeholder_content, 'report' => []]; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { $heading_normalization = AIWEST_Page_Title_Heading_Normalizer::normalize(0, $placeholder_content, [ 'post_title' => $target_title, 'post_type' => 'page', 'mode' => 'standard', 'theme_title_visible' => true, ]); $placeholder_content = $heading_normalization['content']; } $new_id = wp_insert_post([ 'post_title' => $target_title, 'post_name' => sanitize_title($target_title), 'post_type' => 'page', 'post_status' => 'draft', 'post_content' => $placeholder_content, 'post_author' => class_exists('AIWEST_Technical_Operator') ? AIWEST_Technical_Operator::post_author_id(get_current_user_id()) : (get_current_user_id() ?: 1), ], true); if (is_wp_error($new_id)) return $new_id; update_post_meta($new_id, '_aiwest_generated', 'yes'); update_post_meta($new_id, '_aiwest_created_for_internal_link', 'yes'); if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { AIWEST_Page_Title_Heading_Normalizer::record_report($new_id, $heading_normalization['report']); } if (class_exists('AIWEST_Technical_Operator')) { AIWEST_Technical_Operator::annotate_post($new_id, 'create_internal_link_target', AIWEST_Technical_Operator::explain_decision('create_page', 'content_page_create', ['post_type'=>'page','post_status'=>'draft'])); } return [ 'id' => intval($new_id), 'title' => get_the_title($new_id), 'url' => get_permalink($new_id), 'created' => true, ]; } protected static function find_post_by_title_or_slug($title) { $title = trim(wp_strip_all_tags($title)); if ($title === '') return null; $slug = sanitize_title($title); $by_slug = get_page_by_path($slug, OBJECT, ['page', 'post']); if ($by_slug && $by_slug->post_status !== 'trash') return $by_slug; $posts = get_posts([ 'numberposts' => 1, 'post_type' => ['page', 'post'], 'post_status' => ['publish', 'draft', 'pending', 'private'], 'title' => $title, ]); if ($posts) return $posts[0]; // Fallback: loose search by title. $search = get_posts([ 'numberposts' => 8, 'post_type' => ['page', 'post'], 'post_status' => ['publish', 'draft', 'pending', 'private'], 's' => $title, ]); foreach ($search as $p) { if (stripos(get_the_title($p), $title) !== false || stripos($title, get_the_title($p)) !== false) { return $p; } } return null; } }