'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(sprintf(citami_t('Mostrati %1$d su %2$d.', 'Showing %1$d of %2$d.'), count($urls), (int) $total)) . '' . esc_html($summary) . '
';
foreach ($urls as $u) {
$html .= '
';
if ((int) $total > count($urls)) {
$html .= '
| IndexNow |
|
|---|---|
|
|
| URL | HTTP | ||
|---|---|---|---|