$v) { $url .= '&' . rawurlencode((string) $k) . '=' . rawurlencode((string) $v); } return $url; } /** True on the plugin's own admin page. */ function citami_is_admin_page() { return isset($_GET['page']) && (string) $_GET['page'] === CITAMI_ADMIN_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only: are we on our own screen } /** Menu icon: a speech mark, the brand's glyph. WordPress recolours SVG data URIs with the admin scheme. */ function citami_admin_menu_icon() { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode($svg); } /** Inline stroke icons for the navigation and cards (16×16, currentColor). */ function citami_ui_icon($name) { $paths = array( 'overview' => '', 'general' => '', 'seo' => '', 'brand' => '', 'bots' => '', 'redirects'=> '', 'local' => '', 'gsc' => '', 'articles' => '', 'links' => '', 'import' => '', 'pending' => '', 'setup' => '', 'check' => '', 'external' => '', 'arrow' => '', 'spark' => '', 'pages' => '', ); if (!isset($paths[$name])) { return ''; } return ''; } /** The side navigation: id, label, icon, optional badge. Overview first. */ function citami_ui_nav_items($pending_count = 0) { // 2.61 — grouped by what the owner does: every week, the site as the AI // engines see it, then setup. The group label is printed before its first item. $work = citami_t('Ogni settimana', 'Every week'); $ai = citami_t('Il sito per le AI', 'The site for AI'); $setup = citami_t('Configurazione', 'Setup'); $items = array( array('id' => 'overview', 'label' => citami_t('Panoramica', 'Overview'), 'icon' => 'overview', 'group' => $work), array('id' => 'pages', 'label' => citami_t('Pagine', 'Pages'), 'icon' => 'pages', 'group' => $work), array('id' => 'gsc', 'label' => 'Search Console', 'icon' => 'gsc', 'group' => $work), array('id' => 'links', 'label' => citami_t('Link interni', 'Internal links'), 'icon' => 'links', 'group' => $work), array('id' => 'articles', 'label' => citami_t('Articoli', 'Articles'), 'icon' => 'articles', 'group' => $work), array('id' => 'brand', 'label' => citami_t('Brand', 'Brand'), 'icon' => 'brand', 'group' => $ai), array('id' => 'bots', 'label' => citami_t('Bot AI', 'AI bots'), 'icon' => 'bots', 'group' => $ai), array('id' => 'redirects', 'label' => citami_t('Redirect', 'Redirects'), 'icon' => 'redirects', 'group' => $ai), array('id' => 'pending', 'label' => citami_t('Fix in coda', 'Pending fixes'), 'icon' => 'pending', 'badge' => (int) $pending_count, 'group' => $ai), array('id' => 'seo', 'label' => 'SEO', 'icon' => 'seo', 'group' => $setup), array('id' => 'local', 'label' => citami_t('Sedi', 'Locations'), 'icon' => 'local', 'group' => $setup), array('id' => 'import', 'label' => citami_t('Importa', 'Import'), 'icon' => 'import', 'group' => $setup), array('id' => 'general', 'label' => citami_t('Impostazioni', 'Settings'), 'icon' => 'general', 'group' => $setup), ); return $items; } /** The tab ids the page accepts (nav items plus the wizard). */ function citami_ui_tab_ids() { $ids = array(); foreach (citami_ui_nav_items() as $it) { $ids[] = $it['id']; } $ids[] = 'setup'; return $ids; } /** AI crawler hits per day for the last N days, oldest first (search engines and spoofed hits excluded). */ function citami_ui_daily_hits($days = 14) { $days = max(1, (int) $days); $log = get_option(CITAMI_BOTS_LOG_OPT, array()); $skip = function_exists('citami_widget_search_only_bots') ? citami_widget_search_only_bots() : array(); $out = array(); $today = strtotime(gmdate('Y-m-d 00:00:00')); for ($i = $days - 1; $i >= 0; $i--) { $out[gmdate('Y-m-d', $today - $i * DAY_IN_SECONDS)] = 0; } foreach ((array) $log as $e) { if (!is_array($e) || !isset($e['ts'], $e['bot'])) { continue; } if (in_array($e['bot'], $skip, true) || (isset($e['verified']) && $e['verified'] === false)) { continue; } $d = gmdate('Y-m-d', (int) $e['ts']); if (isset($out[$d])) { $out[$d]++; } } $series = array(); foreach ($out as $d => $n) { $series[] = array('d' => $d, 'hits' => $n); } return $series; } /** What to do now: the four things that make a site citable, each with its state and where to do it. */ function citami_ui_checklist() { $key = citami_get_api_key() !== ''; $sheet = function_exists('citami_truth_sheet') ? citami_truth_sheet() : array(); $has_sheet = is_array($sheet) && !empty($sheet['name']); $about = function_exists('citami_truth_about_check') ? citami_truth_about_check() : array('found' => false, 'score' => 0); $about_ok = !empty($about['found']) && (int) $about['score'] >= 60; $policy = function_exists('citami_bots_policy') ? citami_bots_policy() : array('rules' => array()); $bots_set = !empty($policy['rules']); $items = array( array('id' => 'sheet', 'done' => $has_sheet, 'href' => $has_sheet ? citami_admin_url('brand') : citami_setup_url(2), 'label' => citami_t('Scheda dei fatti compilata', 'Fact sheet filled in'), 'hint' => citami_t('Nome, tipo, città, telefono: da qui parte lo schema dell\'organizzazione.', 'Name, type, city, phone: the organisation schema starts here.')), array('id' => 'about', 'done' => $about_ok, 'href' => $has_sheet ? citami_setup_url(3) : citami_setup_url(2), 'label' => citami_t('Pagina Chi siamo completa', 'About page complete'), 'hint' => !empty($about['found']) ? sprintf(citami_t('Punteggio %d/100.', 'Score %d/100.'), (int) $about['score']) : citami_t('La pagina che ogni motore legge per capire chi sei.', 'The page every engine reads to learn who you are.')), array('id' => 'bots', 'done' => $bots_set, 'href' => citami_admin_url('bots'), 'label' => citami_t('Crawler AI impostati', 'AI crawlers set'), 'hint' => citami_t('Consigliato: ricerca e assistenti sì, solo addestramento no.', 'Recommended: search and assistants yes, training-only no.')), array('id' => 'connect', 'done' => $key, 'href' => $key ? citami_admin_url('general') : citami_setup_url(1), 'label' => citami_t('Collegato a Citami', 'Connected to Citami'), 'hint' => citami_t('Audit, fix e prova mensile arrivano dalla dashboard.', 'Audits, fixes and the monthly proof come from the dashboard.')), ); $done = 0; foreach ($items as $it) { if ($it['done']) { $done++; } } return array('items' => $items, 'done' => $done, 'total' => count($items)); } /** Everything the Overview shows. */ function citami_ui_overview_data() { $w = function_exists('citami_widget_data') ? citami_widget_data() : array(); $daily = citami_ui_daily_hits(14); $total14 = 0; foreach ($daily as $d) { $total14 += (int) $d['hits']; } $health = get_option(CITAMI_HEALTH_OPT, array()); return array( 'widget' => $w, 'daily' => $daily, 'hits14' => $total14, 'live14' => function_exists('citami_bots_live_reads') ? citami_bots_live_reads(14) : array('total' => 0, 'by' => array()), 'checklist' => citami_ui_checklist(), 'connected' => citami_get_api_key() !== '', 'health_ok' => is_array($health) && !empty($health['ok']), 'health' => is_array($health) ? $health : array(), 'setup_done'=> function_exists('citami_setup_is_done') ? citami_setup_is_done() : true, ); } /** Ring gauge markup (CSS/JS animate it). $value null = no data. */ function citami_ui_ring($value, $label, $sub = '') { $v = $value === null ? 0 : max(0, min(100, (int) $value)); $tone = $value === null ? 'na' : ($v >= 80 ? 'good' : ($v >= 55 ? 'warn' : 'bad')); ob_start(); ?>

' . wp_nonce_field('citami_overview', '_citami_overview_nonce', true, false) . '' . $extra; } /** * Creates a 301 for the invented URLs the owner ticked (2.58: only those — * the list shows each "from → to" before anything is created). null keeps * the old "every URL with a suggested target" behaviour for callers that * have no selection. */ function citami_overview_create_redirects($selected = null) { $done = 0; $skipped = 0; $existing = array_map(function ($r) { return (string) $r['source']; }, citami_redirects_all()); foreach (function_exists('citami_404_ai_entries') ? citami_404_ai_entries(20) : array() as $h) { if (in_array((string) $h['path'], $existing, true)) { continue; } if (is_array($selected) && !in_array((string) $h['path'], $selected, true)) { continue; } if (empty($h['suggested'])) { $skipped++; continue; } $r = citami_redirect_add(array('source' => $h['path'], 'target' => $h['suggested'], 'code' => 301, 'match' => 'exact')); if (is_wp_error($r)) { $skipped++; continue; } $done++; if (function_exists('citami_activity_log')) { citami_activity_log('redirect', sprintf(citami_t('Redirect creato: %1$s → %2$s', 'Redirect created: %1$s → %2$s'), $h['path'], $h['suggested']), 1); } } return array('done' => $done, 'skipped' => $skipped); } function citami_handle_overview_action() { if (!current_user_can('manage_options')) { wp_die('Permesso negato.', '', array('response' => 403)); } if (!wp_verify_nonce(isset($_POST['_citami_overview_nonce']) ? sanitize_text_field(wp_unslash($_POST['_citami_overview_nonce'])) : '', 'citami_overview')) { wp_die('Nonce non valido.', '', array('response' => 403)); } $do = isset($_POST['do']) ? sanitize_key((string) $_POST['do']) : ''; $ids = array_values(array_filter(array_map('intval', explode(',', isset($_POST['ids']) ? sanitize_text_field(wp_unslash($_POST['ids'])) : '')))); if ($do === 'redirects') { $sel = array(); foreach ((array) (isset($_POST['sel']) ? wp_unslash($_POST['sel']) : array()) as $path) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each item sanitized below $path = sanitize_text_field((string) $path); if ($path !== '') $sel[] = $path; } if (empty($sel)) { $notice = array('type' => 'warning', 'message' => citami_t('Nessun redirect selezionato: non ho creato niente.', 'No redirect selected: nothing was created.')); } else { $r = citami_overview_create_redirects($sel); $notice = array('type' => $r['done'] > 0 ? 'success' : 'warning', 'message' => sprintf(citami_t('%1$d redirect creati: li trovi, e li puoi disattivare, nella scheda Redirect.', '%1$d redirects created: you find them, and can switch them off, in the Redirects tab.'), $r['done'])); } } elseif ($do === 'indexnow') { $urls = array(); foreach ($ids as $id) { $u = get_permalink($id); if ($u) $urls[] = $u; } $r = function_exists('citami_indexnow_submit_now') && !empty($urls) ? citami_indexnow_submit_now($urls) : array('added' => 0, 'report' => array()); $notice = array('type' => $r['added'] > 0 ? 'success' : 'warning', 'message' => sprintf(citami_t('%d URL inviati a IndexNow.', '%d URLs sent to IndexNow.'), (int) $r['added'])); } elseif ($do === 'pin') { $before = array_map('intval', (array) get_option('citami_llms_pinned', array())); $pinned = array_slice(array_values(array_unique(array_merge($before, $ids))), 0, 30); update_option('citami_llms_pinned', $pinned, false); // 2.60 — count only the pages this click added (pinning twice is not "N more"). $added = count(array_diff($pinned, $before)); if (function_exists('citami_activity_log') && $added > 0) { citami_activity_log('llms', sprintf(citami_t('%d pagine messe in evidenza in llms.txt', '%d pages pinned in llms.txt'), $added), 1); } $notice = $added > 0 ? array('type' => 'success', 'message' => sprintf(citami_t('%d pagine ora in cima a llms.txt, sezione "Pagine importanti". Le vedi (e le togli) in SEO → llms.txt.', '%d pages now at the top of llms.txt, "Important pages" section. You see (and remove) them in SEO → llms.txt.'), $added)) : array('type' => 'info', 'message' => citami_t('Queste pagine erano già in cima a llms.txt: nessuna aggiunta.', 'These pages were already at the top of llms.txt: nothing added.')); } else { $notice = array('type' => 'error', 'message' => citami_t('Azione sconosciuta.', 'Unknown action.')); } set_transient('citami_action_notice', $notice, 30); wp_safe_redirect(citami_admin_url('overview')); exit; } add_action('admin_post_citami_overview_action', 'citami_handle_overview_action'); function citami_render_overview_tab() { $d = citami_ui_overview_data(); $w = $d['widget']; $a = isset($w['activity']) ? $w['activity'] : array('days' => 7, 'hits' => 0, 'pages' => array(), 'bots' => array()); $s = isset($w['summary']) ? $w['summary'] : array(); $mis = isset($s['misconceptions_open']) && $s['misconceptions_open'] !== null ? (int) $s['misconceptions_open'] : null; $unread = isset($w['unread']) ? $w['unread'] : array('total' => 0, 'unread' => 0, 'sample' => array()); $hall = isset($w['hallucinated']) ? $w['hallucinated'] : array('count' => 0, 'list' => array()); $cl = $d['checklist']; $spark = implode(',', array_map(function ($x) { return (int) $x['hits']; }, $d['daily'])); $spark_labels = implode(',', array_map(function ($x) { return $x['d']; }, $d['daily'])); $bot_label = function ($slug) { return function_exists('citami_widget_bot_label') ? citami_widget_bot_label($slug) : $slug; }; ?>
0): ?>
  • AEO ·

