bool. */ function citami_pages_fetch() { $key = citami_get_api_key(); if ($key === '') { return array('ok' => false, 'error' => 'no_key'); } $res = wp_remote_get(citami_get_endpoint() . '/api/plugin/page-insights', array( 'timeout' => 45, 'headers' => array( 'Authorization' => 'Bearer ' . $key, 'User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . '; ' . home_url('/'), ), )); if (is_wp_error($res)) { return array('ok' => false, 'error' => $res->get_error_message()); } $code = (int) wp_remote_retrieve_response_code($res); $data = json_decode((string) wp_remote_retrieve_body($res), true); if ($code < 200 || $code >= 300 || !is_array($data) || !isset($data['pages'])) { return array('ok' => false, 'error' => 'HTTP ' . $code); } $data['ok'] = true; $data['cached_at'] = time(); return $data; } /** Cached insights (6 h). $force refreshes. Failures keep the last good copy. */ function citami_pages_data($force = false) { $cached = get_transient(CITAMI_PAGES_TRANSIENT); if (!$force && is_array($cached)) { return $cached; } $fresh = citami_pages_fetch(); if (!empty($fresh['ok'])) { set_transient(CITAMI_PAGES_TRANSIENT, $fresh, CITAMI_PAGES_TTL); return $fresh; } if (is_array($cached)) { $cached['stale_error'] = isset($fresh['error']) ? (string) $fresh['error'] : ''; return $cached; } $fresh['cached_at'] = time(); set_transient(CITAMI_PAGES_TRANSIENT, $fresh, 10 * MINUTE_IN_SECONDS); return $fresh; } /** Page rows with a verdict to act on, mapped to posts, ordered by priority. */ function citami_pages_rows($limit = CITAMI_PAGES_LIST_MAX) { $data = citami_pages_data(); if (empty($data['ok']) || empty($data['pages'])) { return array(); } $rows = array(); foreach ((array) $data['pages'] as $p) { if (!is_array($p) || empty($p['verdict']['kind']) || $p['verdict']['kind'] === 'ok' || empty($p['url'])) { continue; } $p['post_id'] = function_exists('citami_gsc_post_for_url') ? citami_gsc_post_for_url($p['url']) : (int) url_to_postid($p['url']); $rows[] = $p; if (count($rows) >= $limit) { break; } } return $rows; } function citami_pages_pct($ctr) { return number_format_i18n(100 * (float) $ctr, 1) . '%'; } /** The verdict in one plain sentence, with the numbers behind it. */ function citami_pages_verdict_text(array $p, array $curve = array()) { $v = $p['verdict']; $g = isset($p['gsc']) && is_array($p['gsc']) ? $p['gsc'] : array(); $ai = isset($p['ai']) && is_array($p['ai']) ? $p['ai'] : array(); switch ($v['kind']) { case 'lost_clicks': return sprintf(citami_t('Ha perso clic su Google: %1$d negli ultimi 28 giorni contro %2$d nei 28 prima.', 'Losing clicks on Google: %1$d in the last 28 days against %2$d in the 28 before.'), (int) $g['clicks'], (int) $g['clicksPrev']); case 'low_ctr': $bucket = citami_pages_bucket((float) $g['position']); $typ = ($bucket !== '' && isset($curve[$bucket])) ? citami_pages_pct($curve[$bucket]) : ''; return $typ !== '' ? sprintf(citami_t('Google la mostra %1$s volte ma la cliccano solo il %2$s: le tue pagine in posizione simile arrivano al %3$s.', 'Google shows it %1$s times but only %2$s click: your pages at a similar position reach %3$s.'), number_format_i18n((int) $g['impressions']), citami_pages_pct($g['ctr']), $typ) : sprintf(citami_t('Google la mostra %1$s volte ma la cliccano solo il %2$s.', 'Google shows it %1$s times but only %2$s click.'), number_format_i18n((int) $g['impressions']), citami_pages_pct($g['ctr'])); case 'read_not_cited': $lost = isset($ai['lostQuestions']) ? (array) $ai['lostQuestions'] : array(); $s = sprintf(citami_t('Le AI non ti citano su %1$d domande a cui questa pagina dovrebbe rispondere, per esempio «%2$s».', 'The AI engines do not cite you on %1$d questions this page should answer, e.g. "%2$s".'), count($lost), isset($lost[0]) ? $lost[0] : ''); if (!empty($ai['answerReads'])) { $s .= ' ' . sprintf(citami_t('Gli assistenti l\'hanno aperta %d volte per rispondere a qualcuno: la leggono ma non la citano.', 'Assistants opened it %d times to answer someone: they read it but do not cite it.'), (int) $ai['answerReads']); } return $s; case 'cannibal': $c = isset($v['competing']) && is_array($v['competing']) ? $v['competing'] : array('url' => '', 'query' => ''); return sprintf(citami_t('Compete con un\'altra tua pagina per «%1$s» («%2$s»). Google ne mostra una a scapito dell\'altra.', 'Competes with another of your pages for "%1$s" ("%2$s"). Google shows one at the expense of the other.'), $c['query'], citami_pages_title_for_url((string) $c['url'])); case 'near_top': return sprintf(citami_t('In posizione %1$s con %2$s impressioni: a un passo dalla prima pagina.', 'At position %1$s with %2$s impressions: one step from page one.'), number_format_i18n((float) $g['position'], 1), number_format_i18n((int) $g['impressions'])); case 'not_read_by_ai': return sprintf(citami_t('Google la mostra %s volte, ma nessun bot AI l\'ha aperta negli ultimi 30 giorni.', 'Google shows it %s times, but no AI bot opened it in the last 30 days.'), number_format_i18n((int) $g['impressions'])); } return ''; } function citami_pages_bucket($position) { if ($position <= 0) return ''; if ($position <= 3) return '1-3'; if ($position <= 6) return '4-6'; if ($position <= 10) return '7-10'; if ($position <= 20) return '11-20'; return ''; } function citami_pages_action_label($action) { $labels = array( 'meta_draft' => citami_t('Riscrivi titolo e descrizione con AI', 'Rewrite title and description with AI'), 'refresh_brief' => citami_t('Prepara l\'aggiornamento con AI', 'Prepare the update with AI'), 'aeo_draft' => citami_t('Scrivi la risposta in apertura con AI', 'Write the opening answer with AI'), 'indexnow' => citami_t('Invia ai motori (IndexNow)', 'Send to the engines (IndexNow)'), 'links' => citami_t('Aggiungi link interni', 'Add internal links'), 'review' => citami_t('Confronta le due pagine', 'Compare the two pages'), ); return isset($labels[$action]) ? $labels[$action] : ''; } /* ---------------------------------------------------------------- */ /* Before / after */ /* ---------------------------------------------------------------- */ function citami_pages_actions_log() { $log = get_option(CITAMI_PAGES_ACTIONS_OPT, array()); return is_array($log) ? $log : array(); } /** Remembers an applied action with the page's numbers of that day. */ function citami_pages_log_action($post_id, $kind) { $url = get_permalink((int) $post_id); if (!$url) { return; } $path = rtrim((string) wp_parse_url($url, PHP_URL_PATH), '/'); $path = $path === '' ? '/' : $path; $before = null; $data = get_transient(CITAMI_PAGES_TRANSIENT); foreach ((array) (is_array($data) && isset($data['pages']) ? $data['pages'] : array()) as $p) { if (is_array($p) && isset($p['path']) && $p['path'] === $path && !empty($p['gsc'])) { $before = array('clicks' => (int) $p['gsc']['clicks'], 'impressions' => (int) $p['gsc']['impressions'], 'ctr' => (float) $p['gsc']['ctr'], 'position' => (float) $p['gsc']['position']); break; } } $log = citami_pages_actions_log(); $log[$path] = array('post' => (int) $post_id, 'kind' => (string) $kind, 'ts' => time(), 'before' => $before); if (count($log) > 200) { uasort($log, function ($a, $b) { return $b['ts'] - $a['ts']; }); $log = array_slice($log, 0, 200, true); } update_option(CITAMI_PAGES_ACTIONS_OPT, $log, false); } /** "Since Oct 3: CTR 1.2% → 2.8%, clicks 14 → 31" — or "measuring" in the first two weeks. */ function citami_pages_effect_text(array $p) { $log = citami_pages_actions_log(); if (empty($p['path']) || !isset($log[$p['path']])) { return ''; } $e = $log[$p['path']]; $fmt = (string) get_option('date_format'); $when = date_i18n($fmt !== '' ? $fmt : 'j M Y', (int) $e['ts']); if (time() - (int) $e['ts'] < CITAMI_PAGES_MEASURE_DAYS * DAY_IN_SECONDS || empty($e['before']) || empty($p['gsc'])) { return sprintf(citami_t('Modifica fatta (%s): lo misuriamo, servono 2-4 settimane di dati Google.', 'Change made (%s): being measured, it takes 2-4 weeks of Google data.'), $when); } return sprintf( citami_t('Dopo la modifica (%1$s): CTR %2$s → %3$s, clic %4$d → %5$d nei 28 giorni.', 'After the change (%1$s): CTR %2$s → %3$s, clicks %4$d → %5$d over 28 days.'), $when, citami_pages_pct($e['before']['ctr']), citami_pages_pct($p['gsc']['ctr']), (int) $e['before']['clicks'], (int) $p['gsc']['clicks'] ); } /* ---------------------------------------------------------------- */ /* AI drafts from the platform */ /* ---------------------------------------------------------------- */ function citami_pages_brand() { if (function_exists('citami_truth_sheet')) { $s = citami_truth_sheet(); if (is_array($s) && !empty($s['name'])) { return (string) $s['name']; } } return (string) get_bloginfo('name'); } /** Calls /api/plugin/page-ai for one post; stores the draft. kind: meta | refresh. */ function citami_pages_ai_request($post_id, $kind) { $post_id = (int) $post_id; $kind = $kind === 'refresh' ? 'refresh' : 'meta'; if (!get_post($post_id)) { return array('ok' => false, 'message' => citami_t('Pagina non trovata.', 'Page not found.')); } if (citami_get_api_key() === '') { return array('ok' => false, 'message' => citami_t('Collega prima il sito alla piattaforma (Impostazioni → API key).', 'Connect the site to the platform first (Settings → API key).')); } $page = citami_aeo_draft_page_text($post_id); if (mb_strlen($page['text']) < 80) { return array('ok' => false, 'message' => citami_t('La pagina ha troppo poco testo salvato per lavorarci: aggiungi contenuto e salva.', 'The page has too little saved text to work from: add content and save.')); } $seo = function_exists('citami_seo_get_post_fields') ? citami_seo_get_post_fields($post_id) : array('title' => '', 'description' => ''); $res = wp_remote_post(citami_get_endpoint() . '/api/plugin/page-ai', array( 'timeout' => 60, 'headers' => array( 'Authorization' => 'Bearer ' . citami_get_api_key(), 'Content-Type' => 'application/json', 'User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . '; ' . home_url('/'), ), 'body' => wp_json_encode(array( 'kind' => $kind, 'url' => get_permalink($post_id), 'page_title' => $page['title'] !== '' ? $page['title'] : '(senza titolo)', 'current_title' => (string) $seo['title'], 'current_description' => (string) $seo['description'], 'brand' => citami_pages_brand(), 'text' => mb_substr($page['text'], 0, 10000), 'locale' => citami_lang(), ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG), )); if (is_wp_error($res)) { return array('ok' => false, 'message' => sprintf(citami_t('Piattaforma non raggiungibile (%s).', 'Platform unreachable (%s).'), $res->get_error_message())); } $code = (int) wp_remote_retrieve_response_code($res); $data = json_decode((string) wp_remote_retrieve_body($res), true); if ($code === 429) { return array('ok' => false, 'message' => citami_t('Troppe richieste in un minuto: riprova tra poco.', 'Too many requests in a minute: try again shortly.')); } if ($code < 200 || $code >= 300 || !is_array($data)) { return array('ok' => false, 'message' => sprintf(citami_t('La piattaforma ha risposto con un errore (HTTP %d).', 'The platform answered with an error (HTTP %d).'), $code)); } if (isset($data['reason']) && $data['reason'] === 'quota') { return array('ok' => false, 'upgrade' => 'page_ai', 'message' => sprintf(citami_t('Riscritture AI finite per questo mese (%1$d su %2$d): riprendono il mese prossimo.', 'AI rewrites used up for this month (%1$d of %2$d): back next month.'), (int) $data['used'], (int) $data['quota'])); } if (isset($data['reason']) && $data['reason'] === 'no_queries') { return array('ok' => false, 'message' => citami_t('Search Console non ha ancora ricerche per questa pagina: senza quelle non c\'è su cosa riscrivere titolo e descrizione.', 'Search Console has no queries for this page yet: without them there is nothing to rewrite the title and description on.')); } if ($kind === 'meta') { if (!empty($data['dropped']) || empty($data['title'])) { return array('ok' => false, 'message' => citami_t('Bozza scartata: conteneva numeri o fatti non presenti nella pagina. Riprova o scrivi il titolo a mano.', 'Draft dropped: it carried numbers or facts not on the page. Try again or write the title by hand.')); } $draft = array( 'title' => mb_substr(sanitize_text_field((string) $data['title']), 0, 70), 'description' => mb_substr(sanitize_text_field((string) $data['description']), 0, 170), 'why' => mb_substr(sanitize_text_field((string) (isset($data['why']) ? $data['why'] : '')), 0, 240), // 2.64 — the platform may also propose the page heading (H1 = the post title). 'h1' => mb_substr(sanitize_text_field((string) (isset($data['h1']) ? $data['h1'] : '')), 0, 80), 'query' => isset($data['queries'][0]['query']) ? sanitize_text_field((string) $data['queries'][0]['query']) : '', 'ts' => time(), ); update_post_meta($post_id, CITAMI_META_PAGE_DRAFT, $draft); return array('ok' => true, 'message' => citami_t('Titolo e descrizione proposti: leggili qui sotto e decidi se usarli.', 'Title and description proposed: read them below and decide whether to use them.')); } $sections = array(); foreach ((array) (isset($data['sections']) ? $data['sections'] : array()) as $s) { if (is_array($s) && !empty($s['heading'])) { $sections[] = array('heading' => sanitize_text_field((string) $s['heading']), 'why' => sanitize_text_field((string) (isset($s['why']) ? $s['why'] : ''))); } } $updates = array(); foreach ((array) (isset($data['updates']) ? $data['updates'] : array()) as $u) { if (is_string($u) && trim($u) !== '') { $updates[] = sanitize_text_field($u); } } if (empty($sections) && empty($updates)) { return array('ok' => false, 'message' => citami_t('Dai dati non è emerso un aggiornamento da proporre: la pagina potrebbe perdere clic per stagionalità.', 'The data did not point to an update worth proposing: the page may be losing clicks to seasonality.')); } update_post_meta($post_id, CITAMI_META_PAGE_BRIEF, array( 'summary' => sanitize_text_field((string) (isset($data['summary']) ? $data['summary'] : '')), 'sections' => array_slice($sections, 0, 5), 'updates' => array_slice($updates, 0, 5), 'ts' => time(), )); return array('ok' => true, 'message' => citami_t('Scaletta dell\'aggiornamento pronta: la trovi qui sotto. Aprila accanto all\'editor mentre aggiorni la pagina.', 'Update brief ready: you find it below. Keep it open next to the editor while you update the page.')); } /* ---------------------------------------------------------------- */ /* Actions */ /* ---------------------------------------------------------------- */ /** The Pages tab scrolled to one card. */ function citami_pages_card_url($post_id) { return citami_admin_url('pages', array('focus' => (int) $post_id)) . '#citami-page-' . (int) $post_id; } function citami_pages_action_url($do, $post_id = 0, $back = '') { return wp_nonce_url(admin_url('admin-post.php?action=citami_pages_action&do=' . rawurlencode($do) . ($post_id ? '&post=' . (int) $post_id : '') . ($back === 'overview' ? '&back=overview' : '')), 'citami_pages', '_citami_pages_nonce'); } /** 2.59 — pages with an AI short answer waiting for approval (newest first). */ function citami_pages_pending_aeo_drafts($limit = 10) { if (!defined('CITAMI_META_AEO_DRAFT')) { return array(); } return array_map('intval', get_posts(array( 'post_type' => 'any', 'post_status' => array('publish', 'draft', 'pending', 'future', 'private'), 'meta_key' => CITAMI_META_AEO_DRAFT, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- pages with a draft only 'posts_per_page' => (int) $limit, 'orderby' => 'modified', 'fields' => 'ids', ))); } /** 2.59 — Overview: the drafts written from there, approved there. */ function citami_pages_render_overview_drafts() { $ids = citami_pages_pending_aeo_drafts(); $rows = array(); foreach ($ids as $pid) { $d = citami_aeo_draft_get($pid); if ($d !== null) $rows[$pid] = $d; } if (empty($rows)) { return ''; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only: which draft to highlight $focus = isset($_GET['focus']) ? (int) $_GET['focus'] : 0; $notice = $focus ? get_transient(CITAMI_PAGES_CARD_NOTICE) : null; ob_start(); ?>
$d): $is_focus = $pid === $focus; ?>

403)); } if (!wp_verify_nonce(isset($_REQUEST['_citami_pages_nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['_citami_pages_nonce'])) : '', 'citami_pages')) { wp_die('Nonce non valido.', '', array('response' => 403)); } $do = isset($_REQUEST['do']) ? sanitize_key((string) $_REQUEST['do']) : ''; $post_id = isset($_REQUEST['post']) ? (int) $_REQUEST['post'] : 0; if ($post_id && !current_user_can('edit_post', $post_id)) { wp_die('Permesso negato.', '', array('response' => 403)); } $r = citami_pages_do($do, $post_id); $notice = array('type' => !empty($r['ok']) ? 'success' : 'error', 'message' => $r['message'], 'upgrade' => isset($r['upgrade']) ? $r['upgrade'] : ''); $back = isset($_REQUEST['back']) ? sanitize_key((string) $_REQUEST['back']) : ''; if ($post_id && $back === 'overview') { // 2.59 — started from the Overview: back there, on the draft when one is waiting. if (function_exists('citami_aeo_draft_get') && citami_aeo_draft_get($post_id) !== null) { set_transient(CITAMI_PAGES_CARD_NOTICE, $notice + array('post' => $post_id), 60); wp_safe_redirect(citami_admin_url('overview', array('focus' => $post_id)) . '#citami-draft-' . $post_id); exit; } set_transient('citami_action_notice', $notice, 30); wp_safe_redirect(citami_admin_url('overview')); exit; } if ($post_id) { // 2.58 — back to the same card, scrolled into view, with the outcome // (and the draft, when there is one) inside it, not at the page top. set_transient(CITAMI_PAGES_CARD_NOTICE, $notice + array('post' => $post_id), 60); wp_safe_redirect(citami_pages_card_url($post_id)); exit; } set_transient('citami_action_notice', $notice, 30); wp_safe_redirect(citami_admin_url('pages')); exit; } add_action('admin_post_citami_pages_action', 'citami_handle_pages_action'); /** Runs one action; returns array('ok', 'message'). Split from the handler for tests. */ function citami_pages_do($do, $post_id) { switch ($do) { case 'refresh_data': $d = citami_pages_data(true); return !empty($d['ok']) ? array('ok' => true, 'message' => citami_t('Dati aggiornati.', 'Data refreshed.')) : array('ok' => false, 'message' => citami_t('La piattaforma non ha risposto: restano i dati precedenti.', 'The platform did not answer: the previous data stays.')); case 'meta': return citami_pages_ai_request($post_id, 'meta'); case 'refresh': return citami_pages_ai_request($post_id, 'refresh'); case 'meta_apply': $d = get_post_meta($post_id, CITAMI_META_PAGE_DRAFT, true); if (!is_array($d) || empty($d['title'])) { return array('ok' => false, 'message' => citami_t('Nessuna bozza da usare.', 'No draft to use.')); } $r = citami_gsc_apply_meta($post_id, $d['title'], $d['description'], isset($d['query']) ? $d['query'] : ''); if (!empty($r['ok'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked by the handler if (!empty($_REQUEST['apply_h1']) && !empty($d['h1'])) { $h = citami_pages_apply_h1($post_id, $d['h1']); $r['message'] .= ' ' . $h['message']; } delete_post_meta($post_id, CITAMI_META_PAGE_DRAFT); citami_pages_log_action($post_id, 'meta'); } return $r; case 'meta_discard': delete_post_meta($post_id, CITAMI_META_PAGE_DRAFT); return array('ok' => true, 'message' => citami_t('Bozza scartata.', 'Draft discarded.')); case 'brief_done': delete_post_meta($post_id, CITAMI_META_PAGE_BRIEF); citami_pages_log_action($post_id, 'refresh'); return array('ok' => true, 'message' => citami_t('Segnato come aggiornato: da qui misuriamo l\'effetto.', 'Marked as updated: we measure the effect from here.')); case 'brief_discard': delete_post_meta($post_id, CITAMI_META_PAGE_BRIEF); return array('ok' => true, 'message' => citami_t('Scaletta scartata.', 'Brief discarded.')); case 'aeo': $r = citami_aeo_draft_request($post_id); if (!empty($r['ok'])) { // 2.58 — the draft is shown on the card itself: read it there, then decide. $r['message'] = citami_t('Bozza pronta: la leggi qui sotto. Non è ancora nella pagina: la inserisci solo se ti va bene.', 'Draft ready: read it below. It is not on the page yet: you add it only if you like it.'); } return $r; case 'aeo_apply': return citami_aeo_draft_apply($post_id); case 'aeo_discard': citami_aeo_draft_discard($post_id); return array('ok' => true, 'message' => citami_t('Bozza scartata: la pagina non è cambiata.', 'Draft discarded: the page did not change.')); case 'indexnow': $url = get_permalink($post_id); if (!$url || !function_exists('citami_indexnow_submit_now')) { return array('ok' => false, 'message' => citami_t('IndexNow non disponibile.', 'IndexNow not available.')); } citami_indexnow_submit_now(array($url)); citami_pages_log_action($post_id, 'indexnow'); return array('ok' => true, 'message' => sprintf(citami_t('"%s" inviata a Bing e agli altri motori IndexNow, che alimentano ChatGPT e Copilot.', '"%s" sent to Bing and the other IndexNow engines, which feed ChatGPT and Copilot.'), get_the_title($post_id))); } return array('ok' => false, 'message' => citami_t('Azione sconosciuta.', 'Unknown action.')); } /** * 2.64 — the page heading: in WordPress themes the H1 is the post title, so * changing the H1 means changing the title (the address does not change). * The old title goes in the activity log, to put it back by hand if needed. */ function citami_pages_apply_h1($post_id, $h1) { $post = get_post((int) $post_id); $h1 = trim(sanitize_text_field((string) $h1)); if (!$post || $h1 === '') { return array('ok' => false, 'message' => ''); } $old = (string) $post->post_title; if ($old === $h1) { return array('ok' => true, 'message' => ''); } $r = wp_update_post(array('ID' => (int) $post_id, 'post_title' => $h1), true); if (is_wp_error($r)) { return array('ok' => false, 'message' => citami_t('Il titolo della pagina (H1) non è stato cambiato.', 'The page title (H1) was not changed.')); } if (function_exists('citami_activity_log')) { citami_activity_log('meta', sprintf(citami_t('Titolo della pagina (H1) cambiato: "%1$s" → "%2$s"', 'Page title (H1) changed: "%1$s" → "%2$s"'), $old, $h1), 1, array('post' => (int) $post_id, 'before' => $old)); } return array('ok' => true, 'message' => sprintf(citami_t('Anche il titolo della pagina (H1) ora è "%s".', 'The page title (H1) is now "%s" too.'), $h1)); } /** 2.64 — the H1 proposal row + the "change it too" checkbox, shared by the Pages and Search Console tabs. */ function citami_pages_h1_choice_html($post_id, array $draft, $form = '') { if (empty($draft['h1'])) { return ''; } $cur = get_the_title((int) $post_id); return ''; } /** Logs an applied AI short answer as a page action (called from aeo-draft apply). */ function citami_pages_on_aeo_applied($post_id) { citami_pages_log_action($post_id, 'aeo'); } /* ---------------------------------------------------------------- */ /* Render (2.54.1 design) */ /* ---------------------------------------------------------------- */ function citami_pages_kind_meta($kind) { $m = array( 'lost_clicks' => array('label' => citami_t('Perde clic', 'Losing clicks'), 'tone' => 'bad', 'icon' => ''), 'low_ctr' => array('label' => citami_t('Pochi clic', 'Few clicks'), 'tone' => 'warn', 'icon' => ''), 'read_not_cited' => array('label' => citami_t('Le AI la saltano', 'Skipped by AI'), 'tone' => 'ai', 'icon' => ''), 'near_top' => array('label' => citami_t('Quasi in prima pagina', 'Almost on page one'), 'tone' => 'good', 'icon' => ''), 'not_read_by_ai' => array('label' => citami_t('Le AI non la leggono', 'Not read by AI'), 'tone' => 'ai', 'icon' => ''), 'cannibal' => array('label' => citami_t('Due pagine in conflitto', 'Two pages competing'), 'tone' => 'bad', 'icon' => ''), ); return isset($m[$kind]) ? $m[$kind] : array('label' => $kind, 'tone' => 'ai', 'icon' => ''); } function citami_pages_svg($paths, $class = 'citami-ico') { return ''; } /** The short reason used in the compact weekly list. */ function citami_pages_short_reason(array $p, array $curve) { $v = $p['verdict']; $g = isset($p['gsc']) && is_array($p['gsc']) ? $p['gsc'] : array(); $ai = isset($p['ai']) && is_array($p['ai']) ? $p['ai'] : array(); switch ($v['kind']) { case 'lost_clicks': return sprintf(citami_t('%1$d clic contro %2$d nei 28 giorni prima', '%1$d clicks against %2$d in the 28 days before'), (int) $g['clicks'], (int) $g['clicksPrev']); case 'low_ctr': $b = citami_pages_bucket((float) $g['position']); return ($b !== '' && isset($curve[$b])) ? sprintf(citami_t('CTR %1$s contro il %2$s tipico delle tue pagine', 'CTR %1$s against your pages\' typical %2$s'), citami_pages_pct($g['ctr']), citami_pages_pct($curve[$b])) : sprintf(citami_t('CTR %s con %s impressioni', 'CTR %s on %s impressions'), citami_pages_pct($g['ctr']), number_format_i18n((int) $g['impressions'])); case 'read_not_cited': return sprintf(citami_t('%d domande su cui le AI non ti citano', '%d questions the AI engines skip you on'), count((array) (isset($ai['lostQuestions']) ? $ai['lostQuestions'] : array()))); case 'near_top': return sprintf(citami_t('Posizione %s, %s impressioni', 'Position %s, %s impressions'), number_format_i18n((float) $g['position'], 1), number_format_i18n((int) $g['impressions'])); case 'not_read_by_ai': return citami_t('Nessun bot AI l\'ha aperta in 30 giorni', 'No AI bot opened it in 30 days'); case 'cannibal': return sprintf(citami_t('In conflitto con %s', 'Competing with %s'), citami_pages_title_for_url(isset($v['competing']['url']) ? $v['competing']['url'] : '')); } return ''; } function citami_pages_short_action($action) { $l = array( 'meta_draft' => citami_t('Riscrivi con AI', 'Rewrite with AI'), 'refresh_brief' => citami_t('Prepara l\'aggiornamento', 'Prepare the update'), 'aeo_draft' => citami_t('Scrivi la risposta', 'Write the answer'), 'indexnow' => citami_t('Invia a IndexNow', 'Send to IndexNow'), 'links' => citami_t('Aggiungi link', 'Add links'), 'review' => citami_t('Confronta', 'Compare'), ); return isset($l[$action]) ? $l[$action] : ''; } /** A site URL as its page title (or its path). */ function citami_pages_title_for_url($url) { if ($url === '') return ''; $id = function_exists('citami_gsc_post_for_url') ? citami_gsc_post_for_url($url) : 0; if ($id) return get_the_title($id); $path = (string) wp_parse_url($url, PHP_URL_PATH); return $path !== '' ? $path : $url; } /** The URL or edit link a page's action points to; '' when it is a POST action. */ function citami_pages_action_href(array $p) { $pid = (int) $p['post_id']; $v = $p['verdict']; $map = array('meta_draft' => 'meta', 'refresh_brief' => 'refresh', 'aeo_draft' => 'aeo', 'indexnow' => 'indexnow'); if (isset($map[$v['action']])) return citami_pages_action_url($map[$v['action']], $pid); if ($v['action'] === 'links') return get_edit_post_link($pid, 'raw'); if ($v['action'] === 'review' && !empty($v['competing']['url'])) return $v['competing']['url']; return ''; } /** Metric pills: clicks with delta, CTR vs typical, position, AI questions and reads. */ function citami_pages_metrics(array $p, array $curve) { $g = isset($p['gsc']) && is_array($p['gsc']) ? $p['gsc'] : null; $ai = isset($p['ai']) && is_array($p['ai']) ? $p['ai'] : array(); $out = array(); if ($g) { $delta = ''; if (isset($g['clicksPrev']) && $g['clicksPrev'] !== null && (int) $g['clicksPrev'] > 0) { $d = (int) $g['clicks'] - (int) $g['clicksPrev']; if ($d !== 0) { $pct = round(100 * $d / (int) $g['clicksPrev']); $delta = '' . ($d < 0 ? '▼' : '▲') . ' ' . esc_html(($d > 0 ? '+' : '') . $pct . '%') . ''; } } $out[] = '' . esc_html(citami_t('Clic', 'Clicks')) . '' . esc_html(number_format_i18n((int) $g['clicks'])) . '' . $delta . ''; $b = citami_pages_bucket((float) $g['position']); $typ = ($b !== '' && isset($curve[$b])) ? (float) $curve[$b] : null; $ctr = (float) $g['ctr']; $bar = ''; if ($typ !== null && $typ > 0) { $w = max(4, min(100, round(100 * $ctr / max($typ, $ctr)))); $bar = '' . esc_html(sprintf(citami_t('tipico %s', 'typical %s'), citami_pages_pct($typ))) . ''; } $out[] = 'CTR' . esc_html(citami_pages_pct($ctr)) . '' . $bar . ''; $out[] = '' . esc_html(citami_t('Posizione', 'Position')) . '' . esc_html(number_format_i18n((float) $g['position'], 1)) . ''; $out[] = '' . esc_html(citami_t('Impressioni', 'Impressions')) . '' . esc_html(number_format_i18n((int) $g['impressions'])) . ''; } $lost = count((array) (isset($ai['lostQuestions']) ? $ai['lostQuestions'] : array())); $cited = count((array) (isset($ai['citedQuestions']) ? $ai['citedQuestions'] : array())); if ($lost > 0 || $cited > 0) { $out[] = '' . esc_html(citami_t('Domande AI', 'AI questions')) . '' . esc_html(($cited === 1 ? citami_t('1 citata', '1 cited') : sprintf(citami_t('%d citate', '%d cited'), $cited)) . ' · ' . ($lost === 1 ? citami_t('1 persa', '1 lost') : sprintf(citami_t('%d perse', '%d lost'), $lost))) . ''; } $reads = (int) (isset($ai['answerReads']) ? $ai['answerReads'] : 0); if ($reads > 0) { $out[] = '' . esc_html(citami_t('Letta dalle AI', 'Read by AI')) . '' . esc_html(sprintf(citami_t('%d× per rispondere', '%d× to answer'), $reads)) . ''; } return empty($out) ? '' : '
' . implode('', $out) . '
'; } /* ---------------------------------------------------------------- */ /* 2.58 — what the AI said, and the AI draft, on the card */ /* ---------------------------------------------------------------- */ function citami_pages_engine_label($engine) { $l = array('chatgpt' => 'ChatGPT', 'openai' => 'ChatGPT', 'perplexity' => 'Perplexity', 'gemini' => 'Gemini', 'google_ai' => 'Google AI', 'claude' => 'Claude', 'copilot' => 'Copilot', 'grok' => 'Grok'); $k = strtolower((string) $engine); return isset($l[$k]) ? $l[$k] : ucfirst($k); } /** The AI questions behind the "AI questions" pill: [lost[], cited[]], each array('q', 'engines', 'instead'). */ function citami_pages_question_rows(array $p) { $ai = isset($p['ai']) && is_array($p['ai']) ? $p['ai'] : array(); $lost = array(); $cited = array(); if (!empty($ai['questions']) && is_array($ai['questions'])) { foreach ($ai['questions'] as $q) { if (!is_array($q) || empty($q['q'])) continue; $row = array('q' => (string) $q['q'], 'engines' => array_map('strval', (array) (isset($q['engines']) ? $q['engines'] : array())), 'instead' => isset($q['instead']) ? (string) $q['instead'] : ''); if (isset($q['status']) && $q['status'] === 'cited') $cited[] = $row; else $lost[] = $row; } return array($lost, $cited); } // Older platform: the plain lists. foreach ((array) (isset($ai['lostQuestions']) ? $ai['lostQuestions'] : array()) as $q) { if (is_string($q) && $q !== '') $lost[] = array('q' => $q, 'engines' => array(), 'instead' => ''); } foreach ((array) (isset($ai['citedQuestions']) ? $ai['citedQuestions'] : array()) as $q) { if (is_string($q) && $q !== '') $cited[] = array('q' => $q, 'engines' => array(), 'instead' => ''); } return array($lost, $cited); } /** The questions themselves, lost first: what the engines answer without you, and where they do cite you. */ function citami_pages_questions_html(array $p, $open = false) { list($lost, $cited) = citami_pages_question_rows($p); if (empty($lost) && empty($cited)) { return ''; } $h = '
'; $n_lost = count($lost) === 1 ? citami_t('1 persa', '1 lost') : sprintf(citami_t('%d perse', '%d lost'), count($lost)); $n_cited = count($cited) === 1 ? citami_t('1 citata', '1 cited') : sprintf(citami_t('%d citate', '%d cited'), count($cited)); $h .= '' . esc_html(sprintf(citami_t('Quali domande: %1$s, %2$s', 'Which questions: %1$s, %2$s'), $n_lost, $n_cited)) . ''; if (!empty($lost)) { $h .= '

' . esc_html(citami_t('Le AI rispondono senza citarti', 'The AI engines answer without citing you')) . '

    '; foreach (array_slice($lost, 0, 10) as $q) { $h .= '
  • «' . esc_html($q['q']) . '»'; if ($q['instead'] !== '') { $h .= ' ' . esc_html(sprintf(citami_t('citano invece %s', 'they cite %s instead'), $q['instead'])) . ''; } $h .= '
  • '; } $h .= '
'; } if (!empty($cited)) { $h .= '

' . esc_html(citami_t('Le AI citano questa pagina', 'The AI engines cite this page')) . '

    '; foreach (array_slice($cited, 0, 10) as $q) { $h .= '
  • «' . esc_html($q['q']) . '»'; if (!empty($q['engines'])) { $h .= ' ' . esc_html(implode(', ', array_map('citami_pages_engine_label', $q['engines']))) . ''; } $h .= '
  • '; } $h .= '
'; } $base = function_exists('citami_brand_site') ? citami_brand_site() : citami_get_endpoint(); $h .= '

' . esc_html(citami_t('Leggi le risposte complete nella dashboard', 'Read the full answers in the dashboard')) . '

'; return $h . '
'; } /** The pending AI short answer + FAQ, with "now" next to it and the three choices. */ function citami_pages_aeo_draft_html($pid, array $draft, $back = '') { $cur = function_exists('citami_content_tldr') ? (string) citami_content_tldr($pid) : ''; $h = '
'; $h .= '
' . citami_ui_icon('spark') . ' ' . esc_html(citami_t('Proposta AI: risposta in apertura e FAQ', 'AI proposal: opening answer and FAQ')) . ' · ' . esc_html(citami_t('non è ancora nella pagina', 'not on the page yet')) . '
'; if ($draft['tldr'] !== '') { $h .= '
'; $h .= '
' . esc_html(citami_t('Ora', 'Now')) . '

' . ($cur !== '' ? esc_html($cur) : '' . esc_html(citami_t('nessuna risposta in apertura', 'no opening answer')) . '') . '

'; $h .= '
' . esc_html(citami_t('Proposta', 'Proposed')) . '

' . esc_html($draft['tldr']) . '

'; $h .= '
'; } if (!empty($draft['faq'])) { $h .= '' . esc_html(sprintf(citami_t('%d FAQ da aggiungere in fondo alla pagina', '%d FAQ to add at the bottom of the page'), count($draft['faq']))) . '
'; foreach ($draft['faq'] as $it) { $h .= '
' . esc_html($it['q']) . '
' . esc_html($it['a']) . '
'; } $h .= '
'; } $h .= '

' . esc_html(citami_t('Scritta solo dal testo della pagina. "Inserisci" la scrive nella pagina e la salva; le FAQ già presenti restano.', 'Written from the page text only. "Add to the page" writes it into the page and saves it; existing FAQ stay.')) . '

'; $h .= '
'; } /** One page card: verdict, numbers, the action, drafts, effect of past changes. */ function citami_pages_render_card(array $p, array $curve, $compact = false) { $v = $p['verdict']; $pid = (int) $p['post_id']; $km = citami_pages_kind_meta($v['kind']); $title = $pid ? get_the_title($pid) : $p['path']; $draft = $pid ? get_post_meta($pid, CITAMI_META_PAGE_DRAFT, true) : null; $brief = $pid ? get_post_meta($pid, CITAMI_META_PAGE_BRIEF, true) : null; $has_draft = is_array($draft) && !empty($draft['title']); $has_brief = is_array($brief) && (!empty($brief['sections']) || !empty($brief['updates'])); $aeo = ($pid && function_exists('citami_aeo_draft_get')) ? citami_aeo_draft_get($pid) : null; $href = $pid ? citami_pages_action_href($p) : ''; $effect = citami_pages_effect_text($p); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only: which card to highlight $focus = $pid && isset($_GET['focus']) && (int) $_GET['focus'] === $pid; $notice = $focus ? get_transient(CITAMI_PAGES_CARD_NOTICE) : null; if (is_array($notice) && (int) $notice['post'] === $pid) { delete_transient(CITAMI_PAGES_CARD_NOTICE); } else { $notice = null; } ob_start(); ?>
>

'), citami_kses_allowed()); ?>

CTR
'', 'description' => ''); $cur_t = $seo['title'] !== '' ? $seo['title'] : get_the_title($pid); $cur_d = $seo['description']; ?>
·

' . (int) mb_strlen($cur_d) . '' : '' . esc_html(citami_t('nessuna descrizione: Google ne inventa una', 'no description: Google makes one up')) . ''; ?>

  1. H2

0): ?>
+
0): ?>
/

  1. · +