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( '