settings = AEO_Bob_Settings::get_instance(); } add_action('add_meta_boxes', array($this, 'add_meta_box')); add_action('wp_ajax_aeo_bob_analyze_content', array($this, 'ajax_analyze_content')); add_action('wp_ajax_aeo_bob_get_seo_suggestions', array($this, 'ajax_get_seo_suggestions')); } public function add_meta_box() { add_meta_box( 'aeo-bob-seo-analysis', '🚀 AEO BOB - SEO Analysis', array($this, 'render_meta_box'), array('post', 'page'), 'side', 'high' // Changed back to 'high' for top positioning ); } public function render_meta_box($post) { // Nonce verification happens via AJAX with check_ajax_referer() instead // Removed wp_nonce_field() to prevent duplicate #_ajax_nonce warnings $target_keyword = get_post_meta($post->ID, '_aeo_bob_target_keyword', true); $seo_score = get_post_meta($post->ID, '_aeo_bob_seo_score', true); $aeo_score = get_post_meta($post->ID, '_aeo_bob_aeo_score', true) ?: 0; $last_analysis = get_post_meta($post->ID, '_aeo_bob_last_analysis', true); // Always calculate fresh score for current post $current_content = get_post_field('post_content', $post->ID); $current_title = get_post_field('post_title', $post->ID); $seo_score = $this->calculate_initial_score($post); update_post_meta($post->ID, '_aeo_bob_seo_score', $seo_score); ?>
/100
/100
analyze_content($content, $title, $post_id); // Save analysis results for current post update_post_meta($post_id, '_aeo_bob_seo_score', $analysis['seo_score']); update_post_meta($post_id, '_aeo_bob_last_analysis', current_time('mysql')); // Calculate and update AEO score if (class_exists('AEO_Bob_AEO_Analyzer')) { $aeo_analyzer = AEO_Bob_AEO_Analyzer::get_instance(); $aeo_score = $aeo_analyzer->calculate_aeo_score($post_id); update_post_meta($post_id, '_aeo_bob_aeo_score', $aeo_score); } update_post_meta($post_id, '_aeo_bob_analysis_data', $analysis); update_post_meta($post_id, '_aeo_bob_word_count', $analysis['word_count']); update_post_meta($post_id, '_aeo_bob_headings_count', $analysis['headings_count']); update_post_meta($post_id, '_aeo_bob_images_count', $analysis['images_count']); update_post_meta($post_id, '_aeo_bob_links_count', $analysis['links_count']); wp_send_json_success($analysis); } public function ajax_get_seo_suggestions() { check_ajax_referer('aeo_bob_seo_nonce', 'nonce'); if (!current_user_can('edit_posts')) { wp_die('Insufficient permissions'); } $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0; if (!$post_id) { wp_die('Invalid post ID'); } $target_keyword = get_post_meta($post_id, '_aeo_bob_target_keyword', true); $analysis_data = get_post_meta($post_id, '_aeo_bob_analysis_data', true); $suggestions = $this->generate_seo_suggestions($target_keyword, $analysis_data); wp_send_json_success(array( 'suggestions' => $suggestions )); } private function analyze_content($content, $title, $post_id) { $target_keyword = get_post_meta($post_id, '_aeo_bob_target_keyword', true); // Basic content analysis $word_count = str_word_count(wp_strip_all_tags($content)); $headings_count = preg_match_all('/]*>/', $content); $images_count = preg_match_all('/]+>/', $content); $links_count = preg_match_all('/]+>/', $content); // Calculate SEO score $score = $this->calculate_seo_score($content, $title, $target_keyword); return array( 'seo_score' => $score, 'word_count' => $word_count, 'headings_count' => $headings_count, 'images_count' => $images_count, 'links_count' => $links_count, 'target_keyword' => $target_keyword, 'keyword_density' => $this->calculate_keyword_density($content, $target_keyword), 'readability_score' => $this->calculate_readability_score($content) ); } private function calculate_seo_score($content, $title, $target_keyword) { $score = 0; $clean_content = wp_strip_all_tags($content); $word_count = str_word_count($clean_content); // Content length (20 points max) if ($word_count >= 300) $score += 20; elseif ($word_count >= 150) $score += 15; elseif ($word_count >= 100) $score += 10; elseif ($word_count >= 50) $score += 5; // Title optimization (15 points max) if ($target_keyword && stripos($title, $target_keyword) !== false) { $score += 15; } elseif (strlen($title) >= 30 && strlen($title) <= 60) { $score += 10; } elseif (strlen($title) > 0) { $score += 5; } // Keyword usage (20 points max) if ($target_keyword) { $keyword_count = substr_count(strtolower($clean_content), strtolower($target_keyword)); $density = ($word_count > 0) ? ($keyword_count / $word_count) * 100 : 0; if ($density >= 1 && $density <= 3) { $score += 20; } elseif ($density > 0) { $score += 10; } } else { // If no keyword, give some points for having content if ($word_count > 0) $score += 5; } // Content structure (15 points max) if (preg_match('/]*>/', $content)) $score += 8; if (preg_match('/]+>/', $content)) $score += 7; // Internal/external links (10 points max) if (preg_match('/]+>/', $content)) $score += 10; // Content quality indicators (10 points max) if ($word_count > 500) $score += 5; if (preg_match('/\b(what|how|why|when|where)\b/i', $content)) $score += 5; // Ensure minimum score for any content if ($word_count > 0) { $score = max($score, 10); // Minimum 10 points for having content } return min(100, max(0, $score)); } private function calculate_keyword_density($content, $keyword) { if (!$keyword) return 0; $clean_content = wp_strip_all_tags($content); $word_count = str_word_count($clean_content); $keyword_count = substr_count(strtolower($clean_content), strtolower($keyword)); return $word_count > 0 ? round(($keyword_count / $word_count) * 100, 2) : 0; } private function calculate_readability_score($content) { $clean_content = wp_strip_all_tags($content); $sentences = preg_split('/[.!?]+/', $clean_content); $words = str_word_count($clean_content); $sentence_count = count(array_filter($sentences)); if ($sentence_count === 0) return 0; $avg_sentence_length = $words / $sentence_count; // Simple readability score (higher is better) if ($avg_sentence_length <= 15) return 90; elseif ($avg_sentence_length <= 20) return 80; elseif ($avg_sentence_length <= 25) return 70; else return 60; } private function generate_seo_suggestions($target_keyword, $analysis_data) { $provider = aeo_bob_provider(); $api_key = aeo_bob_api_key_for($provider); $model = function_exists('aeo_bob_model') ? aeo_bob_model() : aeo_bob_get('api_model', 'gpt-4o-mini'); if (function_exists('aeo_bob_model_alias')) { $model = aeo_bob_model_alias($provider, $model); } if (empty($api_key)) { /* translators: %s: API provider name (e.g., OpenAI, Anthropic) */ return new WP_Error('aeo_bob_missing_key', sprintf(__('API key for %s is not set. Go to AEO BOB → Settings to add it.', 'aeo-bob-lite'), esc_html($provider))); } $word_count = $analysis_data['word_count'] ?? 0; $headings_count = $analysis_data['headings_count'] ?? 0; $images_count = $analysis_data['images_count'] ?? 0; $keyword_density = $analysis_data['keyword_density'] ?? 0; $seo_score = $analysis_data['seo_score'] ?? 0; $prompt = "You are an expert SEO analyst. Based on the following content analysis, provide 5-7 specific, actionable SEO improvement suggestions.\n\n"; $prompt .= "CONTENT ANALYSIS:\n"; $prompt .= "- Word Count: {$word_count}\n"; $prompt .= "- Headings Count: {$headings_count}\n"; $prompt .= "- Images Count: {$images_count}\n"; $prompt .= "- Keyword Density: {$keyword_density}%\n"; $prompt .= "- SEO Score: {$seo_score}/100\n"; $prompt .= "- Target Keyword: " . ($target_keyword ?: 'Not set') . "\n\n"; if (!$target_keyword) { $prompt .= "The target keyword is not set. Provide general content improvement suggestions.\n\n"; } $prompt .= "Provide your suggestions as a numbered list. Each suggestion should be specific and actionable.\n"; $prompt .= "Focus on:\n"; $prompt .= "- Content length and quality\n"; $prompt .= "- Keyword optimization\n"; $prompt .= "- Content structure and headings\n"; $prompt .= "- Visual elements and media\n"; $prompt .= "- Internal and external linking\n"; $prompt .= "- Meta descriptions and titles\n\n"; $prompt .= "Format: 1. [Specific suggestion]\n2. [Specific suggestion]\n..."; $response = $this->call_ai_api($provider, $api_key, $model, $prompt); if ($response) { $ai_response = ''; // Parse response based on provider switch ($provider) { case 'openai': case 'perplexity': if (isset($response['choices'][0]['message']['content'])) { $ai_response = $response['choices'][0]['message']['content']; } break; case 'anthropic': if (isset($response['content'][0]['text'])) { $ai_response = $response['content'][0]['text']; } break; case 'google': if (isset($response['candidates'][0]['content']['parts'][0]['text'])) { $ai_response = $response['candidates'][0]['content']['parts'][0]['text']; } break; case 'cohere': if (isset($response['text'])) { $ai_response = $response['text']; } break; } if (!empty($ai_response)) { return $this->parse_seo_suggestions_response($ai_response); } } // Fallback to mock suggestions if API fails return $this->generate_mock_seo_suggestions($target_keyword, $analysis_data); } /** * Call AI API for the selected provider */ private function call_ai_api($provider, $api_key, $model, $prompt) { $headers = array('Content-Type' => 'application/json'); $body = array(); switch ($provider) { case 'openai': $endpoint = 'https://api.openai.com/v1/chat/completions'; $headers['Authorization'] = 'Bearer ' . $api_key; $body = array( 'model' => $model, 'messages' => array( array('role' => 'user', 'content' => $prompt) ), 'max_tokens' => 1000, 'temperature' => 0.7 ); break; case 'anthropic': $endpoint = 'https://api.anthropic.com/v1/messages'; $headers['x-api-key'] = $api_key; $headers['anthropic-version'] = '2023-06-01'; $body = array( 'model' => $model, 'max_tokens' => 1000, 'messages' => array( array('role' => 'user', 'content' => $prompt) ) ); break; case 'google': $endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $model . ':generateContent?key=' . rawurlencode($api_key); $body = array( 'contents' => array( array( 'parts' => array( array('text' => $prompt) ) ) ), 'generationConfig' => array( 'maxOutputTokens' => 1000, 'temperature' => 0.7 ) ); break; case 'perplexity': $endpoint = 'https://api.perplexity.ai/chat/completions'; $headers['Authorization'] = 'Bearer ' . $api_key; $body = array( 'model' => $model, 'messages' => array( array('role' => 'user', 'content' => $prompt) ), 'max_tokens' => 1000, 'temperature' => 0.7 ); break; case 'cohere': $endpoint = 'https://api.cohere.ai/v1/chat'; $headers['Authorization'] = 'Bearer ' . $api_key; $body = array( 'model' => $model, 'message' => $prompt, 'max_tokens' => 1000, 'temperature' => 0.7 ); break; default: $endpoint = 'https://api.openai.com/v1/chat/completions'; $headers['Authorization'] = 'Bearer ' . $api_key; $body = array( 'model' => $model, 'messages' => array( array('role' => 'user', 'content' => $prompt) ), 'max_tokens' => 1000, 'temperature' => 0.7 ); } $response = wp_remote_post($endpoint, array( 'headers' => $headers, 'body' => json_encode($body), 'timeout' => 60 )); if (is_wp_error($response)) { // Debug logging removed for production return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['error'])) { // Debug logging removed for production return false; } return $data; } /** * Parse SEO suggestions from AI response */ private function parse_seo_suggestions_response($ai_response) { $suggestions = array(); // Extract numbered suggestions preg_match_all('/^\d+\.\s*(.+)$/m', $ai_response, $matches); if (isset($matches[1])) { $suggestions = array_map('trim', $matches[1]); } // If no numbered suggestions found, try to extract bullet points if (empty($suggestions)) { preg_match_all('/^[-*]\s*(.+)$/m', $ai_response, $matches); if (isset($matches[1])) { $suggestions = array_map('trim', $matches[1]); } } // If still no suggestions, split by lines and clean up if (empty($suggestions)) { $lines = explode("\n", $ai_response); foreach ($lines as $line) { $line = trim($line); if (strlen($line) > 20 && !preg_match('/^(prompt|content|keyword|analysis|word|heading|image|density|score)/i', $line)) { $suggestions[] = $line; } } } // Limit to 7 suggestions and ensure they're not empty $suggestions = array_filter($suggestions, function($s) { return !empty($s); }); return array_slice($suggestions, 0, 7); } /** * Generate mock SEO suggestions (fallback) */ private function generate_mock_seo_suggestions($target_keyword, $analysis_data) { $suggestions = array(); $word_count = $analysis_data['word_count'] ?? 0; $headings_count = $analysis_data['headings_count'] ?? 0; $images_count = $analysis_data['images_count'] ?? 0; $keyword_density = $analysis_data['keyword_density'] ?? 0; $seo_score = $analysis_data['seo_score'] ?? 0; // If no keyword is set, provide general content suggestions if (!$target_keyword) { if ($word_count < 300) { $suggestions[] = 'Add more content to reach at least 300 words for better SEO performance'; } if ($headings_count < 2) { $suggestions[] = 'Add more headings (H2, H3) to improve content structure and readability'; } if ($images_count < 1) { $suggestions[] = 'Add relevant images to make your content more engaging and visually appealing'; } $suggestions[] = 'Set a target keyword in the Content Optimizer to get more specific suggestions'; return $suggestions; } // Content length suggestions if ($word_count < 300) { $suggestions[] = "Expand your content about '{$target_keyword}' to at least 300 words for better SEO"; } // Keyword density suggestions if ($keyword_density < 1) { $suggestions[] = "Include '{$target_keyword}' more naturally throughout the content"; } elseif ($keyword_density > 3) { $suggestions[] = "Reduce keyword density for '{$target_keyword}' to avoid over-optimization"; } // Structure suggestions if ($headings_count === 0) { $suggestions[] = 'Add H2 and H3 headings to improve content structure'; } if ($images_count === 0) { $suggestions[] = 'Add relevant images with descriptive alt text'; } // General SEO suggestions $suggestions[] = 'Add internal links to related pages on your site'; $suggestions[] = 'Include external links to authoritative sources'; $suggestions[] = 'Optimize meta description to include your target keyword'; return $suggestions; } private function calculate_initial_score($post) { $content = $post->post_content; $title = $post->post_title; $target_keyword = get_post_meta($post->ID, '_aeo_bob_target_keyword', true); return $this->calculate_seo_score($content, $title, $target_keyword); } private function get_score_color($score) { if ($score >= 80) return 'success'; elseif ($score >= 60) return 'primary'; else return 'secondary'; } private function get_score_label($score) { if ($score >= 90) return 'Excellent SEO optimization!'; elseif ($score >= 80) return 'Good SEO optimization'; elseif ($score >= 60) return 'Fair SEO, needs improvement'; elseif ($score >= 40) return 'Poor SEO, requires attention'; else return 'Very poor SEO, needs work'; } }