'auto'); $s = get_option(CITAMI_INDEXNOW_SETTINGS_OPT, array()); if (is_array($s) && isset($s['enabled']) && in_array($s['enabled'], array('auto', 'on', 'off'), true)) { $d['enabled'] = $s['enabled']; } return $d; } add_action('admin_init', function () { register_setting('citami_seo', CITAMI_INDEXNOW_SETTINGS_OPT, array( 'type' => 'array', 'sanitize_callback' => function ($v) { return array('enabled' => (is_array($v) && isset($v['enabled']) && in_array($v['enabled'], array('auto', 'on', 'off'), true)) ? $v['enabled'] : 'auto'); }, 'default' => array(), )); }); /** auto = on unless Yoast / Rank Math (both ship their own IndexNow) is active. */ function citami_indexnow_enabled() { $s = citami_indexnow_settings(); if ($s['enabled'] === 'on') { return true; } if ($s['enabled'] === 'off') { return false; } return citami_active_seo_plugin() === null; } function citami_indexnow_generate_key() { try { return bin2hex(random_bytes(16)); } catch (Exception $e) { return md5(uniqid('citami', true) . wp_rand()); } } /** The site's key (created on first use). */ function citami_indexnow_key() { $key = (string) get_option(CITAMI_INDEXNOW_KEY_OPT, ''); if (!preg_match('/^[a-f0-9]{32}$/', $key)) { $key = citami_indexnow_generate_key(); update_option(CITAMI_INDEXNOW_KEY_OPT, $key, false); } return $key; } function citami_indexnow_regenerate_key() { $key = citami_indexnow_generate_key(); update_option(CITAMI_INDEXNOW_KEY_OPT, $key, false); return $key; } function citami_indexnow_key_url() { return home_url('/' . citami_indexnow_key() . '.txt'); } /** True when the request is for /{key}.txt (query string ignored). */ function citami_indexnow_is_key_request($uri) { $path = wp_parse_url((string) $uri, PHP_URL_PATH); return $path === '/' . citami_indexnow_key() . '.txt'; } // The key file, served virtually (works on read-only hosts). add_action('init', function () { if (is_admin() || !citami_indexnow_enabled() || empty($_SERVER['REQUEST_URI'])) { return; } if (!citami_indexnow_is_key_request(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])))) { return; } if (!headers_sent()) { header('Content-Type: text/plain; charset=UTF-8'); header('X-Robots-Tag: noindex'); } echo esc_html(citami_indexnow_key()); exit; }, 1); /* ---------------------------------------------------------------- */ /* Queue */ /* ---------------------------------------------------------------- */ function citami_indexnow_host() { return strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); } /** Only absolute URLs on this host are accepted by the engines. */ function citami_indexnow_url_ok($url) { $url = (string) $url; if (!preg_match('#^https?://#i', $url)) { return false; } $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST)); return $host !== '' && $host === citami_indexnow_host(); } function citami_indexnow_queue() { $q = get_option(CITAMI_INDEXNOW_QUEUE_OPT, array()); return is_array($q) ? $q : array(); } /** Adds URLs (deduplicated) and arms the flush. Returns how many were new. */ function citami_indexnow_enqueue($urls, $delay = 30) { if (!citami_indexnow_enabled()) { return 0; } $q = citami_indexnow_queue(); $added = 0; foreach ((array) $urls as $url) { $url = trim((string) $url); if (!citami_indexnow_url_ok($url) || isset($q[$url])) { continue; } if (count($q) >= CITAMI_INDEXNOW_QUEUE_MAX) { break; } $q[$url] = array('ts' => time(), 'attempts' => 0); $added++; } if ($added > 0) { update_option(CITAMI_INDEXNOW_QUEUE_OPT, $q, false); if (!wp_next_scheduled(CITAMI_INDEXNOW_HOOK)) { wp_schedule_single_event(time() + max(0, (int) $delay), CITAMI_INDEXNOW_HOOK); } } return $added; } /** A post's URL, when the engines should hear about it. */ function citami_indexnow_enqueue_post($post_id, $force = false) { $post = get_post($post_id); if (!$post || !in_array($post->post_type, citami_seo_post_types(), true)) { return 0; } if (!$force) { if ($post->post_status !== 'publish') { return 0; } if ((string) get_post_meta($post->ID, CITAMI_META_ROBOTS_INDEX, true) === 'noindex') { return 0; } if (function_exists('post_password_required') && !empty($post->post_password)) { return 0; } } return citami_indexnow_enqueue(array(get_permalink($post))); } // Publish, update, unpublish: one hook covers them all. add_action('transition_post_status', function ($new_status, $old_status, $post) { if (!is_object($post) || empty($post->ID)) { return; } if ($new_status === 'publish') { citami_indexnow_enqueue_post($post->ID); } elseif ($old_status === 'publish' && $new_status !== 'trash') { // Unpublished (draft/private/pending): tell them the URL is gone. citami_indexnow_enqueue_post($post->ID, true); } }, 10, 3); // Slug or parent change: the old address needs a recrawl too. add_action('post_updated', function ($post_id, $after, $before) { if (!is_object($after) || !is_object($before) || $before->post_status !== 'publish') { return; } $old = get_permalink($before); $new = get_permalink($after); if ($old !== $new && in_array($before->post_type, citami_seo_post_types(), true)) { citami_indexnow_enqueue(array($old)); } }, 10, 3); // Trash / delete: the URL must be submitted before it stops resolving. add_action('wp_trash_post', function ($post_id) { citami_indexnow_enqueue_post($post_id, true); }); add_action('before_delete_post', function ($post_id) { $post = get_post($post_id); if ($post && $post->post_status !== 'trash') { citami_indexnow_enqueue_post($post_id, true); } }); // Platform-driven meta changes (POST /citami/v1/page-meta). add_action('citami_page_meta_saved', function ($post_id) { citami_indexnow_enqueue_post($post_id); }); /* ---------------------------------------------------------------- */ /* Flush */ /* ---------------------------------------------------------------- */ function citami_indexnow_log_add(array $entry) { $log = get_option(CITAMI_INDEXNOW_LOG_OPT, array()); if (!is_array($log)) { $log = array(); } array_unshift($log, $entry); update_option(CITAMI_INDEXNOW_LOG_OPT, array_slice($log, 0, CITAMI_INDEXNOW_LOG_MAX), false); } /** * Sends the queue (one batch per call). Returns a report; null when * nothing was sent. Cron calls it; the admin "submit" buttons too. */ function citami_indexnow_flush() { $q = citami_indexnow_queue(); if (empty($q)) { return null; } if (!citami_indexnow_enabled()) { delete_option(CITAMI_INDEXNOW_QUEUE_OPT); return null; } $batch = array_slice(array_keys($q), 0, CITAMI_INDEXNOW_BATCH); $body = array( 'host' => citami_indexnow_host(), 'key' => citami_indexnow_key(), 'keyLocation' => citami_indexnow_key_url(), 'urlList' => array_values($batch), ); $res = wp_remote_post(CITAMI_INDEXNOW_ENDPOINT, array( 'timeout' => 15, 'headers' => array('Content-Type' => 'application/json; charset=utf-8', 'User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . '; ' . home_url('/')), 'body' => wp_json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG), )); $code = is_wp_error($res) ? 0 : (int) wp_remote_retrieve_response_code($res); $ok = in_array($code, array(200, 202), true); if ($ok) { do_action('citami_indexnow_submitted', count($batch)); // 2.42 — the activity log } $retry = $code === 429 || $code >= 500 || $code === 0; $dropped = 0; foreach ($batch as $url) { if ($ok || !$retry) { unset($q[$url]); continue; } $q[$url]['attempts'] = (isset($q[$url]['attempts']) ? (int) $q[$url]['attempts'] : 0) + 1; if ($q[$url]['attempts'] >= CITAMI_INDEXNOW_RETRIES) { unset($q[$url]); $dropped++; } } if (empty($q)) { delete_option(CITAMI_INDEXNOW_QUEUE_OPT); } else { update_option(CITAMI_INDEXNOW_QUEUE_OPT, $q, false); if (!wp_next_scheduled(CITAMI_INDEXNOW_HOOK)) { wp_schedule_single_event(time() + ($retry ? 10 * MINUTE_IN_SECONDS : 30), CITAMI_INDEXNOW_HOOK); } } $report = array( 'ts' => time(), 'count' => count($batch), 'code' => $code, 'status' => $ok ? 'ok' : ($retry ? ($dropped > 0 ? 'dropped' : 'retry') : 'rejected'), 'sample' => array_slice($batch, 0, 3), 'urls' => array_slice($batch, 0, CITAMI_INDEXNOW_LOG_URLS), // 2.60 'error' => is_wp_error($res) ? $res->get_error_message() : '', ); citami_indexnow_log_add($report); return $report; } add_action(CITAMI_INDEXNOW_HOOK, 'citami_indexnow_flush'); /** Enqueue + send right away (admin buttons). */ function citami_indexnow_submit_now($urls) { $added = citami_indexnow_enqueue($urls, 0); $report = citami_indexnow_flush(); return array('added' => $added, 'report' => $report); } /** Every indexable URL the sitemap lists (posts, pages, other public types). */ function citami_indexnow_all_urls($limit = CITAMI_INDEXNOW_BATCH) { $urls = array(home_url('/')); if (!function_exists('citami_sitemap_types')) { return $urls; } foreach (citami_sitemap_types() as $type) { $page = 1; do { $entries = citami_sitemap_entries($type, $page++); foreach ($entries as $e) { $urls[] = $e['loc']; if (count($urls) >= $limit) { return $urls; } } } while (count($entries) === CITAMI_SITEMAP_PER_PAGE); } return $urls; } /** 2.60 — how many URLs "submit the whole sitemap" would send (same list, same cap). */ function citami_indexnow_sitemap_count() { return count(citami_indexnow_all_urls(CITAMI_INDEXNOW_BATCH)); } /* ---------------------------------------------------------------- */ /* Admin: settings row + actions */ /* ---------------------------------------------------------------- */ /** 2.60 — a URL as a site-relative path, for compact lists. */ function citami_indexnow_rel($url) { return str_replace(home_url('/'), '/', (string) $url); } /** 2.60 — URLs as a folded list; $total > count($urls) says how many are not shown. */ function citami_indexnow_urls_details_html(array $urls, $summary, $total = 0, $attr = '') { $urls = array_values(array_filter(array_map('strval', $urls), 'strlen')); $html = '
' . esc_html($summary) . ''; if ((int) $total > count($urls)) { $html .= '

