// ================================================================================= // PASTE THIS ENTIRE CLASS TO REPLACE THE EXISTING AICE_Processor CLASS // ================================================================================= class AICE_Processor { private $post_id, $post, $options, $language_code, $language_name, $site_domain; private static $location_map = [ 'ar' => 2784, 'bs' => 2070, 'bg' => 2100, 'zh-hans' => 2156, 'hr' => 2191, 'cs' => 2203, 'da' => 2208, 'nl' => 2528, 'en' => 2840, 'et' => 2233, 'fi' => 2246, 'fr' => 2250, 'de' => 2276, 'el' => 2300, 'he' => 2376, 'hu' => 2348, 'is' => 2352, 'id' => 2360, 'ga' => 2372, 'it' => 2380, 'ja' => 2392, 'ko' => 2410, 'ku' => 2368, 'lv' => 2428, 'lt' => 2440, 'mk' => 2807, 'ms' => 2458, 'ne' => 2524, 'no' => 2578, 'pl' => 2616, 'pt-pt' => 2620, 'ro' => 2642, 'ru' => 2643, 'sr' => 2688, 'sl' => 2705, 'es' => 2724, 'sv' => 2752, 'th' => 2764, 'tr' => 2792, 'uk' => 2804, 'vi' => 2704, 'hi' => 2356 ]; public function __construct($post_id) { $this->post_id = $post_id; $this->options = get_option('wp_aice_settings', []); $this->site_domain = str_replace(['http://', 'https://', 'www.'], '', get_site_url()); } public function process() { try { $this->post = get_post($this->post_id); if (!$this->post) throw new Exception("Post object could not be retrieved."); update_post_meta($this->post_id, '_enrichment_status', 'processing'); if (!function_exists('apply_filters') || !defined('ICL_SITEPRESS_VERSION')) throw new Exception("WPML is not active."); $lang_details = apply_filters('wpml_post_language_details', null, $this->post_id); if (empty($lang_details['language_code'])) throw new Exception("Could not determine post language via WPML."); $this->language_code = $lang_details['language_code']; $this->language_name = $lang_details['display_name']; AICE_Logger::log("START: Processing Post ID: {$this->post_id} in {$this->language_name} ({$this->language_code})"); $this->post->post_content = $this->remove_duplicate_featured_image($this->post->post_content); $link_counts = $this->count_existing_links($this->post->post_content); AICE_Logger::log("Pre-flight check: Found {$link_counts['internal']} internal links and {$link_counts['external']} external links."); $dynamic_max_internal = ($link_counts['internal'] > 0) ? 1 : intval($this->options['max_internal_links'] ?? 2); $dynamic_max_external = ($link_counts['external'] > 0) ? 0 : intval($this->options['max_external_links'] ?? 1); AICE_Logger::log("Dynamic link targets for this run: Internal={$dynamic_max_internal}, External={$dynamic_max_external}."); $analysis_result = $this->analyze_and_source_links($dynamic_max_internal, $dynamic_max_external); $curated_internal_links = $analysis_result['selected_internal_links']; $sourced_external_urls = $this->get_external_urls_from_keywords($analysis_result['external_link_keywords']); $integration_result = $this->integrate_and_rewrite($curated_internal_links, $sourced_external_urls); $update_args = [ 'ID' => $this->post_id, 'post_content' => $integration_result['rewritten_content'], 'post_category' => wp_get_post_categories($this->post_id, ['fields' => 'ids']), ]; wp_update_post($update_args); AICE_Logger::log("Post content and categories updated for ID: {$this->post_id}"); $this->assemble_and_set_tags($integration_result['suggested_tags']); if (!empty($this->options['meta_enabled'])) $this->save_meta_tags($integration_result['meta_title'], $integration_result['meta_description']); $this->ping_indexnow(); update_post_meta($this->post_id, '_enrichment_status', 'complete'); update_post_meta($this->post_id, '_enrichment_date', current_time('mysql')); AICE_Logger::log("SUCCESS: Completed all enrichment for Post ID: {$this->post_id}"); } catch (Exception $e) { AICE_Logger::log("FATAL ERROR processing Post ID {$this->post_id}: " . $e->getMessage()); update_post_meta($this->post_id, '_enrichment_status', 'failed'); } } private function count_existing_links($content) { $internal_count = 0; $external_count = 0; preg_match_all('/]*href=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $matches); if (!empty($matches[1])) { foreach ($matches[1] as $url) { if (strpos($url, $this->site_domain) !== false || substr($url, 0, 1) === '/') { $internal_count++; } else { $external_count++; } } } return ['internal' => $internal_count, 'external' => $external_count]; } private function remove_duplicate_featured_image($content) { if (!has_post_thumbnail($this->post_id)) return $content; $thumbnail_url = get_the_post_thumbnail_url($this->post_id, 'full'); if (!$thumbnail_url) return $content; $pattern = '/^(\s*]*>)?\s*]+src\s*=\s*["\']' . preg_quote($thumbnail_url, '/') . '["\'][^>]*>\s*(<\/p>\s*)?/i'; if (preg_match($pattern, $content)) { AICE_Logger::log("STAGE 0: Found and removed duplicate featured image from post content."); return preg_replace($pattern, '', $content, 1); } return $content; } private function analyze_and_source_links($max_internal, $max_external) { AICE_Logger::log("STAGE 1: AI Analysis - Curating internal links and sourcing external link topics."); $candidate_links = []; if ($max_internal > 0) { // --- Get other Posts/Pages in the same language --- $post_args = [ 'post_type' => ['post', 'page'], 'post_status' => 'publish', 'posts_per_page' => 10, 'orderby' => 'rand', 'lang' => $this->language_code, 'suppress_filters' => false, 'post__not_in' => [$this->post_id] ]; $random_posts = get_posts($post_args); foreach($random_posts as $p) { $candidate_links[] = ['title' => $p->post_title, 'url' => get_permalink($p->ID)]; } // *** NEW: Add the language-specific homepage to the candidate list *** AICE_Logger::log("Sourcing language-specific homepage."); $home_url = apply_filters('wpml_home_url', home_url('/')); $home_title = get_bloginfo('name'); if ($home_url && $home_title) { $candidate_links[] = ['title' => "Homepage - " . $home_title, 'url' => $home_url]; AICE_Logger::log("Added homepage '{$home_title}' ({$home_url}) to candidates."); } // *** FIXED: Get Categories for the correct language using WPML's context switcher *** global $sitepress; if ($sitepress) { AICE_Logger::log("Sourcing category links for language: {$this->language_code}."); $original_lang = apply_filters('wpml_current_language', NULL); // Store original language $sitepress->switch_lang($this->language_code, true); // Switch to post's language $cat_args = ['taxonomy' => 'category', 'number' => 5, 'orderby' => 'rand', 'hide_empty' => true]; $random_cats = get_terms($cat_args); $sitepress->switch_lang($original_lang, true); // IMPORTANT: Switch back to original language foreach($random_cats as $c) { $candidate_links[] = ['title' => "Category: " . $c->name, 'url' => get_term_link($c->term_id)]; } } else { AICE_Logger::log("Could not get Sitepress global to switch language for category fetching."); } if (empty($candidate_links)) { AICE_Logger::log("No candidate internal links found."); } else { shuffle($candidate_links); $candidate_links = array_slice($candidate_links, 0, 15); // Increased slice size to give AI more options } } $prompt = "You are an SEO analyst. Your task is to analyze an article and a list of internal links, then identify topics for external links. All work must be in {$this->language_name}.\n\n"; $prompt .= "**ARTICLE CONTEXT:**\nTitle: {$this->post->post_title}\nContent Excerpt: " . mb_substr(wp_strip_all_tags($this->post->post_content), 0, 1000) . "...\n\n"; if (!empty($candidate_links) && $max_internal > 0) { $prompt .= "**TASK 1: Curate Internal Links**\nFrom the following list of candidate internal links, select up to {$max_internal} that are the MOST contextually relevant to the article.\nCandidate Links:\n"; foreach ($candidate_links as $link) { $prompt .= "- Title: {$link['title']}, URL: {$link['url']}\n"; } } else { $prompt .= "**TASK 1: Curate Internal Links**\nNo internal links requested or available to curate.\n"; } if ($max_external > 0) { $prompt .= "\n**TASK 2: Identify External Link Topics**\nBased on the article's content, identify up to {$max_external} topics that would benefit from an external, authoritative link. Provide a short, optimized search keyword for each topic.\n\n"; } else { $prompt .= "\n**TASK 2: Identify External Link Topics**\nNo external links requested.\n"; } $prompt .= "**TASK 3: JSON Output**\nYour entire response MUST be a single, valid JSON object with this exact structure:\n{\n \"selected_internal_links\": [ {\"title\": \"Title of selected link\", \"url\": \"https://example.com/url-of-selected-link\"} ],\n \"external_link_keywords\": [ \"search keyword one\", \"search keyword two\" ]\n}"; $response_json = $this->call_openai($prompt, true); $result = json_decode($response_json, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($result['selected_internal_links']) || !isset($result['external_link_keywords'])) { throw new Exception("AI failed to return valid JSON for analysis stage. Response: " . substr($response_json, 0, 400)); } AICE_Logger::log("AI Analysis complete. Selected " . count($result['selected_internal_links']) . " internal links and identified " . count($result['external_link_keywords']) . " external topics."); return $result; } private function get_external_urls_from_keywords($keywords) { if(empty($keywords)) return []; AICE_Logger::log("STAGE 2: Fetching external URLs from DataForSEO."); $sourced_links = []; foreach($keywords as $keyword) { try { $url = $this->find_best_external_url_for_keyword($keyword); if ($url) { $sourced_links[] = $url; AICE_Logger::log("Sourced external URL for '{$keyword}': {$url}"); } } catch (Exception $e) { AICE_Logger::log("DataForSEO error for keyword '{$keyword}': " . $e->getMessage()); } } return $sourced_links; } private function integrate_and_rewrite($internal_links, $external_links) { AICE_Logger::log("STAGE 3: Starting creative integration and rewrite."); $prompt = "SYSTEM COMMAND: The original article is in '{$this->language_name}'. YOUR ENTIRE RESPONSE MUST BE IN '{$this->language_name}'. THIS IS AN ABSOLUTE REQUIREMENT.\n\n"; $prompt .= "You are an expert SEO content editor. Your task is to perform a improvement of the provided HTML article, integrating new links while preserving existing ones.\n\n"; $prompt .= "**CRITICAL RULE: You MUST preserve all pre-existing `` tags from the original content. Rewrite the text around them, but the original links and their anchor text must remain in your final output.**\n\n"; $prompt .= "**TASK 1: REWRITE AND ENHANCE CONTENT**\n1. Improve the original article in {$this->language_name} for superior readability and engagement.\n2. Improve structure by adding H2/H3 subheadings and using `
    ` or `
      ` lists where appropriate.\n\n"; $prompt .= "**TASK 2: INTEGRATE NEW LINKS**\nYou MUST naturally integrate the following NEW links into your rewritten content. Create a natural, contextually relevant anchor text for each one.\n"; if (!empty($internal_links)) { $prompt .= "**New Internal Links to Insert:**\n"; foreach($internal_links as $link) { $prompt .= "- Title: `".esc_html($link['title'])."` URL: `".esc_html($link['url'])."`\n"; } } if (!empty($external_links)) { $prompt .= "**New External Links to Insert:**\n"; foreach($external_links as $url) { $prompt .= "- URL: `".esc_html($url)."`\n"; } } if (empty($internal_links) && empty($external_links)) { $prompt .= "No new links to integrate.\n"; } $prompt .= "\n**TASK 3: SUGGEST NEW TAGS**\nBased on the rewritten content, suggest up to 5 relevant and specific tags (in {$this->language_name}).\n\n"; $prompt .= "**TASK 4: GENERATE METADATA**\n1. Create a compelling, SEO-optimized meta title (under 60 characters).\n2. Create an engaging meta description (under 160 characters).\n\n"; $prompt .= "**TASK 5: PROVIDE JSON OUTPUT**\nYour response MUST be a single, valid JSON object with this exact structure:\n"; $prompt .= "{\n \"rewritten_content\": \"[The full, rewritten HTML content, containing both the PRESERVED original links and the NEWLY integrated links]\",\n \"suggested_tags\": [\"new tag 1\", \"new tag 2\"],\n \"meta_title\": \"[The generated meta title]\",\n \"meta_description\": \"[The generated meta description]\"\n}\n\n"; $prompt .= "**ORIGINAL HTML CONTENT TO PROCESS:**\n\n" . $this->post->post_content; $response_json = $this->call_openai($prompt, true); $result = json_decode($response_json, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($result['rewritten_content']) || !isset($result['meta_title']) || !isset($result['suggested_tags'])) { throw new Exception("AI returned invalid JSON structure for integration stage. Response: " . substr($response_json, 0, 500)); } AICE_Logger::log("AI integration and rewrite successful."); return $result; } private function assemble_and_set_tags($suggested_tags) { if (!is_array($suggested_tags)) { AICE_Logger::log("Cannot assemble tags: AI did not provide a valid tag array."); return; } $existing_tags = wp_get_post_terms($this->post_id, 'post_tag', ['fields' => 'names']); $combined_tags = array_unique(array_merge($existing_tags, $suggested_tags)); wp_set_post_terms($this->post_id, $combined_tags, 'post_tag', false); AICE_Logger::log("Tags updated. Final list: " . implode(', ', $combined_tags)); } private function find_best_external_url_for_keyword($keyword) { $api_login = $this->options['dataforseo_api_login'] ?? ''; $api_key = $this->options['dataforseo_api_key'] ?? ''; if (empty($api_login) || empty($api_key)) throw new Exception("DataForSEO API credentials not set."); $location_code = self::$location_map[strtolower($this->language_code)] ?? null; if (!$location_code) { AICE_Logger::log("Warning: No location mapping for language code '{$this->language_code}'. Defaulting to US (2840)."); $location_code = 2840; } $post_data = [['keyword' => $keyword, 'language_code' => $this->language_code, 'location_code' => $location_code]]; $response = wp_remote_post('https://api.dataforseo.com/v3/serp/google/organic/live/advanced', ['method' => 'POST', 'timeout' => 90, 'headers' => ['Authorization' => 'Basic ' . base64_encode($api_login . ':' . $api_key), 'Content-Type' => 'application/json'], 'body' => json_encode($post_data)]); if (is_wp_error($response)) throw new Exception("API request failed: " . $response->get_error_message()); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (($data['status_code'] ?? 0) !== 20000 || empty($data['tasks'][0]['result'][0]['items'])) return null; $serp_items = $data['tasks'][0]['result'][0]['items']; $blacklist_str = $this->options['domain_blacklist'] ?? ''; $blacklisted_domains = preg_split("/\r\n|\n|\r/", $blacklist_str); $blacklisted_domains = array_map('trim', $blacklisted_domains); $blacklisted_domains = array_filter($blacklisted_domains); foreach ($serp_items as $item) { if ($item['type'] !== 'organic' || empty($item['url'])) continue; $item_domain = $item['domain'] ?? ''; if (empty($item_domain)) continue; if (strpos($item_domain, $this->site_domain) !== false) continue; foreach ($blacklisted_domains as $blocked_domain) { if ($item_domain === $blocked_domain || str_ends_with($item_domain, '.' . $blocked_domain)) { AICE_Logger::log("Skipping blacklisted external URL: {$item['url']}"); continue 2; } } return $item['url']; } return null; } private function call_openai($prompt, $is_json = false) { $api_key = $this->options['openai_api_key'] ?? ''; $model = $this->options['openai_model'] ?? ''; if (empty($api_key) || empty($model)) throw new Exception("OpenAI API Key or Model not set."); $body = [ 'model' => $model, 'messages' => [['role' => 'user', 'content' => $prompt]], 'temperature' => 0.7 ]; if ($is_json) { $body['response_format'] = ['type' => 'json_object']; } $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [ 'method' => 'POST', 'timeout' => 180, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key ], 'body' => json_encode($body) ]); if (is_wp_error($response)) throw new Exception("OpenAI API request failed: " . $response->get_error_message()); $response_body = wp_remote_retrieve_body($response); $data = json_decode($response_body, true); $content = $data['choices'][0]['message']['content'] ?? null; if (!$content) throw new Exception("OpenAI returned an empty or invalid response."); return $content; } private function save_meta_tags($new_title, $new_desc) { AICE_Logger::log("Saving meta tags."); $new_title = sanitize_text_field($new_title); $new_desc = sanitize_textarea_field($new_desc); if (empty($new_title) || empty($new_desc)) { AICE_Logger::log("Skipping meta save: AI returned empty title or description."); return; } if (is_plugin_active('wordpress-seo/wp-seo.php')) { update_post_meta($this->post_id, '_yoast_wpseo_title', $new_title); update_post_meta($this->post_id, '_yoast_wpseo_metadesc', $new_desc); AICE_Logger::log("Saved meta for Yoast SEO."); } if (is_plugin_active('seo-by-rank-math/rank-math.php')) { update_post_meta($this->post_id, 'rank_math_title', $new_title); update_post_meta($this->post_id, 'rank_math_description', $new_desc); AICE_Logger::log("Saved meta for Rank Math."); } if (is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php') || is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php')) { update_post_meta($this->post_id, '_aioseo_title', $new_title); update_post_meta($this->post_id, '_aioseo_description', $new_desc); AICE_Logger::log("Saved meta for AIO SEO."); } } private function ping_indexnow() { $key = $this->options['indexnow_api_key'] ?? ''; $host = $this->options['indexnow_host'] ?? ''; if (empty($key) || empty($host)) return; $url_to_submit = get_permalink($this->post_id); $api_url = "https://{$host}/indexnow?url=" . urlencode($url_to_submit) . "&key={$key}"; $response = wp_remote_get($api_url, ['timeout' => 15]); if (is_wp_error($response)) { AICE_Logger::log("IndexNow ERROR: " . $response->get_error_message()); } else { AICE_Logger::log("IndexNow ping sent. Status: " . wp_remote_retrieve_response_code($response)); } } }