s). */ /* ────────────────────────────────────────────────────────────────── */ define('CITAMI_SEO_SETTINGS_OPT', 'citami_seo_settings'); define('CITAMI_META_TITLE', '_citami_seo_title'); define('CITAMI_META_DESC', '_citami_seo_description'); define('CITAMI_META_CANONICAL', '_citami_canonical'); define('CITAMI_META_ROBOTS_INDEX', '_citami_robots_index'); define('CITAMI_META_ROBOTS_FOLLOW', '_citami_robots_follow'); define('CITAMI_META_ROBOTS_ADV', '_citami_robots_advanced'); define('CITAMI_SEO_NONCE', 'citami_seo_meta_nonce'); /* ---------------------------------------------------------------- */ /* Settings */ /* ---------------------------------------------------------------- */ function citami_seo_default_settings() { return array( // 'auto' = on unless another SEO plugin is active; 'on' / 'off' force. 'enabled' => 'auto', 'separator' => '–', 'home' => array( 'title' => '%sitename% %sep% %tagline%', 'description' => '%tagline%', ), 'templates' => array( 'post' => array('title' => '%title% %sep% %sitename%', 'description' => '%excerpt%'), 'page' => array('title' => '%title% %sep% %sitename%', 'description' => '%excerpt%'), ), 'archive' => array( 'title' => '%term% %sep% %sitename%', 'description' => '', ), 'noindex' => array( 'search' => true, 'attachment' => true, 'author' => false, 'date' => true, 'paginated' => false, ), ); } function citami_seo_settings() { $defaults = citami_seo_default_settings(); $stored = get_option(CITAMI_SEO_SETTINGS_OPT, array()); if (!is_array($stored)) { $stored = array(); } $out = $defaults; foreach (array('enabled', 'separator') as $k) { if (isset($stored[$k]) && is_string($stored[$k])) { $out[$k] = $stored[$k]; } } foreach (array('home', 'archive') as $k) { if (isset($stored[$k]) && is_array($stored[$k])) { $out[$k] = array_merge($defaults[$k], array_intersect_key($stored[$k], $defaults[$k])); } } if (isset($stored['templates']) && is_array($stored['templates'])) { foreach ($stored['templates'] as $type => $tpl) { if (!is_array($tpl)) { continue; } $out['templates'][$type] = array( 'title' => isset($tpl['title']) ? (string) $tpl['title'] : '', 'description' => isset($tpl['description']) ? (string) $tpl['description'] : '', ); } } if (isset($stored['noindex']) && is_array($stored['noindex'])) { foreach ($defaults['noindex'] as $k => $v) { if (array_key_exists($k, $stored['noindex'])) { $out['noindex'][$k] = (bool) $stored['noindex'][$k]; } } } return $out; } /** Settings sanitizer (register_setting). Templates: text only. */ function citami_sanitize_seo_settings($value) { $defaults = citami_seo_default_settings(); $in = is_array($value) ? $value : array(); $out = array(); $enabled = isset($in['enabled']) ? (string) $in['enabled'] : 'auto'; $out['enabled'] = in_array($enabled, array('auto', 'on', 'off'), true) ? $enabled : 'auto'; $sep = isset($in['separator']) ? wp_strip_all_tags((string) $in['separator']) : $defaults['separator']; $out['separator'] = $sep === '' ? $defaults['separator'] : mb_substr($sep, 0, 5); foreach (array('home', 'archive') as $k) { $out[$k] = array( 'title' => isset($in[$k]['title']) ? citami_seo_sanitize_template($in[$k]['title']) : $defaults[$k]['title'], 'description' => isset($in[$k]['description']) ? citami_seo_sanitize_template($in[$k]['description']) : $defaults[$k]['description'], ); } $out['templates'] = array(); if (isset($in['templates']) && is_array($in['templates'])) { foreach ($in['templates'] as $type => $tpl) { $type = sanitize_key((string) $type); if ($type === '' || !is_array($tpl)) { continue; } $out['templates'][$type] = array( 'title' => citami_seo_sanitize_template(isset($tpl['title']) ? $tpl['title'] : ''), 'description' => citami_seo_sanitize_template(isset($tpl['description']) ? $tpl['description'] : ''), ); } } $out['noindex'] = array(); foreach ($defaults['noindex'] as $k => $v) { $out['noindex'][$k] = !empty($in['noindex'][$k]); } return $out; } function citami_seo_sanitize_template($tpl) { $tpl = wp_strip_all_tags((string) $tpl); $tpl = preg_replace('/\s+/u', ' ', $tpl); return trim(mb_substr($tpl, 0, 300)); } /** Is the per-page meta output active for this site? */ function citami_seo_enabled() { $settings = citami_seo_settings(); if ($settings['enabled'] === 'on') { return true; } if ($settings['enabled'] === 'off') { return false; } // auto: step aside for Yoast / Rank Math (they own and meta). return citami_active_seo_plugin() === null; } /* ---------------------------------------------------------------- */ /* Templates */ /* ---------------------------------------------------------------- */ /** Variables available to templates for the given context. */ function citami_seo_vars($ctx) { $settings = citami_seo_settings(); $vars = array( 'sitename' => get_bloginfo('name'), 'tagline' => get_bloginfo('description'), 'sep' => $settings['separator'], 'title' => '', 'excerpt' => '', 'category' => '', 'author' => '', 'date' => '', 'term' => '', 'page' => '', 'search' => '', ); if (!empty($ctx['post']) && is_object($ctx['post'])) { $post = $ctx['post']; $vars['title'] = isset($post->post_title) ? (string) $post->post_title : ''; $vars['excerpt'] = citami_seo_post_excerpt($post); if (function_exists('get_the_author_meta') && !empty($post->post_author)) { $vars['author'] = (string) get_the_author_meta('display_name', (int) $post->post_author); } if (!empty($post->post_date)) { $vars['date'] = date_i18n(get_option('date_format', 'Y-m-d'), strtotime((string) $post->post_date)); } if (function_exists('get_the_category')) { $cats = get_the_category($post->ID); if (is_array($cats) && !empty($cats) && isset($cats[0]->name)) { $vars['category'] = (string) $cats[0]->name; } } } if (!empty($ctx['term'])) { $vars['term'] = (string) $ctx['term']; } if (!empty($ctx['page']) && (int) $ctx['page'] > 1) { $vars['page'] = sprintf(citami_t('Pagina %d', 'Page %d'), (int) $ctx['page']); } if (!empty($ctx['search'])) { $vars['search'] = (string) $ctx['search']; } return $vars; } /** First ~155 chars of the excerpt or the content, tags stripped. */ function citami_seo_post_excerpt($post) { $text = ''; if (!empty($post->post_excerpt)) { $text = (string) $post->post_excerpt; } else { // 2.31.1 — builder-aware: Elementor pages have an empty post_content. $text = citami_post_content_html($post); } // 2.29.1 — seen on a real site: "…cosciente.Quanto costa" — block // comments dropped, block boundaries become spaces, entities decoded, // the cut lands on a word boundary. $text = preg_replace('/<!--.*?-->/s', ' ', citami_strip_shortcodes_safe($text)); $text = preg_replace('#</?(p|h[1-6]|li|ul|ol|div|br|tr|td|th|table|blockquote|figure|figcaption|section|article|details|summary|pre)\b[^>]*>#i', ' ', $text); $text = html_entity_decode(wp_strip_all_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/\s+/u', ' ', $text); $text = trim($text); if (mb_strlen($text) > 155) { $cut = mb_substr($text, 0, 152); $sp = mb_strrpos($cut, ' '); if ($sp !== false && $sp > 100) { $cut = mb_substr($cut, 0, $sp); } $text = rtrim($cut, " ,;:") . '…'; } return $text; } /** strip_shortcodes() when WP is loaded; a regex fallback in the harness. */ function citami_strip_shortcodes_safe($text) { if (function_exists('strip_shortcodes')) { return strip_shortcodes($text); } return preg_replace('/\[[^\]]+\]/', '', $text); } /** * Renders "%title% %sep% %sitename%" with the vars. Empty vars collapse * cleanly: dangling separators and double spaces are removed. */ function citami_seo_render_template($tpl, array $vars) { $out = (string) $tpl; foreach ($vars as $k => $v) { $out = str_replace('%' . $k . '%', (string) $v, $out); } $out = preg_replace('/%[a-z_]+%/', '', $out); // unknown vars $sep = isset($vars['sep']) ? preg_quote((string) $vars['sep'], '/') : ''; if ($sep !== '') { $out = preg_replace('/(\s*' . $sep . '\s*){2,}/u', ' ' . $vars['sep'] . ' ', $out); $out = preg_replace('/^\s*' . $sep . '\s*|\s*' . $sep . '\s*$/u', '', $out); } $out = preg_replace('/\s+/u', ' ', $out); return trim(wp_strip_all_tags($out)); } /* ---------------------------------------------------------------- */ /* Context + resolution */ /* ---------------------------------------------------------------- */ /** What is being rendered: singular post, home, archive, search, 404. */ function citami_seo_context() { $ctx = array('kind' => 'other', 'post' => null, 'type' => '', 'term' => '', 'page' => 1, 'search' => ''); if (function_exists('get_query_var')) { $paged = (int) get_query_var('paged'); $ctx['page'] = $paged > 1 ? $paged : 1; } if (is_front_page() || is_home()) { $ctx['kind'] = 'home'; if (is_front_page() && is_singular()) { $ctx['post'] = get_post(); } return $ctx; } if (is_search()) { $ctx['kind'] = 'search'; $ctx['search'] = function_exists('get_search_query') ? (string) get_search_query() : ''; return $ctx; } if (is_404()) { $ctx['kind'] = '404'; return $ctx; } if (is_singular()) { $post = get_post(); if ($post) { $ctx['kind'] = 'singular'; $ctx['post'] = $post; $ctx['type'] = isset($post->post_type) ? (string) $post->post_type : 'post'; return $ctx; } } if (is_archive()) { $ctx['kind'] = 'archive'; $obj = get_queried_object(); if (is_object($obj) && isset($obj->name)) { $ctx['term'] = (string) $obj->name; } elseif (is_object($obj) && isset($obj->display_name)) { $ctx['term'] = (string) $obj->display_name; } return $ctx; } return $ctx; } /** Resolved <title> for the current request (null = leave WordPress'). */ function citami_seo_title($ctx = null) { $ctx = $ctx === null ? citami_seo_context() : $ctx; $settings = citami_seo_settings(); $vars = citami_seo_vars($ctx); if ($ctx['kind'] === 'singular' && $ctx['post']) { $custom = (string) get_post_meta($ctx['post']->ID, CITAMI_META_TITLE, true); if (trim($custom) !== '') { return citami_seo_render_template($custom, $vars); } $tpl = isset($settings['templates'][$ctx['type']]['title']) ? $settings['templates'][$ctx['type']]['title'] : '%title% %sep% %sitename%'; return citami_seo_render_template($tpl, $vars); } if ($ctx['kind'] === 'home') { if ($ctx['post']) { $custom = (string) get_post_meta($ctx['post']->ID, CITAMI_META_TITLE, true); if (trim($custom) !== '') { return citami_seo_render_template($custom, $vars); } } return citami_seo_render_template($settings['home']['title'], $vars); } if ($ctx['kind'] === 'archive' && $ctx['term'] !== '') { return citami_seo_render_template($settings['archive']['title'], $vars); } if ($ctx['kind'] === 'search') { return citami_seo_render_template('%search% %sep% %sitename%', $vars); } return null; } /** Resolved meta description (empty = print nothing). */ function citami_seo_description($ctx = null) { $ctx = $ctx === null ? citami_seo_context() : $ctx; $settings = citami_seo_settings(); $vars = citami_seo_vars($ctx); if (($ctx['kind'] === 'singular' || $ctx['kind'] === 'home') && $ctx['post']) { $custom = (string) get_post_meta($ctx['post']->ID, CITAMI_META_DESC, true); if (trim($custom) !== '') { return citami_seo_render_template($custom, $vars); } } if ($ctx['kind'] === 'singular') { $tpl = isset($settings['templates'][$ctx['type']]['description']) ? $settings['templates'][$ctx['type']]['description'] : '%excerpt%'; return citami_seo_render_template($tpl, $vars); } if ($ctx['kind'] === 'home') { return citami_seo_render_template($settings['home']['description'], $vars); } if ($ctx['kind'] === 'archive') { return citami_seo_render_template($settings['archive']['description'], $vars); } return ''; } /** Canonical URL: custom meta wins, else the permalink (singular only). */ function citami_seo_canonical($ctx = null) { $ctx = $ctx === null ? citami_seo_context() : $ctx; if (($ctx['kind'] === 'singular' || $ctx['kind'] === 'home') && $ctx['post']) { $custom = trim((string) get_post_meta($ctx['post']->ID, CITAMI_META_CANONICAL, true)); if ($custom !== '' && preg_match('#^https?://#i', $custom)) { return $custom; } return $ctx['kind'] === 'home' ? home_url('/') : get_permalink($ctx['post']); } if ($ctx['kind'] === 'home') { return home_url('/'); } return ''; } /** Robots directives as the wp_robots filter expects (name => true). */ function citami_seo_robots(array $robots, $ctx = null) { $ctx = $ctx === null ? citami_seo_context() : $ctx; $settings = citami_seo_settings(); $noindex = false; $nofollow = false; $advanced = array(); if ($ctx['kind'] === 'search' && $settings['noindex']['search']) { $noindex = true; } if ($ctx['kind'] === 'archive' && $settings['noindex']['date'] && function_exists('is_date') && is_date()) { $noindex = true; } if ($ctx['kind'] === 'archive' && $settings['noindex']['author'] && function_exists('is_author') && is_author()) { $noindex = true; } if ($ctx['page'] > 1 && $settings['noindex']['paginated']) { $noindex = true; } if ($ctx['post']) { if ($settings['noindex']['attachment'] && isset($ctx['post']->post_type) && $ctx['post']->post_type === 'attachment') { $noindex = true; } $idx = (string) get_post_meta($ctx['post']->ID, CITAMI_META_ROBOTS_INDEX, true); if ($idx === 'noindex') { $noindex = true; } elseif ($idx === 'index') { $noindex = false; } $fol = (string) get_post_meta($ctx['post']->ID, CITAMI_META_ROBOTS_FOLLOW, true); if ($fol === 'nofollow') { $nofollow = true; } $adv = (string) get_post_meta($ctx['post']->ID, CITAMI_META_ROBOTS_ADV, true); foreach (array_filter(array_map('trim', explode(',', $adv))) as $d) { if (in_array($d, array('noarchive', 'nosnippet', 'noimageindex', 'notranslate'), true)) { $advanced[] = $d; } } } if ($noindex) { unset($robots['index']); $robots['noindex'] = true; } if ($nofollow) { unset($robots['follow']); $robots['nofollow'] = true; } foreach ($advanced as $d) { $robots[$d] = true; } if (!$noindex && !isset($robots['noindex']) && !isset($robots['index'])) { // WordPress' default "max-image-preview:large" stays; be explicit. $robots['index'] = true; } if (!$nofollow && !isset($robots['nofollow']) && !isset($robots['follow'])) { $robots['follow'] = true; } return $robots; } /* ---------------------------------------------------------------- */ /* Front-end output */ /* ---------------------------------------------------------------- */ add_filter('pre_get_document_title', function ($title) { if (!citami_seo_enabled()) { return $title; } $ours = citami_seo_title(); return ($ours !== null && $ours !== '') ? $ours : $title; }, 15); add_filter('wp_robots', function ($robots) { if (!citami_seo_enabled()) { return $robots; } return citami_seo_robots(is_array($robots) ? $robots : array()); }, 15); /** Prints description + canonical. Runs early so it sits at the top of <head>. */ function citami_seo_print_head() { if (!citami_seo_enabled()) { return; } $ctx = citami_seo_context(); $desc = citami_seo_description($ctx); if ($desc !== '') { echo '<meta name="description" content="' . esc_attr($desc) . '" />' . "\n"; } $canonical = citami_seo_canonical($ctx); if ($canonical !== '') { echo '<link rel="canonical" href="' . esc_url($canonical) . '" />' . "\n"; } } add_action('wp_head', 'citami_seo_print_head', 1); // WordPress prints its own rel=canonical on singular pages: ours replaces it. add_action('wp', function () { if (citami_seo_enabled() && function_exists('remove_action')) { remove_action('wp_head', 'rel_canonical'); } }); /* ---------------------------------------------------------------- */ /* Editor: meta box + save */ /* ---------------------------------------------------------------- */ function citami_seo_post_types() { $types = get_post_types(array('public' => true), 'names'); if (!is_array($types)) { return array('post', 'page'); } unset($types['attachment']); return array_values($types); } add_action('add_meta_boxes', function () { foreach (citami_seo_post_types() as $type) { add_meta_box('citami_seo', citami_brand_name() . ' · SEO', 'citami_seo_render_meta_box', $type, 'normal', 'high'); } }); add_action('init', function () { foreach (array(CITAMI_META_TITLE, CITAMI_META_DESC, CITAMI_META_CANONICAL, CITAMI_META_ROBOTS_INDEX, CITAMI_META_ROBOTS_FOLLOW, CITAMI_META_ROBOTS_ADV) as $key) { register_post_meta('', $key, array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'auth_callback' => function () { return current_user_can('edit_posts'); }, )); } }); /** Values as stored (all strings). */ function citami_seo_get_post_fields($post_id) { return array( 'title' => (string) get_post_meta($post_id, CITAMI_META_TITLE, true), 'description' => (string) get_post_meta($post_id, CITAMI_META_DESC, true), 'canonical' => (string) get_post_meta($post_id, CITAMI_META_CANONICAL, true), 'robots_index' => (string) get_post_meta($post_id, CITAMI_META_ROBOTS_INDEX, true), 'robots_follow' => (string) get_post_meta($post_id, CITAMI_META_ROBOTS_FOLLOW, true), 'robots_advanced' => (string) get_post_meta($post_id, CITAMI_META_ROBOTS_ADV, true), ); } /** Validates + stores a partial set of fields. Returns the stored values. */ function citami_seo_save_post_fields($post_id, array $fields) { $post_id = (int) $post_id; if (array_key_exists('title', $fields)) { update_post_meta($post_id, CITAMI_META_TITLE, mb_substr(citami_seo_sanitize_template($fields['title']), 0, 200)); } if (array_key_exists('description', $fields)) { update_post_meta($post_id, CITAMI_META_DESC, mb_substr(citami_seo_sanitize_template($fields['description']), 0, 320)); } if (array_key_exists('canonical', $fields)) { $c = trim((string) $fields['canonical']); update_post_meta($post_id, CITAMI_META_CANONICAL, preg_match('#^https?://#i', $c) ? esc_url_raw($c) : ''); } if (array_key_exists('robots_index', $fields)) { $v = (string) $fields['robots_index']; update_post_meta($post_id, CITAMI_META_ROBOTS_INDEX, in_array($v, array('index', 'noindex'), true) ? $v : ''); } if (array_key_exists('robots_follow', $fields)) { $v = (string) $fields['robots_follow']; update_post_meta($post_id, CITAMI_META_ROBOTS_FOLLOW, in_array($v, array('follow', 'nofollow'), true) ? $v : ''); } if (array_key_exists('robots_advanced', $fields)) { $raw = is_array($fields['robots_advanced']) ? $fields['robots_advanced'] : explode(',', (string) $fields['robots_advanced']); $keep = array(); foreach ($raw as $d) { $d = sanitize_key((string) $d); if (in_array($d, array('noarchive', 'nosnippet', 'noimageindex', 'notranslate'), true)) { $keep[] = $d; } } update_post_meta($post_id, CITAMI_META_ROBOTS_ADV, implode(',', array_unique($keep))); } // 2.31.1 — a cache plugin does not see a meta change: purge the page. if (function_exists('citami_purge_cache')) { citami_purge_cache($post_id); } return citami_seo_get_post_fields($post_id); } // 2.40 — per-page meta coverage travels with the content-health snapshot: // how many published pages have a custom title / description, how many are // noindex (and which, as page refs). Counts and refs only: no meta text travels. add_filter('citami_health_extra', function ($extra) { $extra = is_array($extra) ? $extra : array(); $ids = get_posts(array( 'post_type' => citami_seo_post_types(), 'post_status' => 'publish', 'numberposts' => defined('CITAMI_HEALTH_SCAN') ? CITAMI_HEALTH_SCAN : 500, 'orderby' => 'modified', 'order' => 'DESC', 'fields' => 'ids', )); $out = array('checked' => 0, 'custom_title' => 0, 'custom_description' => 0, 'noindex' => 0, 'noindex_pages' => array()); $limit = defined('CITAMI_HEALTH_LIST') ? CITAMI_HEALTH_LIST : 5; foreach ((array) $ids as $id) { $id = (int) $id; $out['checked']++; if ((string) get_post_meta($id, CITAMI_META_TITLE, true) !== '') { $out['custom_title']++; } if ((string) get_post_meta($id, CITAMI_META_DESC, true) !== '') { $out['custom_description']++; } if ((string) get_post_meta($id, CITAMI_META_ROBOTS_INDEX, true) === 'noindex') { $out['noindex']++; if (count($out['noindex_pages']) < $limit && function_exists('citami_health_page_ref')) { $out['noindex_pages'][] = citami_health_page_ref($id); } } } $extra['meta'] = $out; return $extra; }); function citami_seo_render_meta_box($post) { $f = citami_seo_get_post_fields($post->ID); $settings = citami_seo_settings(); $type = isset($post->post_type) ? (string) $post->post_type : 'post'; $tpl = isset($settings['templates'][$type]['title']) ? $settings['templates'][$type]['title'] : '%title% %sep% %sitename%'; // 2.49 — the placeholder is what the page gets when the field is empty, rendered, not the template variables. $default_title = citami_seo_render_template($tpl, citami_seo_vars(array('post' => $post))); $enabled = citami_seo_enabled(); wp_nonce_field('citami_seo_save_' . $post->ID, CITAMI_SEO_NONCE); ?> <div class="citami-seo-box"> <?php do_action('citami_seo_meta_box_top', $post); ?> <?php if (!$enabled): ?> <p class="description" style="color:#b45309"><?php echo esc_html(citami_t( 'Un altro plugin SEO è attivo: questi valori vengono salvati ma non stampati finché non attivi Citami SEO in Impostazioni → Citami → SEO.', 'Another SEO plugin is active: these values are saved but not printed until you enable Citami SEO in Settings → Citami → SEO.' )); ?></p> <?php endif; ?> <p> <label for="citami_seo_title"><strong><?php echo esc_html(citami_t('Titolo SEO', 'SEO title')); ?></strong></label><br> <input type="text" id="citami_seo_title" name="citami_seo[title]" class="widefat" maxlength="200" value="<?php echo esc_attr($f['title']); ?>" placeholder="<?php echo esc_attr($default_title); ?>"> <span class="description"><?php echo esc_html(citami_t('Vuoto = titolo della pagina e nome del sito. Meglio se contiene la domanda del cliente, entro 60 caratteri.', 'Empty = page title and site name. Best when it carries the customer\'s question, within 60 characters.')); ?></span> </p> <p> <label for="citami_seo_description"><strong><?php echo esc_html(citami_t('Descrizione per i risultati di ricerca', 'Search-result description')); ?></strong></label><br> <textarea id="citami_seo_description" name="citami_seo[description]" class="widefat" rows="3" maxlength="320"><?php echo esc_textarea($f['description']); ?></textarea> <span class="description"><?php echo esc_html(citami_t('120–155 caratteri che anticipano la risposta. Vuoto = riassunto automatico.', '120–155 characters that preview the answer. Empty = automatic excerpt.')); ?></span> </p> <?php do_action('citami_seo_meta_box_essentials', $post); ?> <details class="citami-seo-more" id="citami-seo-more"> <summary><?php echo esc_html(citami_t('Altre opzioni: link ad altre pagine, URL canonico, robots, dati strutturati, social', 'More options: links to other pages, canonical URL, robots, structured data, social')); ?></summary> <?php do_action('citami_seo_meta_box_details', $post); ?> <p> <label for="citami_seo_canonical"><strong><?php echo esc_html(citami_t('URL canonico', 'Canonical URL')); ?></strong></label><br> <input type="url" id="citami_seo_canonical" name="citami_seo[canonical]" class="widefat" value="<?php echo esc_attr($f['canonical']); ?>" placeholder="<?php echo esc_attr(get_permalink($post)); ?>"> </p> <p> <strong>Robots</strong><br> <select name="citami_seo[robots_index]"> <option value="" <?php selected($f['robots_index'], ''); ?>><?php echo esc_html(citami_t('Default (index)', 'Default (index)')); ?></option> <option value="index" <?php selected($f['robots_index'], 'index'); ?>>index</option> <option value="noindex" <?php selected($f['robots_index'], 'noindex'); ?>>noindex</option> </select> <select name="citami_seo[robots_follow]"> <option value="" <?php selected($f['robots_follow'], ''); ?>><?php echo esc_html(citami_t('Default (follow)', 'Default (follow)')); ?></option> <option value="follow" <?php selected($f['robots_follow'], 'follow'); ?>>follow</option> <option value="nofollow" <?php selected($f['robots_follow'], 'nofollow'); ?>>nofollow</option> </select> <?php $adv = array_filter(array_map('trim', explode(',', $f['robots_advanced']))); ?> <?php foreach (array('noarchive', 'nosnippet', 'noimageindex', 'notranslate') as $d): ?> <label style="margin-left:8px"><input type="checkbox" name="citami_seo[robots_advanced][]" value="<?php echo esc_attr($d); ?>" <?php checked(in_array($d, $adv, true)); ?>> <?php echo esc_html($d); ?></label> <?php endforeach; ?> </p> <?php do_action('citami_seo_meta_box_fields', $post); ?> </details> </div> <?php } /** save_post: meta box (citami_seo[...]) and quick/bulk edit (citami_qe_*). */ function citami_seo_handle_save($post_id) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (function_exists('wp_is_post_revision') && wp_is_post_revision($post_id)) { return; } if (!current_user_can('edit_post', $post_id)) { return; } // Meta box (full form, nonce required). if (isset($_POST['citami_seo']) && is_array($_POST['citami_seo'])) { $nonce = isset($_POST[CITAMI_SEO_NONCE]) ? sanitize_text_field(wp_unslash($_POST[CITAMI_SEO_NONCE])) : ''; if (!wp_verify_nonce($nonce, 'citami_seo_save_' . $post_id)) { return; } $in = wp_kses_post_deep(wp_unslash($_POST['citami_seo'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- array, sanitised right here with wp_kses_post_deep (recursive wp_kses_post), which the sniff does not list $fields = array( 'title' => isset($in['title']) ? $in['title'] : '', 'description' => isset($in['description']) ? $in['description'] : '', 'canonical' => isset($in['canonical']) ? $in['canonical'] : '', 'robots_index' => isset($in['robots_index']) ? $in['robots_index'] : '', 'robots_follow' => isset($in['robots_follow']) ? $in['robots_follow'] : '', 'robots_advanced' => isset($in['robots_advanced']) ? $in['robots_advanced'] : array(), ); citami_seo_save_post_fields($post_id, $fields); do_action('citami_seo_save_extra', $post_id, $in); return; } // Quick edit / bulk edit: only the fields that were sent, "-1" = unchanged. $fields = array(); foreach (array('title', 'description', 'robots_index', 'robots_follow') as $k) { $name = 'citami_qe_' . $k; if (!isset($_REQUEST[$name])) { continue; } $v = sanitize_text_field(wp_unslash((string) $_REQUEST[$name])); if ($v === '-1') { continue; } $fields[$k] = $v; } if (!empty($fields)) { if (!empty($_REQUEST['citami_qe_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['citami_qe_nonce'])), 'citami_qe')) { return; } citami_seo_save_post_fields($post_id, $fields); } } add_action('save_post', 'citami_seo_handle_save', 10, 1); /* ---------------------------------------------------------------- */ /* Posts list: column + quick edit + bulk edit */ /* ---------------------------------------------------------------- */ add_filter('manage_posts_columns', 'citami_seo_add_column'); add_filter('manage_pages_columns', 'citami_seo_add_column'); function citami_seo_add_column($columns) { if (is_array($columns)) { $columns['citami_seo'] = 'SEO'; } return $columns; } function citami_seo_render_column($column, $post_id) { if ($column !== 'citami_seo') { return; } $f = citami_seo_get_post_fields($post_id); $bits = array(); // 2.52 — the tooltip says what the letters mean; the cell stays short. $bits[] = $f['title'] !== '' ? '<span title="' . esc_attr(sprintf(citami_t('Titolo SEO impostato: %s', 'SEO title set: %s'), $f['title'])) . '">T ✓</span>' : '<span style="color:#999" title="' . esc_attr(citami_t('Titolo SEO automatico: titolo della pagina e nome del sito', 'Automatic SEO title: page title and site name')) . '">T –</span>'; $bits[] = $f['description'] !== '' ? '<span title="' . esc_attr(sprintf(citami_t('Descrizione impostata: %s', 'Description set: %s'), $f['description'])) . '">D ✓</span>' : '<span style="color:#999" title="' . esc_attr(citami_t('Descrizione automatica: riassunto del testo', 'Automatic description: summary of the text')) . '">D –</span>'; if ($f['robots_index'] === 'noindex') { $bits[] = '<span style="color:#b91c1c">noindex</span>'; } echo wp_kses(implode(' · ', $bits), citami_kses_allowed()); // Hidden values the quick-edit JS copies into the inline form. echo '<div class="hidden citami-qe-data" data-title="' . esc_attr($f['title']) . '" data-description="' . esc_attr($f['description']) . '" data-index="' . esc_attr($f['robots_index']) . '" data-follow="' . esc_attr($f['robots_follow']) . '"></div>'; } add_action('manage_posts_custom_column', 'citami_seo_render_column', 10, 2); add_action('manage_pages_custom_column', 'citami_seo_render_column', 10, 2); function citami_seo_quick_edit_box($column_name, $post_type) { if ($column_name !== 'citami_seo') { return; } wp_nonce_field('citami_qe', 'citami_qe_nonce', false); ?> <fieldset class="inline-edit-col-right citami-qe"> <div class="inline-edit-col"> <label><span class="title"><?php echo esc_html(citami_t('Titolo SEO', 'SEO title')); ?></span><input type="text" name="citami_qe_title" value="-1"></label> <label><span class="title"><?php echo esc_html(citami_t('Meta description', 'Meta description')); ?></span><textarea name="citami_qe_description" rows="2">-1</textarea></label> <label><span class="title">Robots</span> <select name="citami_qe_robots_index"><option value="-1">—</option><option value="">default</option><option value="index">index</option><option value="noindex">noindex</option></select> <select name="citami_qe_robots_follow"><option value="-1">—</option><option value="">default</option><option value="follow">follow</option><option value="nofollow">nofollow</option></select> </label> </div> </fieldset> <?php } add_action('quick_edit_custom_box', 'citami_seo_quick_edit_box', 10, 2); function citami_seo_bulk_edit_box($column_name, $post_type) { if ($column_name !== 'citami_seo') { return; } wp_nonce_field('citami_qe', 'citami_qe_nonce', false); ?> <fieldset class="inline-edit-col-right citami-bulk"> <div class="inline-edit-col"> <label><span class="title">Robots</span> <select name="citami_qe_robots_index"><option value="-1"><?php echo esc_html(citami_t('— Nessuna modifica —', '— No change —')); ?></option><option value="">default</option><option value="index">index</option><option value="noindex">noindex</option></select> <select name="citami_qe_robots_follow"><option value="-1"><?php echo esc_html(citami_t('— Nessuna modifica —', '— No change —')); ?></option><option value="">default</option><option value="follow">follow</option><option value="nofollow">nofollow</option></select> </label> </div> </fieldset> <?php } add_action('bulk_edit_custom_box', 'citami_seo_bulk_edit_box', 10, 2); // The quick-edit form is filled by assets/boxes.js (enqueued on edit.php). /* ---------------------------------------------------------------- */ /* REST: the platform drives per-page meta */ /* ---------------------------------------------------------------- */ /** POST /wp-json/citami/v1/page-meta { post_id | url, ...fields } */ function citami_rest_page_meta(WP_REST_Request $request) { $body = $request->get_json_params(); if (!is_array($body)) { $body = array(); } $post_id = isset($body['post_id']) ? (int) $body['post_id'] : 0; if ($post_id <= 0 && !empty($body['url']) && function_exists('url_to_postid')) { $post_id = (int) url_to_postid((string) $body['url']); } if ($post_id <= 0 || !get_post($post_id)) { return new WP_Error('citami_post_not_found', 'post_id o url non risolvibile.', array('status' => 404)); } $fields = array_intersect_key($body, array_flip(array('title', 'description', 'canonical', 'robots_index', 'robots_follow', 'robots_advanced'))); if (empty($fields)) { return new WP_Error('citami_bad_payload', 'Nessun campo da salvare.', array('status' => 400)); } $stored = citami_seo_save_post_fields($post_id, $fields); // WP10 — IndexNow hears about platform-driven changes too. do_action('citami_page_meta_saved', $post_id); return rest_ensure_response(array( 'success' => true, 'post_id' => $post_id, 'fields' => $stored, 'seo_enabled' => citami_seo_enabled(), )); } /** GET /wp-json/citami/v1/page-meta?post_id= */ function citami_rest_page_meta_get(WP_REST_Request $request) { $post_id = (int) $request->get_param('post_id'); if ($post_id <= 0 || !get_post($post_id)) { return new WP_Error('citami_post_not_found', 'post_id non valido.', array('status' => 404)); } return rest_ensure_response(array( 'post_id' => $post_id, 'fields' => citami_seo_get_post_fields($post_id), 'seo_enabled' => citami_seo_enabled(), )); } add_action('rest_api_init', function () { register_rest_route('citami/v1', '/page-meta', array( array( 'methods' => 'POST', 'callback' => 'citami_rest_page_meta', 'permission_callback' => 'citami_check_permission', ), array( 'methods' => 'GET', 'callback' => 'citami_rest_page_meta_get', 'permission_callback' => 'citami_check_permission', ), )); }); /* ---------------------------------------------------------------- */ /* Settings tab (rendered by admin.php under ?tab=seo) */ /* ---------------------------------------------------------------- */ add_action('admin_init', function () { register_setting('citami_seo', CITAMI_SEO_SETTINGS_OPT, array( 'type' => 'array', 'sanitize_callback' => 'citami_sanitize_seo_settings', 'default' => array(), )); }); /** * 2.42 — what is on, off or left to another plugin, one line each. The * SEO tab opens with this instead of forty fields. */ function citami_seo_status_items() { $other = citami_active_seo_plugin(); $items = array(); $meta_on = citami_seo_enabled(); $items[] = array('id' => 'meta', 'label' => citami_t('Title, description, canonical, robots', 'Title, description, canonical, robots'), 'state' => $meta_on ? 'good' : ($other !== null ? 'muted' : 'warn'), 'detail' => $meta_on ? citami_t('gestiti da Citami su ogni pagina', 'managed by Citami on every page') : ($other !== null ? sprintf(citami_t('lasciati a %s', 'left to %s'), $other) : citami_t('disattivati', 'off'))); $schema_on = function_exists('citami_schema_enabled') ? citami_schema_enabled() : false; $items[] = array('id' => 'schema', 'label' => citami_t('Schema (Organization, WebPage, Article, FAQ)', 'Schema (Organization, WebPage, Article, FAQ)'), 'state' => $schema_on ? 'good' : ($other !== null ? 'muted' : 'warn'), 'detail' => $schema_on ? citami_t('stampato in ogni pagina', 'printed on every page') : ($other !== null ? sprintf(citami_t('lasciato a %s', 'left to %s'), $other) : citami_t('disattivato', 'off'))); $sitemap_on = function_exists('citami_sitemap_enabled') ? citami_sitemap_enabled() : false; $items[] = array('id' => 'sitemap', 'label' => 'Sitemap XML', 'state' => $sitemap_on ? 'good' : 'muted', 'detail' => $sitemap_on ? home_url('/sitemap.xml') : citami_t('servita da un altro plugin o disattivata', 'served by another plugin or off')); $llms_on = function_exists('citami_llms_settings') ? citami_llms_settings()['enabled'] === 'on' : false; $items[] = array('id' => 'llms', 'label' => 'llms.txt', 'state' => $llms_on ? 'good' : 'warn', 'detail' => $llms_on ? home_url('/llms.txt') : citami_t('disattivato: i crawler AI non hanno un indice del sito', 'off: AI crawlers have no index of the site')); $in_on = function_exists('citami_indexnow_enabled') ? citami_indexnow_enabled() : false; $in_log = get_option('citami_indexnow_log', array()); $in_last = is_array($in_log) && !empty($in_log) && is_array($in_log[0]) ? $in_log[0] : null; $in_state = !$in_on ? 'warn' : ($in_last === null ? 'good' : ((string) $in_last['status'] === 'ok' ? 'good' : 'bad')); $items[] = array('id' => 'indexnow', 'label' => 'IndexNow', 'state' => $in_state, 'detail' => !$in_on ? citami_t('disattivato: Bing e gli altri motori non sanno delle modifiche', 'off: Bing and the other engines do not hear about changes') : ($in_last === null ? citami_t('attivo, nessun invio ancora', 'on, nothing sent yet') : ((string) $in_last['status'] === 'ok' ? sprintf(citami_t('attivo, ultimo invio riuscito il %s', 'on, last submission succeeded on %s'), wp_date('d/m', (int) $in_last['ts'])) : sprintf(citami_t('ultimo invio fallito (HTTP %d)', 'last submission failed (HTTP %d)'), (int) $in_last['code'])))); $ml = function_exists('citami_multilingual_plugin') ? citami_multilingual_plugin() : null; $hl_on = function_exists('citami_hreflang_enabled') ? citami_hreflang_enabled() : false; $items[] = array('id' => 'hreflang', 'label' => 'hreflang', 'state' => $ml !== null ? 'muted' : ($hl_on ? 'good' : 'muted'), 'detail' => $ml !== null ? sprintf(citami_t('gestito da %s', 'handled by %s'), $ml) : ($hl_on ? citami_t('attivo per le pagine con alternative', 'on for pages with alternates') : citami_t('disattivato', 'off'))); $alt_fill = function_exists('citami_alt_fill_enabled') ? citami_alt_fill_enabled() : false; $alt_ai = function_exists('citami_alt_ai_enabled') ? citami_alt_ai_enabled() : false; $items[] = array('id' => 'alt', 'label' => citami_t('Alt delle immagini', 'Image alt text'), 'state' => $alt_fill ? 'good' : 'warn', 'detail' => $alt_fill ? ($alt_ai ? citami_t('riempiti in uscita e generati in libreria', 'filled at output and generated in the library') : citami_t('riempiti in uscita; descrizione automatica spenta', 'filled at output; automatic description off')) : citami_t('le immagini senza alt restano senza alt', 'images without alt stay without alt')); $aeo_on = function_exists('citami_aeo_settings') ? citami_aeo_settings()['enabled'] === 'on' : false; $items[] = array('id' => 'aeo', 'label' => citami_t('Punteggio AEO nell\'editor', 'AEO score in the editor'), 'state' => $aeo_on ? 'good' : 'muted', 'detail' => $aeo_on ? citami_t('pannello e colonna attivi', 'panel and column on') : citami_t('disattivato', 'off')); $soc_on = function_exists('citami_social_settings') ? citami_social_settings()['enabled'] === 'on' : false; $items[] = array('id' => 'social', 'label' => 'Open Graph / X', 'state' => $soc_on ? 'good' : 'muted', 'detail' => $soc_on ? citami_t('anteprime social attive', 'social previews on') : citami_t('disattivate', 'off')); return $items; } function citami_render_seo_status_card() { $items = citami_seo_status_items(); $good = count(array_filter($items, function ($i) { return $i['state'] === 'good'; })); ?> <section class="citami-card citami-card--status" data-citami="seo-status"> <header class="citami-card-head"> <span class="citami-eyebrow"><?php echo esc_html(citami_t('Stato', 'Status')); ?></span> <span class="citami-muted"><?php echo esc_html(sprintf(citami_t('%1$d di %2$d attivi', '%1$d of %2$d on'), $good, count($items))); ?></span> </header> <ul class="citami-status citami-status--grid"> <?php foreach ($items as $it): ?> <li> <span class="citami-dot citami-dot--<?php echo esc_attr($it['state']); ?>"></span> <a href="<?php echo esc_url(citami_admin_url('seo', array('open' => 1)) . '#citami-seo-' . $it['id']); ?>"><?php echo esc_html($it['label']); ?></a> <small><?php echo esc_html($it['detail']); ?></small> </li> <?php endforeach; ?> </ul> </section> <?php } /** * 2.61 — one card per section of the advanced SEO settings. Prints the * opening <section> and its header; the caller prints the rows (a * form-table) and closes the </section>. The id is the anchor the * status card and the action handlers link to (#citami-seo-<id>). */ function citami_seo_card_head($id, $title, $purpose, $extra_class = '') { $cls = 'citami-card citami-seo-card' . ($extra_class !== '' ? ' ' . $extra_class : ''); echo '<section class="' . esc_attr($cls) . '" id="' . esc_attr('citami-seo-' . $id) . '" data-citami="seo-card">' . '<header class="citami-card-head citami-seo-card-head"><div>' . '<h3 class="citami-eyebrow">' . esc_html($title) . '</h3>' . '<p class="citami-muted">' . esc_html($purpose) . '</p>' . '</div></header>'; } function citami_render_seo_settings_tab() { $s = citami_seo_settings(); $other = citami_active_seo_plugin(); $types = citami_seo_post_types(); citami_render_seo_status_card(); $open = !empty($_GET['open']) || !empty($_GET['settings-updated']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only: whether the advanced card opens expanded $opt = CITAMI_SEO_SETTINGS_OPT; ?> <details class="citami-advanced" id="citami-seo-advanced"<?php echo $open ? ' open' : ''; ?>> <summary><?php echo esc_html(citami_t('Impostazioni avanzate: template, schema, contenuti per le AI, social, sitemap, llms.txt, IndexNow, hreflang, alt', 'Advanced settings: templates, schema, AI content, social, sitemap, llms.txt, IndexNow, hreflang, alt')); ?></summary> <?php // One options form, one Save. Each module prints its own card through // citami_seo_settings_rows; the Save sits in a sticky bar at the bottom // of the form (the floating "unsaved changes" bar is off here: with // the IndexNow/alt buttons inside, it would click the wrong one). ?> <form method="post" action="<?php echo esc_url(admin_url('options.php')); ?>" class="citami-seo-form" data-citami="seo-form" data-citami-nosave> <?php settings_fields('citami_seo'); ?> <?php citami_seo_card_head('meta', citami_t('Meta e template', 'Meta and templates'), citami_t('Titolo e description di ogni pagina: modelli per tipo di contenuto, homepage e archivi.', 'Title and description of every page: templates per content type, homepage and archives.')); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><?php echo esc_html(citami_t('Meta per pagina', 'Per-page meta')); ?></th> <td> <select name="<?php echo esc_attr($opt); ?>[enabled]"> <option value="auto" <?php selected($s['enabled'], 'auto'); ?>><?php echo esc_html(citami_t('Automatico (attivo se nessun altro plugin SEO)', 'Automatic (on unless another SEO plugin is active)')); ?></option> <option value="on" <?php selected($s['enabled'], 'on'); ?>><?php echo esc_html(citami_t('Sempre attivo', 'Always on')); ?></option> <option value="off" <?php selected($s['enabled'], 'off'); ?>><?php echo esc_html(citami_t('Disattivato', 'Off')); ?></option> </select> <p class="description"> <?php if ($other !== null): ?> <?php echo esc_html(sprintf(citami_t('Rilevato: %s. In automatico Citami non stampa titolo, description, canonical e robots per evitare duplicati.', 'Detected: %s. In automatic mode Citami does not print title, description, canonical and robots to avoid duplicates.'), $other)); ?> <?php else: ?> <?php echo esc_html(citami_t('Nessun altro plugin SEO rilevato: Citami gestisce titolo, description, canonical e robots.', 'No other SEO plugin detected: Citami manages title, description, canonical and robots.')); ?> <?php endif; ?> </p> </td> </tr> <tr> <th scope="row"><?php echo esc_html(citami_t('Separatore', 'Separator')); ?></th> <td><input type="text" name="<?php echo esc_attr($opt); ?>[separator]" value="<?php echo esc_attr($s['separator']); ?>" size="3"></td> </tr> <tr> <th scope="row">Homepage</th> <td class="citami-seo-stack"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[home][title]" value="<?php echo esc_attr($s['home']['title']); ?>" placeholder="<?php echo esc_attr(citami_t('Titolo homepage', 'Homepage title')); ?>"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[home][description]" value="<?php echo esc_attr($s['home']['description']); ?>" placeholder="<?php echo esc_attr(citami_t('Description homepage', 'Homepage description')); ?>"> </td> </tr> <?php foreach ($types as $type): $tpl = isset($s['templates'][$type]) ? $s['templates'][$type] : array('title' => '%title% %sep% %sitename%', 'description' => '%excerpt%'); ?> <tr> <th scope="row"><?php echo esc_html(citami_post_type_label($type)); ?></th> <td class="citami-seo-stack"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[templates][<?php echo esc_attr($type); ?>][title]" value="<?php echo esc_attr($tpl['title']); ?>"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[templates][<?php echo esc_attr($type); ?>][description]" value="<?php echo esc_attr($tpl['description']); ?>"> </td> </tr> <?php endforeach; ?> <tr> <th scope="row"><?php echo esc_html(citami_t('Archivi', 'Archives')); ?></th> <td class="citami-seo-stack"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[archive][title]" value="<?php echo esc_attr($s['archive']['title']); ?>"> <input type="text" class="large-text" name="<?php echo esc_attr($opt); ?>[archive][description]" value="<?php echo esc_attr($s['archive']['description']); ?>"> </td> </tr> </table> <p class="description citami-seo-card-foot"><?php echo esc_html(citami_t('Variabili: %title% %sitename% %tagline% %sep% %excerpt% %category% %author% %date% %term% %page% %search%', 'Variables: %title% %sitename% %tagline% %sep% %excerpt% %category% %author% %date% %term% %page% %search%')); ?></p> </section> <?php do_action('citami_seo_settings_rows'); ?> <?php citami_seo_card_head('noindex', 'noindex', citami_t('Pagine da tenere fuori dai risultati di ricerca: di solito non servono a nessuno.', 'Pages to keep out of search results: usually nobody needs them.')); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><?php echo esc_html(citami_t('Escludi', 'Exclude')); ?></th> <td class="citami-seo-checks"> <?php foreach (array('search' => citami_t('Risultati di ricerca', 'Search results'), 'attachment' => citami_t('Pagine allegato', 'Attachment pages'), 'author' => citami_t('Archivi autore', 'Author archives'), 'date' => citami_t('Archivi per data', 'Date archives'), 'paginated' => citami_t('Pagine 2+ degli archivi', 'Paginated archive pages')) as $k => $label): ?> <label><input type="checkbox" name="<?php echo esc_attr($opt); ?>[noindex][<?php echo esc_attr($k); ?>]" value="1" <?php checked(!empty($s['noindex'][$k])); ?>> <?php echo esc_html($label); ?></label> <?php endforeach; ?> </td> </tr> </table> </section> <div class="citami-seo-savefoot" data-citami="seo-save"> <span class="citami-muted"><?php echo esc_html(citami_t('Un solo salvataggio per tutte le sezioni qui sopra.', 'One save for every section above.')); ?></span> <button type="submit" class="citami-btn citami-btn--primary" data-citami-busy="<?php echo esc_attr(citami_t('Salvo…', 'Saving…')); ?>"><?php echo esc_html(citami_t('Salva le impostazioni', 'Save settings')); ?></button> </div> </form> <?php // 2.59 — action forms of the modules (IndexNow, alt) live here, outside // the options form: a <form> nested in it was dropped by the browser and // its hidden "action" field hijacked Save. Their buttons point here via form="…". do_action('citami_seo_settings_after_form'); ?> </details> <?php }