' . esc_html(sprintf(citami_t('Mostrati %1$d su %2$d.', 'Showing %1$d of %2$d.'), count($urls), (int) $total)) . '

'; } return $html . '
'; } /** 2.60 — the URLs of one log row: all of them when stored, else the old 3-URL sample. */ function citami_indexnow_log_urls_html(array $e) { $count = (int) (isset($e['count']) ? $e['count'] : 0); $urls = isset($e['urls']) && is_array($e['urls']) ? $e['urls'] : (isset($e['sample']) ? (array) $e['sample'] : array()); if (empty($urls)) { return (string) $count; } return citami_indexnow_urls_details_html($urls, sprintf(citami_t('%d URL', '%d URLs'), $count), $count, ' data-citami="indexnow-log-urls"'); } /** 2.60 — the queue behind "N URLs queued" (first 50). */ function citami_indexnow_queue_html() { $q = citami_indexnow_queue(); if (empty($q)) { return ''; } $n = count($q); return citami_indexnow_urls_details_html(array_slice(array_keys($q), 0, CITAMI_INDEXNOW_QUEUE_SHOWN), sprintf(citami_t('%d URL in coda', '%d URLs queued'), $n), $n, ' data-citami="indexnow-queue" style="display:inline-block;vertical-align:top"'); } function citami_handle_indexnow_action() { if (!current_user_can('manage_options')) { wp_die('Permesso negato.', '', array('response' => 403)); } if (!wp_verify_nonce(isset($_POST['_citami_indexnow_nonce']) ? sanitize_text_field(wp_unslash($_POST['_citami_indexnow_nonce'])) : '', 'citami_indexnow')) { wp_die('Nonce non valido.', '', array('response' => 403)); } $do = isset($_POST['do']) ? sanitize_key((string) $_POST['do']) : ''; $notice = array('type' => 'success', 'message' => ''); if ($do === 'regenerate') { citami_indexnow_regenerate_key(); $notice['message'] = citami_t('Chiave IndexNow rigenerata.', 'IndexNow key regenerated.'); } elseif ($do === 'submit') { $url = trim(esc_url_raw(wp_unslash($_POST['url'] ?? ''))); if (!citami_indexnow_url_ok($url)) { $notice = array('type' => 'error', 'message' => citami_t('URL non valido: deve essere una pagina di questo sito.', 'Invalid URL: it must be a page of this site.')); } else { $r = citami_indexnow_submit_now(array($url)); $notice = citami_indexnow_notice_from_report($r['report']); } } elseif ($do === 'submit_all') { $r = citami_indexnow_submit_now(citami_indexnow_all_urls()); $notice = citami_indexnow_notice_from_report($r['report']); } elseif ($do === 'clear_log') { delete_option(CITAMI_INDEXNOW_LOG_OPT); $notice['message'] = citami_t('Log svuotato.', 'Log cleared.'); } set_transient('citami_action_notice', $notice, 30); wp_safe_redirect(citami_admin_url('seo', array('open' => 1)) . '#citami-seo-indexnow'); exit; } add_action('admin_post_citami_indexnow_action', 'citami_handle_indexnow_action'); function citami_indexnow_notice_from_report($report) { if ($report === null) { return array('type' => 'error', 'message' => citami_t('Nulla da inviare (IndexNow disattivato o URL già in coda).', 'Nothing to send (IndexNow off or URL already queued).')); } if ($report['status'] === 'ok') { return array('type' => 'success', 'message' => sprintf(citami_t('Inviati %d URL a IndexNow (HTTP %d).', 'Sent %d URLs to IndexNow (HTTP %d).'), $report['count'], $report['code'])); } if ($report['status'] === 'retry') { return array('type' => 'error', 'message' => sprintf(citami_t('IndexNow non raggiungibile (HTTP %d): riprovo tra 10 minuti.', 'IndexNow unreachable (HTTP %d): retrying in 10 minutes.'), $report['code'])); } return array('type' => 'error', 'message' => sprintf(citami_t('IndexNow ha rifiutato l\'invio (HTTP %d). 403 = chiave non raggiungibile su %s.', 'IndexNow rejected the request (HTTP %d). 403 = key not reachable at %s.'), $report['code'], citami_indexnow_key_url())); } add_action('citami_seo_settings_rows', function () { $s = citami_indexnow_settings(); $enabled = citami_indexnow_enabled(); $log = get_option(CITAMI_INDEXNOW_LOG_OPT, array()); $log = is_array($log) ? array_slice($log, 0, 10) : array(); $queue = count(citami_indexnow_queue()); $sitemap_n = $enabled ? citami_indexnow_sitemap_count() : 0; ?>
URLHTTP