$n): ?>

'assistant', 'days' => 14)) : citami_admin_url('bots'); echo wp_kses(sprintf( /* translators: 1: URL of the filtered log, 2: number of reads */ citami_t('Di queste, %2$s sono letture per rispondere a qualcuno: un assistente AI ha aperto la pagina mentre scriveva una risposta.', 'Of these, %2$s are reads to answer someone: an AI assistant opened the page while writing an answer.'), esc_url($live_url), number_format_i18n((int) $d['live14']['total']) ), array('b' => array(), 'a' => array('href' => true))); ?>

/
0); // 2.60 — the 8 latest; "Mostra tutto (N)" (?activity=all) lists everything stored. $activity_total = function_exists('citami_activity_all') ? count(citami_activity_all()) : 0; $activity_all = !empty($_GET['activity']) && $_GET['activity'] === 'all'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only $recent = function_exists('citami_activity_recent') ? citami_activity_recent($activity_all ? max(1, $activity_total) : 8) : array(); ?>
0 ? sprintf(citami_t('oggi %d azioni automatiche', 'today %d automatic actions'), (int) $today['actions']) : citami_t('oggi ancora niente da segnalare', 'nothing to report yet today')); ?>

· ·

count($recent)): ?>

  • ·
'), citami_kses_allowed()); ?> '), citami_kses_allowed()); ?>
  • /
  • · ·

  • →
