term_id : 0; // Get breaking tag ID for exclusion $breaking_tag = get_term_by('slug', 'breaking', 'post_tag'); $breaking_tag_id = $breaking_tag ? $breaking_tag->term_id : 0; // Get video story tag ID for exclusion from main feed $video_story_tag = get_term_by('slug', 'video-story', 'post_tag'); $video_story_tag_id = $video_story_tag ? $video_story_tag->term_id : 0; // Query all posts needed for homepage (optimized) // Include all post types: standard posts + custom post types // FEATURED IMAGE FIX v2: Only fetch posts that HAVE a VALID featured image. // v1 used 'compare' => 'EXISTS' which only checks if the meta key exists — // posts with _thumbnail_id = 0, '' or a deleted attachment ID still passed. // v2 uses a numeric > 0 check so only posts with a real attachment ID qualify. // A secondary filter below removes posts where the attachment file is missing. $args = array( 'post_type' => array('post', 'idf_news', 'mod_news', 'knesset_news'), 'posts_per_page' => 55, // bumped from 50 to compensate for imageless posts being filtered out 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_meta_cache' => true, 'update_post_term_cache' => true, 'meta_query' => array( array( 'key' => '_thumbnail_id', 'value' => '0', 'compare' => '>', 'type' => 'NUMERIC', ), ), ); // Exclude breaking posts (category OR tag) AND Video Story tag from main query if ($breaking_cat_id || $breaking_tag_id || $video_story_tag_id) { $tax_query = array('relation' => 'AND'); if ($breaking_cat_id) { $tax_query[] = array( 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => $breaking_cat_id, 'operator' => 'NOT IN', ); } if ($breaking_tag_id) { $tax_query[] = array( 'taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => $breaking_tag_id, 'operator' => 'NOT IN', ); } if ($video_story_tag_id) { $tax_query[] = array( 'taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => $video_story_tag_id, 'operator' => 'NOT IN', ); } $args['tax_query'] = $tax_query; } $query = new WP_Query($args); $all_posts = $query->posts; // Pre-cache metadata for performance $all_posts_meta_cache = array(); $all_posts_category_cache = array(); $all_posts_author_cache = array(); foreach ($all_posts as $post) { // Get image URLs and ensure they use CDN domain for proper preloading $thumbnail_url = get_the_post_thumbnail_url($post->ID, 'thumbnail'); $medium_url = get_the_post_thumbnail_url($post->ID, 'medium'); $medium_large_url = get_the_post_thumbnail_url($post->ID, 'medium_large'); // 768px - ideal for mobile hero $large_url = get_the_post_thumbnail_url($post->ID, 'large'); $full_url = get_the_post_thumbnail_url($post->ID, 'full'); // Original size for high-DPI hero // Rewrite URLs to use CDN if needed (handles both wp-content and wp-content/uploads paths) $site_url = get_site_url(); $cdn_url = 'https://cdn.israelmedia.uk'; if ($thumbnail_url) { $thumbnail_url = str_replace($site_url, $cdn_url, $thumbnail_url); } if ($medium_url) { $medium_url = str_replace($site_url, $cdn_url, $medium_url); } if ($medium_large_url) { $medium_large_url = str_replace($site_url, $cdn_url, $medium_large_url); } if ($large_url) { $large_url = str_replace($site_url, $cdn_url, $large_url); } if ($full_url) { $full_url = str_replace($site_url, $cdn_url, $full_url); } $all_posts_meta_cache[$post->ID] = array( 'featured_image_thumbnail' => $thumbnail_url, 'featured_image_medium' => $medium_url, 'featured_image_medium_large' => $medium_large_url, 'featured_image_large' => $large_url, 'featured_image_full' => $full_url, ); $all_posts_category_cache[$post->ID] = get_the_category($post->ID); $all_posts_author_cache[$post->ID] = get_the_author_meta('display_name', $post->post_author); } // FEATURED IMAGE FIX v2: Secondary filter — remove posts where the attachment // exists in the DB but none of the image URLs actually resolved (e.g. deleted // media file, corrupted attachment, or _thumbnail_id pointing to wrong ID). $all_posts = array_filter($all_posts, function($post) use (&$all_posts_meta_cache, &$all_posts_category_cache, &$all_posts_author_cache) { $meta = $all_posts_meta_cache[$post->ID] ?? null; if (!$meta) return false; // Keep the post only if at least one image size resolved to a real URL if ($meta['featured_image_thumbnail'] || $meta['featured_image_medium'] || $meta['featured_image_large'] || $meta['featured_image_full']) { return true; } // No valid image URL — strip from cache arrays and exclude unset($all_posts_meta_cache[$post->ID], $all_posts_category_cache[$post->ID], $all_posts_author_cache[$post->ID]); return false; }); $all_posts = array_values($all_posts); // Re-index after filtering $data = array( 'posts' => $all_posts, 'meta_cache' => $all_posts_meta_cache, 'category_cache' => $all_posts_category_cache, 'author_cache' => $all_posts_author_cache, 'breaking_cat_id' => $breaking_cat_id ); // Cache for 5 minutes — use transient (persistent) + wp_cache (fast per-request) // wp_cache alone is per-request only without Redis/Memcached and doesn't persist. set_transient($cache_key, $data, 300); wp_cache_set($cache_key, $data, 'israel_homepage', 300); return $data; } /** * Warm homepage cache * CPU FIX v5: Changed from save_post to publish_post. * CPU FIX v10: Added 30-second debounce. When scrapers publish 5-10 articles with * 1.2s delay, this 50-post WP_Query was running 5-10 times in quick succession. * Only the last one matters — the homepage only needs rebuilding once after the * batch is done. The debounce skips duplicate runs within a 30-second window. * Also: skip entirely during REST API (scraper) requests — schedule for later via * wp_schedule_single_event so it runs ONCE after the scraper batch finishes. */ function israel_warm_homepage_cache($post_id, $post = null) { // Guard: only run for news post types if ($post && !in_array($post->post_type, ['post', 'knesset_news', 'idf_news', 'mod_news'])) { return; } // CPU FIX v7: Skip during MLS slave import — slave sites have their own homepage data if (defined('MLS_IMPORTING') && MLS_IMPORTING) { return; } // CPU FIX v8: Skip homepage warming on slave sites entirely. // The 50-post WP_Query + 150 thumbnail lookups is expensive and unnecessary // on slaves — their homepage data gets naturally cached on first visitor hit. // Only the master needs proactive warming (for fastest news homepage updates). if (function_exists('mls_is_master_site') && !mls_is_master_site()) { return; } // CPU FIX v10: Debounce — only run once per 30 seconds. // During REST API (scraper) requests, defer to a scheduled event instead of running inline. $last_run = get_transient('israel_homepage_warm_debounce'); if ($last_run) { return; // Already ran or scheduled within last 30 seconds } set_transient('israel_homepage_warm_debounce', time(), 30); // During REST API requests (scrapers), schedule for 15 seconds later // so it runs once AFTER the batch of publishes finishes. if (defined('REST_REQUEST') && REST_REQUEST) { if (!wp_next_scheduled('israel_deferred_homepage_warm')) { wp_schedule_single_event(time() + 15, 'israel_deferred_homepage_warm'); } return; } israel_generate_homepage_data(); } add_action('publish_post', 'israel_warm_homepage_cache', 25, 2); add_action('publish_knesset_news', 'israel_warm_homepage_cache', 25, 2); add_action('publish_idf_news', 'israel_warm_homepage_cache', 25, 2); add_action('publish_mod_news', 'israel_warm_homepage_cache', 25, 2); // CPU FIX v10: Deferred homepage warming — runs once after scraper batch finishes add_action('israel_deferred_homepage_warm', 'israel_generate_homepage_data'); /** * FEATURED IMAGE RACE-CONDITION FIX * * When the scraper publishes an article, the featured image may not be uploaded * yet. The homepage query now requires _thumbnail_id to exist, so imageless * articles are hidden. When the image is finally attached (1-2 min later), this * hook fires, busts the homepage transient + object cache, and purges Starter * Cache / Seraphinite / Cloudflare so the article appears immediately. * * Hooked into both added_post_meta (first time) and updated_post_meta (overwrite). */ function israel_on_featured_image_set( $meta_id, $post_id, $meta_key, $meta_value ) { // Only act on featured image meta if ( '_thumbnail_id' !== $meta_key ) { return; } // Only act on published news post types $post = get_post( $post_id ); if ( ! $post || 'publish' !== $post->post_status ) { return; } $news_types = array( 'post', 'knesset_news', 'idf_news', 'mod_news' ); if ( ! in_array( $post->post_type, $news_types, true ) ) { return; } // Debounce: skip if we already busted within the last 10 seconds // (covers bulk meta updates or duplicate hooks during the same request). $debounce_key = 'israel_thumb_cache_bust_debounce'; if ( get_transient( $debounce_key ) ) { return; } set_transient( $debounce_key, 1, 10 ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( 'Featured Image Fix: _thumbnail_id set on post #%d — busting homepage cache', $post_id ) ); } // 1. Clear homepage transient + object cache israel_clear_used_titles(); // 2. Purge all external caches (Starter Cache, Seraphinite, Cloudflare) // and warm the homepage so it rebuilds with the new article + image. israel_purge_external_caches_for_homepage( $post_id ); } add_action( 'added_post_meta', 'israel_on_featured_image_set', 10, 4 ); add_action( 'updated_post_meta', 'israel_on_featured_image_set', 10, 4 ); /** * Clear homepage cache */ function israel_clear_used_titles() { // Clear both old and new cache keys for compatibility wp_cache_delete('israel_homepage_all_posts_v3', 'israel_homepage'); wp_cache_delete('israel_homepage_all_posts_v4', 'israel_homepage'); wp_cache_delete('israel_homepage_all_posts_v5', 'israel_homepage'); wp_cache_delete('israel_homepage_all_posts_v6', 'israel_homepage'); wp_cache_delete('israel_homepage_all_posts_v7', 'israel_homepage'); wp_cache_delete('israel_homepage_all_posts_v8', 'israel_homepage'); // Also clear persistent transient (wp_cache alone is per-request) delete_transient('israel_homepage_all_posts_v6'); delete_transient('israel_homepage_all_posts_v7'); delete_transient('israel_homepage_all_posts_v8'); } // CPU FIX v7: Guard against autosaves/revisions (they don't change titles) function israel_clear_used_titles_on_save($post_id) { if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id) || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)) { return; } // CACHE FIX v11: Always delete the homepage transient, even during MLS imports. // delete_transient is just a single DB DELETE — nearly free, and necessary so // the next visitor rebuilds the homepage with the newly imported article. // Previously, the MLS_IMPORTING guard caused stale transients to persist, // making visitors see old articles even after the Starter Cache file was purged. israel_clear_used_titles(); } add_action('save_post', 'israel_clear_used_titles_on_save'); add_action('delete_post', 'israel_clear_used_titles'); /** * Clear ticker cache when posts are updated */ function israel_clear_ticker_cache($post_id) { // CPU FIX v7: Guard against autosaves/revisions if (is_int($post_id) && (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id))) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // CACHE FIX v11: Removed MLS_IMPORTING guard. Transient deletes are just // single DB DELETEs — nearly free on CPU. Skipping them caused stale ticker // data to persist on slave sites after MLS imports. // Get breaking category (old cache key) $breaking_cat = get_category_by_slug('breaking-only'); if ($breaking_cat) { delete_transient('ticker_posts_' . $breaking_cat->term_id); } // New cache key for tag-aware ticker delete_transient('ticker_posts_breaking_news_v2'); delete_transient('ticker_posts_breaking_news_v3'); } add_action('save_post', 'israel_clear_ticker_cache'); add_action('delete_post', 'israel_clear_ticker_cache'); /** * CRITICAL: Clear all caches and notify Google when post is published * * This ensures articles are immediately accessible for Google News crawling. * Google News requires articles to be accessible within minutes of publishing. * * @param int $post_id Post ID * @param WP_Post $post Post object */ function israel_clear_cache_on_publish($post_id, $post) { // Only process published posts if ($post->post_status !== 'publish') { return; } // Only process post types that should appear in Google News $news_post_types = ['post', 'knesset_news', 'idf_news', 'mod_news']; if (!in_array($post->post_type, $news_post_types)) { return; } // Skip revisions and autosaves if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } // CPU FIX v7: During MLS slave imports, skip heavy operations (Seraphinite, // Cloudflare API, Google/Bing pings, warming) but still purge lightweight // caches so the homepage shows newly imported articles immediately. if ( defined( 'MLS_IMPORTING' ) && MLS_IMPORTING ) { israel_mls_lightweight_homepage_purge( $post_id ); return; } // CPU OPTIMIZATION v3: Targeted cache invalidation instead of wp_cache_flush(). // These lightweight cache deletes always run (they're fast and necessary). wp_cache_delete( 'israel_homepage_all_posts_v3', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v4', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v5', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v6', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v7', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v8', 'israel_homepage' ); wp_cache_delete( 'israel_ticker_data', 'israel_ticker' ); wp_cache_delete( 'israel_breaking_news', 'israel_breaking' ); // Clean post-specific caches clean_post_cache( $post_id ); // WP Rocket — clear only the published post (always, it's targeted and fast) if (function_exists('rocket_clean_post')) { rocket_clean_post($post_id); } // CPU FIX v10: Debounce expensive operations that don't need to run per-article. // rocket_clean_home, Google/Bing pings, and AIOSEO sitemap clears only need to // run ONCE after a batch of publishes, not 5-10 times in 6 seconds. $last_heavy_run = get_transient('israel_publish_heavy_debounce'); if (!$last_heavy_run) { set_transient('israel_publish_heavy_debounce', time(), 30); // WP Rocket home page purge (only once per 30s) if (function_exists('rocket_clean_home')) { rocket_clean_home(); } // Clear homepage cache israel_clear_used_titles(); // Clear ticker cache israel_clear_ticker_cache($post_id); // Clear AIOSEO sitemap cache if (function_exists('aioseo')) { delete_transient('aioseo_sitemap_posts'); delete_transient('aioseo_sitemap_news'); } // Ping Google about new content (non-blocking) $sitemap_url = home_url('/news-sitemap.xml'); wp_remote_get( 'https://www.google.com/ping?sitemap=' . urlencode($sitemap_url), array( 'timeout' => 5, 'blocking' => false, ) ); // Also ping Bing (non-blocking) wp_remote_get( 'https://www.bing.com/ping?sitemap=' . urlencode($sitemap_url), array( 'timeout' => 5, 'blocking' => false, ) ); // Purge all external caches: Seraphinite, Starter Cache, Cloudflare CDN. // This ensures the homepage shows the new article immediately. israel_purge_external_caches_for_homepage( $post_id ); } // Log for debugging (optional) if (defined('WP_DEBUG') && WP_DEBUG) { error_log(sprintf( 'Google News: Cleared all caches for post #%d (%s)', $post_id, get_permalink($post_id) )); } } // Hook into post publishing // Priority changed from 10 to 20 to allow AIOSEO's IndexNow to run first (priority 10) // This prevents cache clearing from interfering with IndexNow URL submissions add_action('publish_post', 'israel_clear_cache_on_publish', 20, 2); add_action('publish_knesset_news', 'israel_clear_cache_on_publish', 20, 2); add_action('publish_idf_news', 'israel_clear_cache_on_publish', 20, 2); add_action('publish_mod_news', 'israel_clear_cache_on_publish', 20, 2); // Log when posts are published (for IndexNow debugging) if (defined('WP_DEBUG') && WP_DEBUG) { add_action('publish_post', function($post_id, $post) { error_log(sprintf( 'IndexNow Debug: 📝 Post #%d published - URL: %s (AIOSEO should submit at priority 10, backup at priority 30)', $post_id, get_permalink($post_id) )); }, 5, 2); // Priority 5 - runs before everything else add_action('publish_knesset_news', function($post_id, $post) { error_log(sprintf( 'IndexNow Debug: 📝 Knesset News #%d published - URL: %s', $post_id, get_permalink($post_id) )); }, 5, 2); add_action('publish_idf_news', function($post_id, $post) { error_log(sprintf( 'IndexNow Debug: 📝 IDF News #%d published - URL: %s', $post_id, get_permalink($post_id) )); }, 5, 2); add_action('publish_mod_news', function($post_id, $post) { error_log(sprintf( 'IndexNow Debug: 📝 MOD News #%d published - URL: %s', $post_id, get_permalink($post_id) )); }, 5, 2); } // CPU FIX v5: REMOVED duplicate transition_post_status hook. // israel_clear_cache_on_publish() already fires via publish_post / publish_knesset_news / etc. // The transition_post_status hook was causing DOUBLE execution of the entire cache-clear // chain (Google/Bing pings, WP Rocket purge, transient deletes) on every publish. // The publish_{post_type} hooks already cover the draft→publish transition. /** * Lightweight homepage cache purge for MLS slave imports. * * During MLS imports, the full cache-clear chain (Seraphinite, Cloudflare API, * Google/Bing pings, warming) is skipped to avoid exhausting PHP-FPM workers. * But Starter Cache homepage + WP object cache MUST be invalidated so visitors * see newly imported articles. These operations are nearly free (in-memory * deletes + file unlink), safe to run per-import with a 10 s debounce. * * @param int $post_id The imported post ID. */ function israel_mls_lightweight_homepage_purge( $post_id ) { $debug = defined( 'WP_DEBUG' ) && WP_DEBUG; // Always invalidate WP object cache — free, in-memory only per request, // but matters if a persistent object cache (Redis) is present. wp_cache_delete( 'israel_homepage_all_posts_v3', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v4', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v5', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v6', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v7', 'israel_homepage' ); wp_cache_delete( 'israel_homepage_all_posts_v8', 'israel_homepage' ); wp_cache_delete( 'israel_ticker_data', 'israel_ticker' ); wp_cache_delete( 'israel_breaking_news', 'israel_breaking' ); clean_post_cache( $post_id ); // CACHE FIX v11: Also delete persistent transients. wp_cache_delete alone only // clears in-memory (per-request) cache — without Redis, subsequent requests still // read the stale transient from wp_options, causing the homepage to show old articles. delete_transient( 'israel_homepage_all_posts_v6' ); delete_transient( 'israel_homepage_all_posts_v7' ); delete_transient( 'israel_homepage_all_posts_v8' ); delete_transient( 'ticker_posts_breaking_news_v2' ); delete_transient( 'ticker_posts_breaking_news_v3' ); // Debounce the Starter Cache file purge: at most once per 10 seconds. // During a batch of 10 imports over 12 s this fires ~2 times, not 10. if ( get_transient( 'israel_mls_homepage_purge_debounce' ) ) { return; } set_transient( 'israel_mls_homepage_purge_debounce', 1, 10 ); $homepage_url = home_url( '/' ); // Starter Cache: delete cached homepage files (desktop + mobile). if ( class_exists( 'Starter_Cache_Plugin' ) && method_exists( 'Starter_Cache_Plugin', 'instance' ) ) { $starter = Starter_Cache_Plugin::instance(); if ( $starter ) { $cache = $starter->get_cache(); if ( $cache && method_exists( $cache, 'purge_url' ) ) { $cache->purge_url( $homepage_url ); // Also purge paginated pages so archives stay fresh. for ( $i = 2; $i <= 5; $i++ ) { $cache->purge_url( home_url( "/page/{$i}/" ) ); } if ( $debug ) { error_log( 'MLS Import Cache Purge: Starter Cache — homepage + paginated pages purged' ); } } } } // Seraphinite (main site only — won't execute on Starter-Cache-only sites). if ( function_exists( 'seraph_accel\\CacheOpUrls' ) ) { try { \seraph_accel\CacheOpUrls( false, array( $homepage_url ), 2 ); if ( $debug ) error_log( 'MLS Import Cache Purge: Seraphinite CacheOpUrls — homepage deleted' ); } catch ( \Throwable $e ) { error_log( 'MLS Import Cache Purge: Seraphinite FAILED — ' . $e->getMessage() ); } } if ( $debug ) { error_log( sprintf( 'MLS Import Cache Purge: Lightweight purge complete for post #%d', $post_id ) ); } } /** * Purge Cloudflare CDN cache for specific URLs via API. * * Uses a BLOCKING call so failures are logged. Timeout is short (5 s) * so it doesn't slow down publishing noticeably. * * Credentials: define CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN in wp-config.php, * or store them as WordPress options (israel_cloudflare_zone_id / israel_cloudflare_api_token). * * @param array $urls URLs to purge from Cloudflare edge cache. * @return bool True if Cloudflare confirmed the purge, false otherwise. */ function israel_purge_cloudflare_urls( $urls = array() ) { $zone_id = defined( 'CLOUDFLARE_ZONE_ID' ) ? CLOUDFLARE_ZONE_ID : get_option( 'israel_cloudflare_zone_id', '' ); $api_token = defined( 'CLOUDFLARE_API_TOKEN' ) ? CLOUDFLARE_API_TOKEN : get_option( 'israel_cloudflare_api_token', '' ); if ( empty( $zone_id ) || empty( $api_token ) || empty( $urls ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'Cloudflare Purge: SKIPPED — missing CLOUDFLARE_ZONE_ID or CLOUDFLARE_API_TOKEN in wp-config.php' ); } return false; } $unique_urls = array_values( array_unique( $urls ) ); $response = wp_remote_request( 'https://api.cloudflare.com/client/v4/zones/' . $zone_id . '/purge_cache', array( 'method' => 'POST', 'timeout' => 5, 'blocking' => false, // Fire-and-forget — don't hold PHP-FPM worker for 5s 'headers' => array( 'Authorization' => 'Bearer ' . $api_token, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'files' => $unique_urls ) ), ) ); // Non-blocking: we can't check the response, but log the intent if ( is_wp_error( $response ) ) { error_log( 'Cloudflare Purge: request dispatch failed — ' . $response->get_error_message() ); return false; } error_log( sprintf( 'Cloudflare Purge: dispatched non-blocking purge for %d URLs: %s', count( $unique_urls ), implode( ', ', $unique_urls ) ) ); return true; } /** * Purge all external cache layers for the homepage (and optionally a post URL). * * Covers every layer in the stack: * 1. Seraphinite Accelerator internal page cache (CacheOpUrls + CacheExt_Clear) * 2. Starter Cache file-based page cache * 3. Cloudflare CDN edge cache via direct API call * 4. Warm the homepage so Seraphinite and Cloudflare rebuild with fresh content * * @param int $post_id Optional post ID whose permalink should also be purged. */ function israel_purge_external_caches_for_homepage( $post_id = 0 ) { $homepage_url = home_url( '/' ); $urls_to_purge = array( $homepage_url ); $debug = defined( 'WP_DEBUG' ) && WP_DEBUG; if ( $post_id ) { $post_url = get_permalink( $post_id ); if ( $post_url ) { $urls_to_purge[] = $post_url; } } // ── 1. Seraphinite Accelerator: delete homepage from internal page cache ── $seraph_purged = false; if ( function_exists( 'seraph_accel\\CacheOpUrls' ) ) { try { // $isExpr = false → exact URL match (not a regex pattern) \seraph_accel\CacheOpUrls( false, array( $homepage_url ), 2 ); $seraph_purged = true; if ( $debug ) error_log( 'Cache Purge: Seraphinite CacheOpUrls — homepage deleted' ); } catch ( \Throwable $e ) { error_log( 'Cache Purge: Seraphinite CacheOpUrls FAILED — ' . $e->getMessage() ); } } if ( function_exists( 'seraph_accel\\CacheExt_Clear' ) ) { try { \seraph_accel\CacheExt_Clear( $homepage_url ); $seraph_purged = true; if ( $debug ) error_log( 'Cache Purge: Seraphinite CacheExt_Clear — homepage external cache cleared' ); } catch ( \Throwable $e ) { error_log( 'Cache Purge: Seraphinite CacheExt_Clear FAILED — ' . $e->getMessage() ); } } // Fallback: if Seraphinite functions aren't loaded, try full cache clear via CacheOp if ( ! $seraph_purged && function_exists( 'seraph_accel\\CacheOp' ) ) { try { \seraph_accel\CacheOp( 0 ); // 0 = revalidate all if ( $debug ) error_log( 'Cache Purge: Seraphinite CacheOp(0) — full revalidation triggered' ); } catch ( \Throwable $e ) { error_log( 'Cache Purge: Seraphinite CacheOp FAILED — ' . $e->getMessage() ); } } if ( ! $seraph_purged && $debug ) { error_log( 'Cache Purge: WARNING — No Seraphinite purge functions available. Is the plugin active?' ); } // ── 2. Starter Cache: purge homepage from file cache ── if ( class_exists( 'Starter_Cache_Plugin' ) && method_exists( 'Starter_Cache_Plugin', 'instance' ) ) { $starter = Starter_Cache_Plugin::instance(); if ( $starter ) { $cache = $starter->get_cache(); if ( $cache && method_exists( $cache, 'purge_url' ) ) { $cache->purge_url( $homepage_url ); if ( $debug ) error_log( 'Cache Purge: Starter Cache — homepage purged' ); } } } // ── 3. Cloudflare CDN: blocking API purge with error logging ── israel_purge_cloudflare_urls( $urls_to_purge ); // ── 4. Warm the homepage so all cache layers rebuild with fresh content ── // This fires a non-blocking GET to the homepage. After Seraphinite + Cloudflare // are purged, this request forces them to re-cache the NEW page. wp_remote_get( $homepage_url, array( 'timeout' => 1, 'blocking' => false, 'user-agent' => 'Israel-Cache-Warmer/1.0', 'headers' => array( 'Cache-Control' => 'no-cache' ), ) ); if ( $debug ) { error_log( sprintf( 'Cache Purge: Complete — purged %d URLs, warming homepage', count( $urls_to_purge ) ) ); } } /** * GUARANTEED IndexNow Submission with Duplicate Prevention * * This runs at priority 30 (after AIOSEO at 10 and cache clearing at 20). * Uses transient-based tracking to prevent duplicates while ensuring 100% submission. * * Strategy: * - Track submissions in WordPress transients (1 hour expiry) * - Submit if URL hasn't been submitted in the last hour * - Works regardless of AIOSEO state * - Guarantees every article is submitted exactly once * * @param int $post_id Post ID * @param WP_Post $post Post object */ function israel_backup_indexnow_submission($post_id, $post) { // CPU FIX v7: Skip during MLS slave import — slave sites have their own IndexNow keys if (defined('MLS_IMPORTING') && MLS_IMPORTING) { return; } // CPU FIX v8: Only master should ping search engines via IndexNow. // Slave sites have content synced FROM master — submitting the same URLs from 7 slaves // wastes 7 redundant HTTP calls per article and provides no SEO benefit. if (function_exists('mls_is_master_site') && !mls_is_master_site()) { return; } // Enable detailed logging for debugging $debug_enabled = defined('WP_DEBUG') && WP_DEBUG; if ($debug_enabled) { error_log(sprintf( 'IndexNow: 🚀 Function triggered for post #%d (type: %s, status: %s)', $post_id, $post->post_type, $post->post_status )); } // Only process published posts if ($post->post_status !== 'publish') { if ($debug_enabled) { error_log(sprintf( 'IndexNow: ⏭️ Skipping post #%d - not published (status: %s)', $post_id, $post->post_status )); } return; } // Only process news post types $news_post_types = ['post', 'knesset_news', 'idf_news', 'mod_news']; if (!in_array($post->post_type, $news_post_types)) { if ($debug_enabled) { error_log(sprintf( 'IndexNow: ⏭️ Skipping post #%d - not a news post type (%s)', $post_id, $post->post_type )); } return; } // Skip revisions and autosaves if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { if ($debug_enabled) { error_log(sprintf( 'IndexNow: ⏭️ Skipping post #%d - revision or autosave', $post_id )); } return; } // Get post URL $url = get_permalink($post_id); // Create a unique transient key for this URL $transient_key = 'indexnow_submitted_' . md5($url); // Check if we've already submitted this URL in the last hour $already_submitted = get_transient($transient_key); if ($already_submitted) { if ($debug_enabled) { $time_ago = human_time_diff($already_submitted, current_time('timestamp')); error_log(sprintf( 'IndexNow: ⏭️ SKIPPING post #%d (%s) - Already submitted %s ago (preventing duplicate)', $post_id, $url, $time_ago )); } return; } if ($debug_enabled) { error_log(sprintf( 'IndexNow: 📤 Processing post #%d - URL: %s (not submitted in last hour)', $post_id, $url )); } // IndexNow configuration (multi-domain safe) $cfg = function_exists('israel_indexnow_get_config') ? israel_indexnow_get_config() : array('enabled' => false); if (empty($cfg['enabled'])) { if ($debug_enabled) { error_log(sprintf( 'IndexNow: ⚠️ Not configured for this site. Set ISRAEL_INDEXNOW_KEY or option israel_indexnow_key. URL: %s', $url )); } return; } $indexnow_url = $cfg['endpoint']; // Prepare IndexNow payload $payload = array( 'host' => $cfg['host'], 'key' => $cfg['key'], 'keyLocation' => $cfg['keyLocation'], 'urlList' => array($url), ); if ($debug_enabled) { error_log(sprintf( 'IndexNow: 🌐 Submitting to %s', $indexnow_url )); } // CPU FIX v5: Changed from blocking to non-blocking. // The blocking call was holding a PHP-FPM worker for up to 10 seconds waiting // for the IndexNow API response. During scraper runs with 5-10 articles publishing // in rapid succession, this could lock up 5-10 PHP-FPM workers simultaneously. // IndexNow accepts fire-and-forget — we don't need the response to confirm. $response = wp_remote_post($indexnow_url, array( 'timeout' => 5, 'blocking' => false, // Fire-and-forget — don't hold PHP-FPM worker 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), 'body' => json_encode($payload) )); // CPU FIX v5: Non-blocking means we can't read the response. // Mark as submitted optimistically — IndexNow is idempotent, so duplicates // are harmless, and the 1-hour transient prevents spamming. set_transient($transient_key, current_time('timestamp'), HOUR_IN_SECONDS); if ($debug_enabled) { error_log(sprintf( 'IndexNow: 📤 Fired non-blocking submission for post #%d (%s)', $post_id, $url )); } } // Hook guaranteed IndexNow submission at priority 15 // This runs AFTER AIOSEO (priority 10) but BEFORE cache clearing (priority 20) // The transient system prevents duplicates regardless of what AIOSEO does add_action('publish_post', 'israel_backup_indexnow_submission', 15, 2); add_action('publish_knesset_news', 'israel_backup_indexnow_submission', 15, 2); add_action('publish_idf_news', 'israel_backup_indexnow_submission', 15, 2); add_action('publish_mod_news', 'israel_backup_indexnow_submission', 15, 2); // IMPORTANT: To prevent potential duplicates with AIOSEO, consider disabling AIOSEO's IndexNow: // Go to: WordPress Admin → All in One SEO → Feature Manager → IndexNow → Deactivate // Our system provides guaranteed submission with duplicate prevention. // IndexNow API can handle duplicates, but it's more efficient to use one system. // ========================================================================== // VIDEO SECTION TRANSIENT CACHE INVALIDATION // PERF FIX: Changed from save_post (fires hundreds of times) to publish_* // hooks that only fire on actual publishes. Added 30-second debounce, // MLS import guard, REST API defer, and post-type filtering. // ========================================================================== function israel_clear_video_transients( $post_id, $post = null ) { // Ignore autosaves and revisions if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) return; if ( $post && $post->post_type === 'revision' ) return; // PERF FIX: Skip during MLS slave import (hooks fire 13+ times per import) if ( defined('MLS_IMPORTING') && MLS_IMPORTING ) return; // PERF FIX: 30-second debounce — scrapers publish 5-10 articles rapidly, // only one cache clear is needed after the batch finishes. $last_run = get_transient('israel_video_cache_debounce'); if ( $last_run ) return; set_transient('israel_video_cache_debounce', time(), 30); // PERF FIX: During REST API (scraper) requests, defer to a scheduled // event so the heavy rebuild runs ONCE after the batch completes. if ( defined('REST_REQUEST') && REST_REQUEST ) { if ( ! wp_next_scheduled('israel_deferred_video_cache_clear') ) { wp_schedule_single_event( time() + 20, 'israel_deferred_video_cache_clear' ); } return; } delete_transient('israel_fp_video_cards'); delete_transient('israel_watch_videos_v2'); delete_transient('israel_watch_videos'); // legacy key cleanup delete_transient('israel_watch_video_ids'); // legacy key cleanup } // Deferred clear for REST/scraper context function israel_do_deferred_video_clear() { delete_transient('israel_fp_video_cards'); delete_transient('israel_watch_videos_v2'); delete_transient('israel_watch_videos'); // legacy key cleanup delete_transient('israel_watch_video_ids'); // legacy key cleanup } add_action('israel_deferred_video_cache_clear', 'israel_do_deferred_video_clear'); // PERF FIX: Only hook publish events for news post types (not save_post) add_action('publish_post', 'israel_clear_video_transients', 20, 2); add_action('publish_knesset_news', 'israel_clear_video_transients', 20, 2); add_action('publish_idf_news', 'israel_clear_video_transients', 20, 2); add_action('publish_mod_news', 'israel_clear_video_transients', 20, 2); add_action('delete_post', 'israel_clear_video_transients', 20, 1); add_action('trashed_post', 'israel_clear_video_transients', 20, 1); // Also clear when a video attachment is uploaded (Strategy D: media library videos) function israel_clear_video_on_attachment( $attachment_id ) { $mime = get_post_mime_type( $attachment_id ); if ( $mime && strpos( $mime, 'video/' ) === 0 ) { delete_transient('israel_fp_video_cards'); delete_transient('israel_watch_videos_v2'); delete_transient('israel_watch_videos'); // legacy key cleanup delete_transient('israel_watch_video_ids'); // legacy key cleanup } } add_action('add_attachment', 'israel_clear_video_on_attachment', 20, 1); add_action('delete_attachment', 'israel_clear_video_on_attachment', 20, 1); // ========================================================================== // VIDEO META FLAG: _has_video // PERF FIX: Replaces the catastrophic LIKE '%post_type === 'revision' ) return; if ( ! in_array( $post->post_type, array('post', 'idf_news', 'knesset_news', 'mod_news'), true ) ) return; $has_video = false; // Check 1: