load_options(); $this->init_hooks(); $this->llm_service = new LLM_Service(); } /** * Initialize WordPress hooks. */ private function init_hooks() { // Replace page load generation with cached excerpt display add_filter('the_excerpt', array($this, 'display_cached_excerpt'), 10); add_filter('get_the_excerpt', array($this, 'display_cached_excerpt'), 10); // Generate excerpts on post save add_action('save_post', array($this, 'generate_excerpt_on_save'), 10, 2); add_action('transition_post_status', array($this, 'handle_post_status_change'), 10, 3); // Background processing hooks add_action('ai_smart_excerpt_generate', array($this, 'generate_excerpt_background'), 10, 2); add_action('ai_smart_excerpt_bulk_process', array($this, 'process_bulk_excerpts')); add_action('ai_smart_excerpt_bulk_process_backup', array($this, 'process_bulk_excerpts_backup')); add_action('ai_smart_excerpt_seo_bulk_process', array($this, 'process_bulk_seo')); // Admin hooks if (is_admin()) { new AI_Smart_Excerpt_Admin(); new AI_Smart_Excerpt_Feedback_Manager(); } // Load text domain add_action('plugins_loaded', array($this, 'load_textdomain')); } /** * Load plugin options. */ private function load_options() { $defaults = array( 'is_active' => false, 'admin_only' => false, 'llm_provider' => 'claude', 'claude_model' => 'claude-haiku-4-5', 'openai_model' => 'gpt-5-mini', 'gemini_model' => 'gemini-1.5-flash', 'api_key' => '', 'excerpt_type' => 'seo', 'max_length' => 300, 'min_content_length' => 100, 'featured_image' => false, 'featured_size' => 'thumbnail', 'featured_align' => 'alignleft', 'search_featured' => false, 'more_link_text' => '', 'skip_manual' => false, 'fallback_enabled' => true, 'background_processing' => true, 'immediate_threshold' => 1000, // Process immediately if content < 1000 chars 'batch_size' => 5, // Process 5 posts per batch 'batch_delay' => 300, // 5 minutes between batches // SEO Integration options 'seo_integration_enabled' => true, 'auto_assign_categories' => true, 'auto_assign_tags' => true, 'max_auto_tags' => 8, 'create_new_categories' => false, // Only assign existing categories 'seo_focus_keyword' => true, // Set focus keyword for SEO plugins 'taxonomy_confidence_threshold' => 0.7, // Only assign if AI is confident // Keyword generation settings 'keywords_enabled' => true, 'keyword_type' => 'seo', // Default keyword type for bulk processing 'max_keywords' => 10, // Maximum number of keywords to generate 'keywords_as_tags' => true, // Assign keywords as post tags 'keywords_display_meta' => true, // Show in post meta box // Auto-generation settings 'auto_excerpt_on_schedule' => true, // Auto-generate excerpt when post is scheduled/planned 'auto_seo_on_schedule' => true, // Auto-generate SEO when post is scheduled/planned ); $this->options = wp_parse_args(get_option('ai_smart_excerpt_options', array()), $defaults); } /** * Display cached AI excerpt or fallback. * * @param string $excerpt Current excerpt. * @return string Generated excerpt. */ public function display_cached_excerpt($excerpt = '') { global $post; // Return early if conditions not met if (!$this->should_process_post($post)) { return $excerpt; } // Get AI excerpt from post meta $ai_excerpt = get_post_meta($post->ID, '_ai_smart_excerpt', true); if (!empty($ai_excerpt)) { return $this->format_excerpt($ai_excerpt, $post); } // If no AI excerpt exists and we have manual excerpt, use it if (!empty($post->post_excerpt) && !$this->options['skip_manual']) { return $post->post_excerpt; } // Generate fallback excerpt immediately (fast) if ($this->options['fallback_enabled']) { $fallback = $this->fallback_excerpt($post); if (!empty($fallback)) { return $this->format_excerpt($fallback, $post); } } return $excerpt; } /** * Generate excerpt when post is saved. * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ public function generate_excerpt_on_save($post_id, $post) { // Avoid infinite loops and autosaves if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { return; } // Check if we should process this post type if (!$this->should_process_post($post)) { return; } // Generate excerpt if setting is enabled and excerpt is empty if (!empty($this->options['auto_excerpt_on_schedule']) && $this->options['auto_excerpt_on_schedule']) { // Check if excerpt needs generation (empty or override enabled) if (empty($post->post_excerpt) || $this->options['skip_manual']) { $this->generate_excerpt_immediately($post_id); } } // Generate SEO content if setting is enabled if (!empty($this->options['auto_seo_on_schedule']) && $this->options['auto_seo_on_schedule']) { $this->generate_seo_immediately($post_id, $post); } } /** * Handle post status changes (for scheduled posts). * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ public function handle_post_status_change($new_status, $old_status, $post) { // When a scheduled post becomes published if ($new_status === 'publish' && $old_status !== 'publish') { $this->generate_excerpt_on_save($post->ID, $post); } } /** * Generate excerpt immediately (synchronous). * * @param int $post_id Post ID. */ private function generate_excerpt_immediately($post_id) { $post = get_post($post_id); if (!$post) { return; } $content = $this->prepare_content($post); if (empty($content)) { return; } // Generate excerpt $ai_excerpt = $this->llm_service->generate_excerpt( $content, $post->post_title, $this->options['excerpt_type'], $this->options['max_length'] ); // Fallback if AI fails if (!$ai_excerpt && $this->options['fallback_enabled']) { $ai_excerpt = $this->fallback_excerpt($post); } // Store the excerpt if ($ai_excerpt) { update_post_meta($post_id, '_ai_smart_excerpt', $ai_excerpt); update_post_meta($post_id, '_ai_excerpt_generated_at', current_time('timestamp')); // Trigger feedback tracking do_action('ai_smart_excerpt_generated', $post_id, $ai_excerpt); } } /** * Generate SEO content immediately (synchronous). * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ private function generate_seo_immediately($post_id, $post) { // Check if SEO processing is needed if (!$this->post_needs_seo_processing($post_id)) { return; } // Prepare content for AI processing $content = $this->prepare_content($post); if (empty($content)) { return; } // Temporarily enable all SEO features for immediate processing $original_settings = array( 'seo_integration_enabled' => $this->options['seo_integration_enabled'] ?? false, 'auto_assign_tags' => $this->options['auto_assign_tags'] ?? false, 'auto_assign_categories' => $this->options['auto_assign_categories'] ?? false, 'seo_focus_keyword' => $this->options['seo_focus_keyword'] ?? false ); $this->options['seo_integration_enabled'] = true; $this->options['auto_assign_tags'] = true; $this->options['auto_assign_categories'] = true; $this->options['seo_focus_keyword'] = true; // Process SEO optimization $seo_results = $this->assign_smart_taxonomies($post_id, $content, $post->post_title); // Restore original settings foreach ($original_settings as $key => $value) { $this->options[$key] = $value; } if ($seo_results) { // Log the SEO assignments update_post_meta($post_id, '_ai_seo_assignments', array( 'categories' => $seo_results['categories'] ?? array(), 'tags' => $seo_results['tags'] ?? array(), 'focus_keyword' => $seo_results['focus_keyword'] ?? null, 'assigned_at' => current_time('timestamp') )); } } /** * Schedule excerpt generation in background. * * @param int $post_id Post ID. */ private function schedule_excerpt_generation($post_id) { // Schedule single event for this post if (!wp_next_scheduled('ai_smart_excerpt_generate', array($post_id, false))) { wp_schedule_single_event( time() + 30, // Process in 30 seconds 'ai_smart_excerpt_generate', array($post_id, false) ); } } /** * Generate excerpt in background (WordPress Cron). * * @param int $post_id Post ID. * @param bool $force Force regeneration even if excerpt exists. */ public function generate_excerpt_background($post_id, $force = false) { $post = get_post($post_id); if (!$post) { return; } // Skip if excerpt already exists and not forcing if (!$force && get_post_meta($post_id, '_ai_smart_excerpt', true)) { return; } // Add a small delay to avoid API rate limits if (!$force) { sleep(1); } $this->generate_excerpt_immediately($post_id); } /** * Check if we should process this post. * * @param WP_Post $post Post object. * @return bool */ private function should_process_post($post) { // Plugin not active if (!$this->options['is_active']) { return false; } // Admin only mode if ($this->options['admin_only'] && !is_admin()) { return false; } // Only process posts and pages if (!in_array($post->post_type, array('post', 'page'))) { return false; } // Check if post has enough content $content = strip_shortcodes($post->post_content); $content_length = strlen(wp_strip_all_tags($content)); if ($content_length < $this->options['min_content_length']) { return false; } return true; } /** * Check if we should process immediately vs background. * * @param WP_Post $post Post object. * @return bool */ private function should_process_immediately($post) { // Always process immediately in admin if (is_admin()) { return true; } // Process small posts immediately $content_length = strlen(wp_strip_all_tags($post->post_content)); if ($content_length < 1000) { return true; } // Check if we have less than 5 pending background jobs $pending_jobs = $this->count_scheduled_hooks('ai_smart_excerpt_generate'); if ($pending_jobs < 5) { return true; } return false; } /** * Bulk process excerpts for existing posts. */ public function process_bulk_excerpts() { // Get current progress $bulk_progress = get_option('ai_smart_excerpt_bulk_progress', array( 'status' => 'idle', 'total' => 0, 'processed' => 0, 'current_batch' => 0, 'errors' => 0, 'started_at' => 0 )); // If this is the first run, initialize the progress if ($bulk_progress['status'] === 'idle') { // Count total posts needing excerpts using more efficient query global $wpdb; // Check if this is force regeneration $is_force_regenerate = !empty($bulk_progress['force_regenerate']); if ($is_force_regenerate) { // Force mode: process ALL posts regardless of existing excerpts $total_count = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p WHERE p.post_type IN ('post', 'page') AND p.post_status = 'publish' ")); } else { // Normal mode: only posts without excerpts $total_count = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_ai_smart_excerpt' WHERE p.post_type IN ('post', 'page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '') ")); } $bulk_progress = array( 'status' => 'running', 'total' => (int) $total_count, 'processed' => 0, 'current_batch' => 1, 'errors' => 0, 'started_at' => time(), 'last_updated' => time(), 'force_regenerate' => $is_force_regenerate ); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); // If no posts to process, mark as completed immediately if ($total_count == 0) { $bulk_progress['status'] = 'completed'; $bulk_progress['completed_at'] = time(); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); return; } } // Get posts to process in this batch using more efficient query global $wpdb; $batch_size = 5; $offset = ($bulk_progress['current_batch'] - 1) * $batch_size; // Check if this is force regeneration $is_force_regenerate = !empty($bulk_progress['force_regenerate']); if ($is_force_regenerate) { // Force mode: get ALL posts regardless of existing excerpts $post_ids = $wpdb->get_col($wpdb->prepare(" SELECT DISTINCT p.ID FROM {$wpdb->posts} p WHERE p.post_type IN ('post', 'page') AND p.post_status = 'publish' ORDER BY p.ID ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); } else { // Normal mode: only posts without excerpts $post_ids = $wpdb->get_col($wpdb->prepare(" SELECT DISTINCT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_ai_smart_excerpt' WHERE p.post_type IN ('post', 'page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '') ORDER BY p.ID ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); } $processed_in_batch = 0; $errors_in_batch = 0; foreach ($post_ids as $post_id) { // Check for script timeout if (time() - $bulk_progress['started_at'] > 45) { // Max 45 seconds per batch // Schedule next batch immediately to continue wp_schedule_single_event(time() + 10, 'ai_smart_excerpt_bulk_process'); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); return; } $post = get_post($post_id); if ($post && $this->should_process_post($post)) { try { $start_time = time(); $this->generate_excerpt_immediately($post->ID); $processing_time = time() - $start_time; $processed_in_batch++; // Update progress after each post $bulk_progress['processed']++; $bulk_progress['last_updated'] = time(); $bulk_progress['last_post_time'] = $processing_time; update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); // Dynamic delay based on processing time (1-3 seconds) $delay = max(1, min(3, $processing_time)); sleep($delay); } catch (Exception $e) { $errors_in_batch++; $bulk_progress['errors']++; $bulk_progress['last_updated'] = time(); $bulk_progress['last_error'] = $e->getMessage(); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); // Shorter delay on errors sleep(1); } } else { // Skip this post but count it as processed to avoid infinite loops $bulk_progress['processed']++; $bulk_progress['last_updated'] = time(); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); } } // Update final batch progress $bulk_progress['current_batch']++; $bulk_progress['last_updated'] = time(); // Check if there are more posts to process $remaining_posts = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_ai_smart_excerpt' WHERE p.post_type IN ('post', 'page') AND p.post_status = 'publish' AND (pm.meta_value IS NULL OR pm.meta_value = '') AND p.ID > %d ", max($post_ids ?: array(0)))); // Schedule next batch if there are more posts and we processed something in this batch if ($remaining_posts > 0 && count($post_ids) > 0 && $processed_in_batch > 0) { // Shorter delay for better user experience wp_schedule_single_event(time() + 30, 'ai_smart_excerpt_bulk_process'); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); } elseif ($remaining_posts > 0 && $processed_in_batch === 0) { // If we didn't process anything, there might be an issue - wait longer and try again if ($bulk_progress['current_batch'] < 50) { // Prevent infinite loops wp_schedule_single_event(time() + 120, 'ai_smart_excerpt_bulk_process'); update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); } else { // Too many empty batches - mark as completed to prevent infinite loop $bulk_progress['status'] = 'completed'; $bulk_progress['completed_at'] = time(); $bulk_progress['completion_reason'] = 'max_batches_reached'; update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); do_action('ai_smart_excerpt_bulk_completed', $bulk_progress['processed']); } } else { // Mark as completed $bulk_progress['status'] = 'completed'; $bulk_progress['completed_at'] = time(); $bulk_progress['completion_reason'] = 'all_posts_processed'; update_option('ai_smart_excerpt_bulk_progress', $bulk_progress); // Trigger bulk completion feedback do_action('ai_smart_excerpt_bulk_completed', $bulk_progress['processed']); } } /** * Backup bulk processing method to handle failed cron triggers. */ public function process_bulk_excerpts_backup() { $bulk_progress = get_option('ai_smart_excerpt_bulk_progress', array('status' => 'idle')); // Only run if there's an active bulk process that seems stuck if ($bulk_progress['status'] === 'running') { $last_updated = isset($bulk_progress['last_updated']) ? $bulk_progress['last_updated'] : 0; $time_since_update = time() - $last_updated; // If no update for more than 3 minutes, restart processing if ($time_since_update > 180) { $this->process_bulk_excerpts(); } } } /** * Bulk process SEO for existing posts - add missing tags and focus keywords. */ public function process_bulk_seo() { error_log('AI Smart Excerpt SEO Bulk: process_bulk_seo() function called at ' . gmdate('Y-m-d H:i:s')); // Get current SEO bulk progress $seo_bulk_progress = get_option('ai_smart_excerpt_seo_bulk_progress', array( 'status' => 'idle', 'total' => 0, 'processed' => 0, 'current_batch' => 0, 'batch_size' => 3, 'start_time' => 0, 'last_updated' => 0, 'errors' => 0 )); // Don't start if already running if ($seo_bulk_progress['status'] === 'running') { error_log('AI Smart Excerpt SEO Bulk: Status is already running, continuing with existing batch...'); // Don't return, continue processing the next batch } // Initialize if this is a fresh start if ($seo_bulk_progress['status'] === 'idle' || empty($seo_bulk_progress['post_ids'])) { // Get all published and scheduled posts $posts = get_posts(array( 'post_type' => array('post', 'page'), 'post_status' => array('publish', 'future'), // Include scheduled posts 'numberposts' => -1, 'fields' => 'ids' )); // Log for debugging error_log('AI Smart Excerpt SEO Bulk: Found ' . count($posts) . ' posts to process'); $seo_bulk_progress = array( 'status' => 'running', 'total' => count($posts), 'processed' => 0, 'current_batch' => 0, 'batch_size' => 3, 'start_time' => time(), 'last_updated' => time(), 'errors' => 0, 'post_ids' => $posts ); update_option('ai_smart_excerpt_seo_bulk_progress', $seo_bulk_progress); // Early return if no posts if (count($posts) === 0) { $seo_bulk_progress['status'] = 'completed'; update_option('ai_smart_excerpt_seo_bulk_progress', $seo_bulk_progress); return; } } // Process a batch of posts $batch_start = $seo_bulk_progress['current_batch'] * $seo_bulk_progress['batch_size']; $batch_posts = array_slice($seo_bulk_progress['post_ids'], $batch_start, $seo_bulk_progress['batch_size']); error_log('AI Smart Excerpt SEO Bulk: Processing batch ' . $seo_bulk_progress['current_batch'] . ' (start: ' . $batch_start . ', size: ' . $seo_bulk_progress['batch_size'] . ')'); error_log('AI Smart Excerpt SEO Bulk: Batch posts: ' . implode(',', $batch_posts)); error_log('AI Smart Excerpt SEO Bulk: Current progress: ' . $seo_bulk_progress['processed'] . '/' . $seo_bulk_progress['total']); if (empty($batch_posts)) { error_log('AI Smart Excerpt SEO Bulk: No more posts to process, completing'); $seo_bulk_progress['status'] = 'completed'; update_option('ai_smart_excerpt_seo_bulk_progress', $seo_bulk_progress); return; } foreach ($batch_posts as $post_id) { try { $post = get_post($post_id); if (!$post) { $seo_bulk_progress['errors']++; error_log('AI Smart Excerpt SEO Bulk: Post ' . $post_id . ' not found'); $seo_bulk_progress['processed']++; // Still count as processed continue; } // Check if SEO processing is needed $needs_seo = $this->post_needs_seo_processing($post_id); error_log('AI Smart Excerpt SEO Bulk: Post ' . $post_id . ' (' . $post->post_title . ') needs SEO: ' . ($needs_seo ? 'YES' : 'NO')); if ($needs_seo) { // Prepare content for AI processing $content = $this->prepare_content($post); $title = $post->post_title; error_log('AI Smart Excerpt SEO Bulk: Processing post ' . $post_id . ' - ' . $title); // Temporarily enable all SEO features for bulk processing $original_settings = array( 'seo_integration_enabled' => $this->options['seo_integration_enabled'] ?? false, 'auto_assign_tags' => $this->options['auto_assign_tags'] ?? false, 'auto_assign_categories' => $this->options['auto_assign_categories'] ?? false, 'seo_focus_keyword' => $this->options['seo_focus_keyword'] ?? false ); $this->options['seo_integration_enabled'] = true; $this->options['auto_assign_tags'] = true; $this->options['auto_assign_categories'] = true; $this->options['seo_focus_keyword'] = true; // Process SEO optimization $seo_results = $this->assign_smart_taxonomies($post_id, $content, $title); // Restore original settings foreach ($original_settings as $key => $value) { $this->options[$key] = $value; } if ($seo_results) { // Log the SEO assignments update_post_meta($post_id, '_ai_seo_assignments', array( 'categories' => $seo_results['categories'] ?? array(), 'tags' => $seo_results['tags'] ?? array(), 'focus_keyword' => $seo_results['focus_keyword'] ?? null, 'assigned_at' => current_time('timestamp') )); error_log('AI Smart Excerpt SEO Bulk: Successfully processed post ' . $post_id); } else { error_log('AI Smart Excerpt SEO Bulk: SEO processing failed for post ' . $post_id); } } else { error_log('AI Smart Excerpt SEO Bulk: Post ' . $post_id . ' skipped (doesn\'t need SEO processing)'); } // Always increment processed count regardless of whether SEO was needed $seo_bulk_progress['processed']++; error_log('AI Smart Excerpt SEO Bulk: Processed count now: ' . $seo_bulk_progress['processed']); // Update progress immediately after each post $seo_bulk_progress['last_updated'] = time(); update_option('ai_smart_excerpt_seo_bulk_progress', $seo_bulk_progress); // Small delay between posts to avoid overwhelming the LLM API if ($seo_bulk_progress['processed'] % 3 !== 0) { // Not at end of batch sleep(2); // 2 second delay between posts in same batch } } catch (Exception $e) { error_log('AI Smart Excerpt SEO Bulk Processing Error for post ' . $post_id . ': ' . $e->getMessage()); $seo_bulk_progress['errors']++; } } // Update progress $seo_bulk_progress['current_batch']++; $seo_bulk_progress['last_updated'] = time(); error_log('AI Smart Excerpt SEO Bulk: Batch completed. Current batch now: ' . $seo_bulk_progress['current_batch']); error_log('AI Smart Excerpt SEO Bulk: Final progress: ' . $seo_bulk_progress['processed'] . '/' . $seo_bulk_progress['total']); // Check if we're done if ($seo_bulk_progress['processed'] >= $seo_bulk_progress['total']) { $seo_bulk_progress['status'] = 'completed'; error_log('AI Smart Excerpt SEO Bulk: Processing completed!'); } update_option('ai_smart_excerpt_seo_bulk_progress', $seo_bulk_progress); // Schedule next batch if not done - give LLM time to process if ($seo_bulk_progress['status'] === 'running') { error_log('AI Smart Excerpt SEO Bulk: Scheduling next batch...'); // Give more time between batches to allow for LLM processing // Method 1: Standard WordPress cron - allow 10 seconds for LLM calls wp_schedule_single_event(time() + 10, 'ai_smart_excerpt_seo_bulk_process'); // Method 2: Backup scheduling - 15 seconds later wp_schedule_single_event(time() + 15, 'ai_smart_excerpt_seo_bulk_process'); // Method 3: JavaScript fallback will handle cases where cron fails set_transient('ai_seo_bulk_needs_continue', true, 60); // 60 second timeout } else { // Clean up on completion delete_transient('ai_seo_bulk_needs_continue'); do_action('ai_smart_excerpt_seo_bulk_completed', $seo_bulk_progress['processed']); error_log('AI Smart Excerpt SEO Bulk: Processing completed. Total processed: ' . $seo_bulk_progress['processed']); } } /** * Check if a post needs SEO processing (missing tags or focus keywords). * * @param int $post_id Post ID. * @return bool True if SEO processing is needed. */ public function post_needs_seo_processing($post_id) { // Check if we're in force regeneration mode $seo_bulk_progress = get_option('ai_smart_excerpt_seo_bulk_progress', array()); $is_force_regenerate = !empty($seo_bulk_progress['force_regenerate']); // If force regeneration is enabled, process all posts if ($is_force_regenerate) { return true; } // Check if post has tags $post_tags = wp_get_post_tags($post_id); $has_few_tags = count($post_tags) < 8; // More lenient - consider posts with fewer than 5 tags as needing more // Check if focus keyword exists for SEO plugins $has_focus_keyword = false; $seo_plugin = $this->get_seo_plugin_integration(); switch ($seo_plugin) { case 'yoast': $focus_keyword = get_post_meta($post_id, '_yoast_wpseo_focuskw', true); $has_focus_keyword = !empty($focus_keyword); break; case 'rankmath': $focus_keyword = get_post_meta($post_id, 'rank_math_focus_keyword', true); $has_focus_keyword = !empty($focus_keyword); break; case 'aioseo': $focus_keyword = get_post_meta($post_id, '_aioseo_keywords', true); $has_focus_keyword = !empty($focus_keyword); break; default: // If no SEO plugin detected, always try to add tags $has_focus_keyword = false; } // Always process if post has fewer than 5 tags, regardless of focus keyword status // This ensures we always have something to process return $has_few_tags || !$has_focus_keyword; } /** * Prepare content for AI processing. * * @param WP_Post $post Post object. * @return string Cleaned content. */ public function prepare_content($post) { $content = $post->post_content; // Remove shortcodes $content = strip_shortcodes($content); // Remove HTML tags $content = wp_strip_all_tags($content); // Clean up whitespace $content = preg_replace('/\s+/', ' ', $content); $content = trim($content); // Limit content length for API efficiency (keep first 3000 chars) if (strlen($content) > 3000) { $content = substr($content, 0, 3000) . '...'; } return $content; } /** * Format the final excerpt with images and links. * * @param string $excerpt Generated excerpt. * @param WP_Post $post Post object. * @return string Formatted excerpt. */ private function format_excerpt($excerpt, $post) { $output = ''; // Add featured image if enabled if ($this->options['featured_image']) { $image = $this->get_featured_image($post); if ($image) { $output .= $image; } } // Add the excerpt text $output .= '

' . esc_html($excerpt) . '

'; // Add more link if configured if (!empty($this->options['more_link_text'])) { $more_link = sprintf( ' %s', esc_url(get_permalink($post)), esc_html($this->options['more_link_text']) ); $output .= $more_link; } return $output; } /** * Get featured image HTML. * * @param WP_Post $post Post object. * @return string Image HTML or empty string. */ private function get_featured_image($post) { if (has_post_thumbnail($post->ID)) { return get_the_post_thumbnail( $post->ID, $this->options['featured_size'], array('class' => $this->options['featured_align']) ); } // Search for first image in content if enabled if ($this->options['search_featured']) { $image_url = $this->extract_first_image($post); if ($image_url) { return sprintf( '%s', esc_url(get_permalink($post)), esc_url($image_url), esc_attr($this->options['featured_align']), esc_attr(get_the_title($post)) ); } } return ''; } /** * Extract first image from post content. * * @param WP_Post $post Post object. * @return string|false Image URL or false. */ private function extract_first_image($post) { $content = $post->post_content; if (stripos($content, 'post_content; $original_excerpt = $post->post_excerpt; // Clear any existing excerpt to force WordPress to generate from content $post->post_excerpt = ''; $post->post_content = $original_content; // Use WordPress's built-in excerpt generation $excerpt = wp_trim_excerpt('', $post); // Restore original values $post->post_content = $original_content; $post->post_excerpt = $original_excerpt; return $excerpt; } /** * Load plugin text domain for translations. */ public function load_textdomain() { // Load plugin textdomain for translations load_plugin_textdomain( 'ai-smart-excerpt', false, dirname(plugin_basename(AI_SMART_EXCERPT_PLUGIN_PATH . 'smart-excerpt.php')) . '/languages' ); } /** * Plugin activation. */ public static function activate() { // Set default options $defaults = array( 'is_active' => false, 'llm_provider' => 'claude', 'excerpt_type' => 'seo', 'max_length' => 300, ); add_option('ai_smart_excerpt_options', $defaults); // Clear any existing caches wp_cache_flush(); } /** * Plugin deactivation. */ public static function deactivate() { // Clean up known transients - WordPress will handle the cleanup // Common transient patterns used by this plugin $transient_patterns = array( 'ai_excerpt_cache_', 'ai_excerpt_response_', 'ai_excerpt_settings_', 'ai_excerpt_status_' ); // Attempt to delete common transients foreach ($transient_patterns as $pattern) { for ($i = 0; $i < 1000; $i++) { $transient_name = $pattern . $i; delete_transient($transient_name); } } // Clear object cache wp_cache_flush(); } /** * Get plugin options. * * @return array Plugin options. */ public function get_options() { return $this->options; } /** * Update plugin options. * * @param array $options New options. */ public function update_options($options) { $this->options = wp_parse_args($options, $this->options); update_option('ai_smart_excerpt_options', $this->options); } /** * Detect installed SEO plugins and integrate accordingly. * * @return string|false SEO plugin identifier or false if none detected. */ public function get_seo_plugin_integration() { // Yoast SEO if (defined('WPSEO_VERSION')) { return 'yoast'; } // RankMath if (defined('RANK_MATH_VERSION')) { return 'rankmath'; } // All in One SEO if (defined('AIOSEO_PHP_VERSION_DIR')) { return 'aioseo'; } return false; } /** * Generate and set focus keywords for SEO plugins. * * @param int $post_id Post ID. * @param array $keywords Array of keywords. * @return bool Success status. */ public function set_seo_focus_keywords($post_id, $keywords) { if (empty($keywords) || !is_array($keywords)) { error_log('AI Smart Excerpt SEO: set_seo_focus_keywords called with empty/invalid keywords'); return false; } $seo_plugin = $this->get_seo_plugin_integration(); $primary_keyword = $keywords[0]; // Use first keyword as primary error_log('AI Smart Excerpt SEO: Setting focus keywords for post ' . $post_id . ', plugin: ' . ($seo_plugin ?: 'NONE')); error_log('AI Smart Excerpt SEO: Primary keyword: ' . $primary_keyword); switch ($seo_plugin) { case 'yoast': // Set primary focus keyword $result = update_post_meta($post_id, '_yoast_wpseo_focuskw', $primary_keyword); error_log('AI Smart Excerpt SEO: Yoast focus keyword set: ' . $primary_keyword . ' (result: ' . ($result ? 'SUCCESS' : 'FAILED') . ')'); // Set additional keywords as synonyms if available if (count($keywords) > 1) { $additional = array_slice($keywords, 1, 4); // Max 4 additional keywords $synonyms_result = update_post_meta($post_id, '_yoast_wpseo_keywordsynonyms', implode(',', $additional)); error_log('AI Smart Excerpt SEO: Yoast synonyms set: ' . implode(',', $additional) . ' (result: ' . ($synonyms_result ? 'SUCCESS' : 'FAILED') . ')'); } // Set meta description if not already present $existing_metadesc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); if (empty($existing_metadesc)) { $excerpt = get_post_meta($post_id, '_ai_smart_excerpt', true); if (!empty($excerpt)) { $metadesc_result = update_post_meta($post_id, '_yoast_wpseo_metadesc', $excerpt); error_log('AI Smart Excerpt SEO: Yoast meta description set from excerpt (result: ' . ($metadesc_result ? 'SUCCESS' : 'FAILED') . ')'); } } return true; case 'rankmath': // Set primary focus keyword $result = update_post_meta($post_id, 'rank_math_focus_keyword', $primary_keyword); error_log('AI Smart Excerpt SEO: RankMath focus keyword set: ' . $primary_keyword . ' (result: ' . ($result ? 'SUCCESS' : 'FAILED') . ')'); // Set meta description if not already present $existing_metadesc = get_post_meta($post_id, 'rank_math_description', true); if (empty($existing_metadesc)) { $excerpt = get_post_meta($post_id, '_ai_smart_excerpt', true); if (!empty($excerpt)) { $metadesc_result = update_post_meta($post_id, 'rank_math_description', $excerpt); error_log('AI Smart Excerpt SEO: RankMath meta description set from excerpt (result: ' . ($metadesc_result ? 'SUCCESS' : 'FAILED') . ')'); } } return true; case 'aioseo': // Set focus keyword (AIOSEO stores keywords as comma-separated string) $result = update_post_meta($post_id, '_aioseo_keywords', implode(',', array_slice($keywords, 0, 10))); error_log('AI Smart Excerpt SEO: AIOSEO keywords set: ' . implode(',', array_slice($keywords, 0, 10)) . ' (result: ' . ($result ? 'SUCCESS' : 'FAILED') . ')'); // Set meta description if not already present $existing_metadesc = get_post_meta($post_id, '_aioseo_description', true); if (empty($existing_metadesc)) { $excerpt = get_post_meta($post_id, '_ai_smart_excerpt', true); if (!empty($excerpt)) { $metadesc_result = update_post_meta($post_id, '_aioseo_description', $excerpt); error_log('AI Smart Excerpt SEO: AIOSEO meta description set from excerpt (result: ' . ($metadesc_result ? 'SUCCESS' : 'FAILED') . ')'); } } return true; default: return false; } } /** * Generate taxonomies from content and assign to post. * * @param int $post_id Post ID. * @param string $content Post content. * @param string $title Post title. * @return array Array with assigned categories and tags. */ public function assign_smart_taxonomies($post_id, $content, $title) { error_log('AI Smart Excerpt SEO: assign_smart_taxonomies called for post ' . $post_id); error_log('AI Smart Excerpt SEO: Options - auto_assign_tags: ' . ($this->options['auto_assign_tags'] ? 'YES' : 'NO')); error_log('AI Smart Excerpt SEO: Options - auto_assign_categories: ' . ($this->options['auto_assign_categories'] ? 'YES' : 'NO')); error_log('AI Smart Excerpt SEO: Options - seo_focus_keyword: ' . ($this->options['seo_focus_keyword'] ? 'YES' : 'NO')); $results = array( 'categories' => array(), 'tags' => array(), 'focus_keyword' => null ); // Generate category suggestions if ($this->options['auto_assign_categories']) { $existing_categories = get_categories(array('hide_empty' => false)); $category_suggestions = $this->llm_service->generate_categories($content, $title, $existing_categories); if ($category_suggestions) { $assigned_categories = $this->assign_categories($post_id, $category_suggestions, $existing_categories); $results['categories'] = $assigned_categories; } } // Generate tag suggestions if ($this->options['auto_assign_tags']) { error_log('AI Smart Excerpt SEO: Calling LLM service for tags...'); error_log('AI Smart Excerpt SEO: Content length: ' . strlen($content) . ' chars'); error_log('AI Smart Excerpt SEO: API Key present: ' . (!empty($this->options['api_key']) ? 'YES' : 'NO')); try { $tag_suggestions = $this->llm_service->generate_tags($content, $title, $this->options['max_auto_tags']); error_log('AI Smart Excerpt SEO: LLM returned tags: ' . print_r($tag_suggestions, true)); if ($tag_suggestions && is_array($tag_suggestions)) { $assigned_tags = $this->assign_tags($post_id, $tag_suggestions); error_log('AI Smart Excerpt SEO: Assigned tags: ' . print_r($assigned_tags, true)); $results['tags'] = $assigned_tags; } else { error_log('AI Smart Excerpt SEO: LLM returned invalid/empty tags'); } } catch (Exception $e) { error_log('AI Smart Excerpt SEO: Exception during tag generation: ' . $e->getMessage()); $results['tags'] = array(); // Ensure we don't break processing } } else { error_log('AI Smart Excerpt SEO: Tag assignment is disabled in options'); } // Generate focus keyword for SEO plugins if ($this->options['seo_focus_keyword']) { error_log('AI Smart Excerpt SEO: Calling LLM service for focus keyword...'); try { $focus_keyword = $this->llm_service->generate_focus_keyword($content, $title); error_log('AI Smart Excerpt SEO: LLM returned focus keyword: ' . ($focus_keyword ?: 'EMPTY')); if ($focus_keyword) { $success = $this->set_seo_focus_keywords($post_id, array($focus_keyword)); if ($success) { $results['focus_keyword'] = $focus_keyword; error_log('AI Smart Excerpt SEO: Focus keyword set successfully: ' . $focus_keyword); } else { error_log('AI Smart Excerpt SEO: Failed to set focus keyword in SEO plugin'); } } else { error_log('AI Smart Excerpt SEO: LLM returned empty focus keyword'); } } catch (Exception $e) { error_log('AI Smart Excerpt SEO: Exception during focus keyword generation: ' . $e->getMessage()); $results['focus_keyword'] = null; } } return $results; } /** * Intelligent category assignment based on existing site categories. * * @param int $post_id Post ID. * @param array $suggestions Category suggestions from AI. * @param array $existing_categories Existing site categories. * @return array Assigned category names. */ private function assign_categories($post_id, $suggestions, $existing_categories) { $matched_categories = array(); $assigned_names = array(); foreach ($suggestions as $suggestion) { // Find closest matching existing category $match = $this->find_closest_category($suggestion, $existing_categories); if ($match) { $matched_categories[] = $match->term_id; $assigned_names[] = $match->name; } elseif ($this->options['create_new_categories']) { // Create new category if enabled $new_category = wp_insert_category(array( 'cat_name' => $suggestion, 'category_description' => 'Auto-created by AI Smart Excerpt', 'category_nicename' => sanitize_title($suggestion) )); if (!is_wp_error($new_category)) { $matched_categories[] = $new_category; $assigned_names[] = $suggestion; } } } if (!empty($matched_categories)) { wp_set_post_categories($post_id, $matched_categories, true); // Append, don't replace } return $assigned_names; } /** * Find closest matching category using fuzzy matching. * * @param string $suggestion AI suggestion. * @param array $existing_categories Existing categories. * @return object|null Matching category object or null. */ private function find_closest_category($suggestion, $existing_categories) { $suggestion_lower = strtolower($suggestion); $best_match = null; $highest_similarity = 0; foreach ($existing_categories as $category) { $category_lower = strtolower($category->name); // Exact match if ($category_lower === $suggestion_lower) { return $category; } // Partial match (category contains suggestion or vice versa) if (strpos($category_lower, $suggestion_lower) !== false || strpos($suggestion_lower, $category_lower) !== false) { $similarity = 0.8; // High similarity for partial matches } else { // Use similarity percentage similar_text($category_lower, $suggestion_lower, $similarity); $similarity /= 100; } // Only consider matches above confidence threshold if ($similarity > $this->options['taxonomy_confidence_threshold'] && $similarity > $highest_similarity) { $highest_similarity = $similarity; $best_match = $category; } } return $best_match; } /** * Assign tags to post. * * @param int $post_id Post ID. * @param array $tag_suggestions Tag suggestions from AI. * @return array Assigned tag names. */ private function assign_tags($post_id, $tag_suggestions) { if (empty($tag_suggestions)) { return array(); } // Clean up tag suggestions $clean_tags = array(); foreach ($tag_suggestions as $tag) { $clean_tag = trim($tag); if (!empty($clean_tag) && strlen($clean_tag) <= 50) { // Reasonable tag length $clean_tags[] = $clean_tag; } } if (!empty($clean_tags)) { wp_set_post_tags($post_id, $clean_tags, true); // Append, don't replace return $clean_tags; } return array(); } /** * Get SEO impact statistics for admin dashboard. * * @return array SEO impact statistics. */ public function get_seo_impact_stats() { global $wpdb; $stats = array( 'seo_plugin_detected' => $this->get_seo_plugin_integration(), 'posts_with_ai_categories' => 0, 'posts_with_ai_tags' => 0, 'focus_keywords_set' => 0, 'total_posts' => 0 ); // Count total published posts $stats['total_posts'] = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type IN ('post', 'page') AND post_status = 'publish' "); // Count posts with AI-generated excerpts (proxy for AI-processed posts) $ai_processed_posts = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_smart_excerpt' AND meta_value != '' AND meta_value IS NOT NULL "); // For now, assume AI categories/tags are proportional to AI excerpts // In future, we could track specific meta fields for AI-assigned taxonomies $stats['posts_with_ai_categories'] = $ai_processed_posts; $stats['posts_with_ai_tags'] = $ai_processed_posts; // Count focus keywords set by SEO plugins $seo_plugin = $this->get_seo_plugin_integration(); $stats['focus_keywords_set'] = 0; // Default to 0 switch ($seo_plugin) { case 'yoast': $stats['focus_keywords_set'] = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_focuskw' AND meta_value != '' AND meta_value IS NOT NULL "); break; case 'rankmath': $stats['focus_keywords_set'] = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = 'rank_math_focus_keyword' AND meta_value != '' AND meta_value IS NOT NULL "); break; case 'aioseo': $stats['focus_keywords_set'] = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_aioseo_keywords' AND meta_value != '' AND meta_value IS NOT NULL "); break; } return $stats; } /** * Count scheduled hooks for a specific hook name. * * @param string $hook_name Hook name to count. * @return int Number of scheduled hooks. */ private function count_scheduled_hooks($hook_name) { $cron_array = _get_cron_array(); if (!$cron_array) { return 0; } $count = 0; foreach ($cron_array as $timestamp => $hooks) { if (isset($hooks[$hook_name])) { $count += count($hooks[$hook_name]); } } return $count; } }