array( citami_t('Cosa fare su ogni pagina', 'What to do on each page'), citami_t('Per ogni pagina il plugin incrocia Search Console, le risposte delle AI e le visite dei bot, e ti dice una cosa sola da fare.', 'For each page the plugin crosses Search Console, AI answers and bot visits, and tells you one thing to do.'), array( citami_t('Le pagine che Google mostra ma nessuno clicca, con titolo e descrizione già riscritti', 'Pages Google shows but nobody clicks, with title and description already rewritten'), citami_t('Le domande a cui le AI rispondono senza citarti, e la risposta da aggiungere', 'Questions the AI answers without citing you, and the answer to add'), citami_t('Le pagine che perdono clic, con la bozza di aggiornamento da approvare', 'Pages losing clicks, with an update draft to approve'), ), ), 'gsc' => array( citami_t('Search Console dentro WordPress', 'Search Console inside WordPress'), citami_t('Clic, impressioni e posizioni del sito, e tre liste di azioni pronte.', 'Clicks, impressions and positions of the site, and three lists of ready actions.'), array( citami_t('Title e description da riscrivere, con la bozza già scritta', 'Titles and descriptions to rewrite, with the draft already written'), citami_t('Le domande cercate su Google da aggiungere come FAQ', 'Questions searched on Google to add as FAQ'), citami_t('Le stesse domande tracciate nelle risposte di ChatGPT, Perplexity e Gemini', 'The same questions tracked in ChatGPT, Perplexity and Gemini answers'), ), ), 'articles' => array( citami_t('Articoli pronti da pubblicare', 'Articles ready to publish'), citami_t('Gli articoli scritti sulla piattaforma arrivano qui: un clic e sono in bozza su questo sito.', 'Articles written on the platform land here: one click and they are a draft on this site.'), array( citami_t('Scritti partendo dalle pagine che oggi vengono citate per quella domanda', 'Written from the pages that get cited for that question today'), citami_t('Con immagini, descrizione, FAQ e dati strutturati', 'With images, description, FAQ and structured data'), citami_t('Misurati dopo la pubblicazione: vedi se le AI iniziano a citarli', 'Measured after publishing: you see whether the AI starts citing them'), ), ), 'pending' => array( citami_t('Correzioni dall\'audit', 'Fixes from the audit'), citami_t('Dopo ogni audit la piattaforma prepara le correzioni per il sito; qui le approvi o le rifiuti.', 'After each audit the platform prepares fixes for the site; here you approve or reject them.'), array( citami_t('Dati strutturati, meta tag, llms.txt e robots.txt', 'Structured data, meta tags, llms.txt and robots.txt'), citami_t('Niente cambia senza la tua approvazione, e tutto si può revocare', 'Nothing changes without your approval, and everything can be revoked'), ), ), ); return isset($copy[$feature]) ? $copy[$feature] : null; } function citami_connect_card_html($feature) { $c = citami_connect_card_copy($feature); if ($c === null) { return ''; } $items = ''; foreach ($c[2] as $b) { $items .= '
  • ' . esc_html($b) . '
  • '; } return '
    ' . '' . esc_html(sprintf(citami_t('Con un account %s', 'With a %s account'), citami_brand_name())) . '' . '

    ' . esc_html($c[0]) . '

    ' . '

    ' . esc_html($c[1]) . '

    ' . '
      ' . $items . '
    ' . '

    ' . '' . esc_html(citami_t('Collega con un clic', 'Connect with one click')) . ' ' . '' . esc_html(citami_t('Ho già la chiave', 'I already have a key')) . '' . '

    ' . '

    ' . esc_html(citami_t('Entri o crei un account gratuito con la tua mail: la chiave arriva da sola.', 'Sign in or create a free account with your email: the key arrives by itself.')) . ' ' . esc_html(citami_t('Crea un account gratuito', 'Create a free account')) . '

    ' . '

    ' . esc_html(citami_t('Tutto il resto del plugin (schema, /ai-info, crawler AI, redirect, link interni, punteggio AEO) funziona anche senza account.', 'Everything else in the plugin (schema, /ai-info, AI crawlers, redirects, internal links, AEO score) works without an account too.')) . '

    ' . '
    '; }