'', 'tldr_position' => 'top', // top | off (shortcode only) 'tldr_style' => 'box', // 2.63 — box | line | plain 'tldr_show_label' => 'on', // 2.63 — on | off ("In breve:" before the text) 'accent' => '', // 2.63 — hex colour; '' = the plugin violet 'faq_title' => '', 'faq_position' => 'bottom', // bottom | off 'faq_style' => 'accordion', // 2.63 — accordion | plain 'faq_existing' => 'merge', // 2.63 — merge into a FAQ the page already has | separate 'author_box' => 'on', // on | off ); $s = get_option(CITAMI_CONTENT_SETTINGS_OPT, array()); if (!is_array($s)) { return $d; } foreach ($d as $k => $v) { if (isset($s[$k]) && is_string($s[$k])) { $d[$k] = $s[$k]; } } return $d; } function citami_sanitize_content_settings($value) { $in = is_array($value) ? $value : array(); return array( 'tldr_label' => mb_substr(sanitize_text_field(isset($in['tldr_label']) ? (string) $in['tldr_label'] : ''), 0, 60), 'tldr_position' => (isset($in['tldr_position']) && $in['tldr_position'] === 'off') ? 'off' : 'top', 'faq_title' => mb_substr(sanitize_text_field(isset($in['faq_title']) ? (string) $in['faq_title'] : ''), 0, 80), 'faq_position' => (isset($in['faq_position']) && $in['faq_position'] === 'off') ? 'off' : 'bottom', 'author_box' => (isset($in['author_box']) && $in['author_box'] === 'off') ? 'off' : 'on', 'tldr_style' => (isset($in['tldr_style']) && in_array($in['tldr_style'], array('box', 'line', 'plain'), true)) ? $in['tldr_style'] : 'box', 'tldr_show_label' => (isset($in['tldr_show_label']) && $in['tldr_show_label'] === 'off') ? 'off' : 'on', 'accent' => (isset($in['accent']) && preg_match('/^#[0-9a-fA-F]{6}$/', (string) $in['accent'])) ? strtolower((string) $in['accent']) : '', 'faq_style' => (isset($in['faq_style']) && $in['faq_style'] === 'plain') ? 'plain' : 'accordion', 'faq_existing' => (isset($in['faq_existing']) && $in['faq_existing'] === 'separate') ? 'separate' : 'merge', ); } /** 2.63 — the accent colour of the blocks ('' setting = the plugin violet). */ function citami_content_accent() { $a = citami_content_settings()['accent']; return $a !== '' ? $a : '#7c3aed'; } add_action('admin_init', function () { register_setting('citami_seo', CITAMI_CONTENT_SETTINGS_OPT, array( 'type' => 'array', 'sanitize_callback' => 'citami_sanitize_content_settings', 'default' => array(), )); }); /* ---------------------------------------------------------------- */ /* Post data */ /* ---------------------------------------------------------------- */ function citami_content_tldr($post_id) { return trim((string) get_post_meta($post_id, CITAMI_META_TLDR, true)); } /** FAQ items as array of ['q' => ..., 'a' => ...] (plain text, tags stripped). */ function citami_content_faq($post_id) { $raw = get_post_meta($post_id, CITAMI_META_FAQ, true); $items = is_string($raw) ? json_decode($raw, true) : $raw; if (!is_array($items)) { return array(); } $out = array(); foreach ($items as $it) { if (!is_array($it)) { continue; } $q = trim((string) (isset($it['q']) ? $it['q'] : '')); $a = trim((string) (isset($it['a']) ? $it['a'] : '')); if ($q !== '' && $a !== '') { $out[] = array('q' => $q, 'a' => $a); } } return $out; } /** Stores TL;DR + FAQ. Answers keep basic formatting (wp_kses_post). */ function citami_content_save($post_id, $tldr, array $faq) { $tldr = mb_substr(trim(wp_strip_all_tags((string) $tldr)), 0, 600); update_post_meta($post_id, CITAMI_META_TLDR, $tldr); $clean = array(); foreach ($faq as $it) { $q = mb_substr(trim(wp_strip_all_tags((string) (isset($it['q']) ? $it['q'] : ''))), 0, 200); $a = mb_substr(trim(wp_kses_post((string) (isset($it['a']) ? $it['a'] : ''))), 0, 2000); if ($q !== '' && $a !== '') { $clean[] = array('q' => $q, 'a' => $a); } if (count($clean) >= 30) { break; } } if (empty($clean)) { delete_post_meta($post_id, CITAMI_META_FAQ); } else { update_post_meta($post_id, CITAMI_META_FAQ, wp_json_encode($clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG)); } return array('tldr' => $tldr, 'faq' => $clean); } /* ---------------------------------------------------------------- */ /* Rendering */ /* ---------------------------------------------------------------- */ function citami_content_render_tldr($post_id) { $tldr = citami_content_tldr($post_id); if ($tldr === '') { return ''; } $set = citami_content_settings(); $label = $set['tldr_label']; if ($label === '') { $label = citami_t('In breve', 'In short'); } // 2.63 — three looks: a tinted box, only the accent line, or plain text // that blends with the theme; the label ("In breve:") can be left out. $accent = citami_content_accent(); $styles = array( 'box' => 'border-left:4px solid ' . $accent . ';padding:12px 16px;margin:0 0 1.5em;background:' . $accent . '10', 'line' => 'border-left:4px solid ' . $accent . ';padding:2px 0 2px 16px;margin:0 0 1.5em', 'plain' => 'margin:0 0 1.5em', ); $style = isset($styles[$set['tldr_style']]) ? $set['tldr_style'] : 'box'; $prefix = $set['tldr_show_label'] === 'off' ? '' : '' . esc_html($label) . ': '; return '

' . $prefix . esc_html($tldr) . '

'; } function citami_content_render_faq($post_id, $items = null) { $items = $items === null ? citami_content_faq($post_id) : $items; if (empty($items)) { return ''; } $set = citami_content_settings(); $title = $set['faq_title']; if ($title === '') { $title = citami_t('Domande frequenti', 'Frequently asked questions'); } $html = '

' . esc_html($title) . '

'; foreach ($items as $it) { if ($set['faq_style'] === 'plain') { $html .= '

' . esc_html($it['q']) . '

' . wp_kses_post(wpautop($it['a'])) . '
'; } else { $html .= '
' . esc_html($it['q']) . '
' . wp_kses_post(wpautop($it['a'])) . '
'; } } return $html . '
'; } /* ---------------------------------------------------------------- */ /* 2.63 — a FAQ the page already has (Yoast, Rank Math, or text) */ /* ---------------------------------------------------------------- */ /** Lower-cased question without punctuation, to spot duplicates. */ function citami_content_norm_q($q) { return trim(preg_replace('/\s+/u', ' ', preg_replace('/[?¿!.,;:\'"“”‘’]/u', '', mb_strtolower(wp_strip_all_tags((string) $q))))); } function citami_content_faq_heading_re() { return '/\b(faq|f\.a\.q|domande frequenti|domande e risposte|frequently asked|questions and answers|q\s*&\s*a|q\s*&\s*a|preguntas frecuentes|questions fréquentes|häufige fragen)\b/iu'; } /** * The FAQ already in the page content, if any: * array('kind' => 'yoast'|'rankmath'|'heading'|'', 'items' => [['q','a']], 'level' => int). */ function citami_content_existing_faq($html) { $html = (string) $html; $out = array('kind' => '', 'items' => array(), 'level' => 2); if ($html === '') { return $out; } $clean = function ($s) { return trim(preg_replace('/\s+/u', ' ', html_entity_decode(wp_strip_all_tags((string) $s), ENT_QUOTES | ENT_HTML5, 'UTF-8'))); }; if (strpos($html, 'schema-faq-question') !== false && preg_match_all('#]*class="[^"]*schema-faq-question[^"]*"[^>]*>(.*?)\s*(?:]*class="[^"]*schema-faq-answer[^"]*"[^>]*>(.*?)

)?#is', $html, $m, PREG_SET_ORDER)) { $out['kind'] = 'yoast'; foreach ($m as $x) $out['items'][] = array('q' => $clean($x[1]), 'a' => $clean(isset($x[2]) ? $x[2] : '')); return $out; } if (strpos($html, 'rank-math-question') !== false && preg_match_all('#]*class="[^"]*rank-math-question[^"]*"[^>]*>(.*?)\s*(?:]*class="[^"]*rank-math-answer[^"]*"[^>]*>(.*?))?#is', $html, $m, PREG_SET_ORDER)) { $out['kind'] = 'rankmath'; foreach ($m as $x) $out['items'][] = array('q' => $clean($x[1]), 'a' => $clean(isset($x[2]) ? $x[2] : '')); return $out; } // A heading like "FAQ" / "Domande frequenti", then questions as smaller headings or bold lines. if (!preg_match_all('#]*>(.*?)#is', $html, $hs, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { return $out; } foreach ($hs as $h) { if (!preg_match(citami_content_faq_heading_re(), $clean($h[2][0]))) continue; $level = (int) $h[1][0]; $start = $h[0][1] + strlen($h[0][0]); $end = strlen($html); if (preg_match('#]#i', $html, $nx, PREG_OFFSET_CAPTURE, $start)) $end = $nx[0][1]; $section = substr($html, $start, $end - $start); $parts = preg_split('#(]*>.*?|]*>\s*<(?:strong|b)>[^<]*\?\s*\s*

)#is', $section, -1, PREG_SPLIT_DELIM_CAPTURE); $items = array(); for ($i = 1; $i < count($parts); $i += 2) { $q = $clean($parts[$i]); if ($q === '') continue; $items[] = array('q' => $q, 'a' => $clean(isset($parts[$i + 1]) ? $parts[$i + 1] : '')); } if (!empty($items)) { return array('kind' => 'heading', 'items' => $items, 'level' => $level, 'end' => $end); } } return $out; } /** Index just before the that closes the
opening at $open (balanced), or -1. */ function citami_content_div_close($html, $open) { $depth = 0; if (!preg_match_all('#<(/?)div\b[^>]*>#i', $html, $m, PREG_OFFSET_CAPTURE, $open)) return -1; foreach ($m[0] as $i => $tag) { $depth += $m[1][$i][0] === '/' ? -1 : 1; if ($depth === 0) return $tag[1]; } return -1; } /** Our questions not already on the page. */ function citami_content_faq_new_items($post_id, array $existing) { $seen = array(); foreach ($existing as $it) $seen[citami_content_norm_q($it['q'])] = true; $out = array(); foreach (citami_content_faq($post_id) as $it) { $k = citami_content_norm_q($it['q']); if ($k === '' || isset($seen[$k])) continue; $seen[$k] = true; $out[] = $it; } return $out; } /** * Adds our questions inside the FAQ the page already has, in its own * markup (no second "Domande frequenti" title). Returns the new HTML, or * null when there is no existing FAQ to merge into. */ function citami_content_merge_into_existing_faq($html, $post_id) { $ex = citami_content_existing_faq($html); if ($ex['kind'] === '') return null; $items = citami_content_faq_new_items($post_id, $ex['items']); if (empty($items)) return $html; $add = ''; if ($ex['kind'] === 'yoast') { foreach ($items as $it) $add .= '
' . esc_html($it['q']) . '

' . wp_kses_post(wp_strip_all_tags($it['a']) === $it['a'] ? esc_html($it['a']) : $it['a']) . '

'; if (!preg_match('#]*class="[^"]*wp-block-yoast-faq-block[^"]*"#i', $html, $mm, PREG_OFFSET_CAPTURE) && !preg_match('#]*class="schema-faq(?:\s[^"]*)?"#i', $html, $mm, PREG_OFFSET_CAPTURE)) return null; $close = citami_content_div_close($html, $mm[0][1]); return $close < 0 ? null : substr($html, 0, $close) . $add . substr($html, $close); } if ($ex['kind'] === 'rankmath') { foreach ($items as $it) $add .= '

' . esc_html($it['q']) . '

' . wp_kses_post(wpautop($it['a'])) . '
'; if (!preg_match('#]*class="[^"]*rank-math-list(?:\s[^"]*)?"#i', $html, $mm, PREG_OFFSET_CAPTURE)) return null; $close = citami_content_div_close($html, $mm[0][1]); return $close < 0 ? null : substr($html, 0, $close) . $add . substr($html, $close); } $hl = min(6, (int) $ex['level'] + 1); foreach ($items as $it) $add .= '' . esc_html($it['q']) . '' . wp_kses_post(wpautop($it['a'])); $pos = isset($ex['end']) ? (int) $ex['end'] : strlen($html); return substr($html, 0, $pos) . $add . substr($html, $pos); } function citami_content_render_author_box($post) { if (citami_content_settings()['author_box'] !== 'on' || empty($post->post_author) || $post->post_type !== 'post') { return ''; } $uid = (int) $post->post_author; $name = (string) get_the_author_meta('display_name', $uid); if ($name === '') { return ''; } $job = (string) get_user_meta($uid, 'citami_job_title', true); $bio = (string) get_the_author_meta('description', $uid); $sameas = citami_content_user_sameas($uid); $avatar = function_exists('get_avatar_url') ? (string) get_avatar_url($uid, array('size' => 96)) : ''; $html = ''; } function citami_content_user_sameas($uid) { $raw = get_user_meta($uid, 'citami_sameas', true); $list = is_array($raw) ? $raw : preg_split('/[\s,]+/', (string) $raw); $out = array(); foreach ((array) $list as $u) { $u = trim((string) $u); if (preg_match('#^https?://#i', $u)) { $out[] = $u; } } return array_values(array_unique($out)); } /** the_content: TL;DR on top, FAQ + author box at the bottom (unless shortcodes are used). */ function citami_content_filter($content) { if (!is_singular() || (function_exists('is_main_query') && !is_main_query())) { return $content; } $post = get_post(); if (!$post) { return $content; } $settings = citami_content_settings(); $has_tldr_sc = strpos($content, '[citami_tldr') !== false; $top = ($settings['tldr_position'] === 'top' && !$has_tldr_sc) ? citami_content_render_tldr($post->ID) : ''; return $top . $content; } add_filter('the_content', 'citami_content_filter', 9); /** * 2.63 — FAQ and author box once the blocks are rendered (priority 11), so * a FAQ the page already has (Yoast or Rank Math block, or a "FAQ" section * in the text) can receive our questions instead of getting a second one. */ function citami_content_filter_bottom($content) { if (!is_singular() || (function_exists('is_main_query') && !is_main_query())) { return $content; } $post = get_post(); if (!$post) { return $content; } $settings = citami_content_settings(); $bottom = ''; if ($settings['faq_position'] === 'bottom' && strpos(isset($post->post_content) ? (string) $post->post_content : '', '[citami_faq') === false && strpos($content, '[citami_faq') === false && strpos($content, 'class="citami-faq"') === false) { $merged = $settings['faq_existing'] === 'merge' ? citami_content_merge_into_existing_faq($content, $post->ID) : null; if ($merged !== null) { $content = $merged; } else { $bottom = citami_content_render_faq($post->ID); } } $bottom .= citami_content_render_author_box($post); return $content . $bottom; } add_filter('the_content', 'citami_content_filter_bottom', 11); add_shortcode('citami_tldr', function () { $post = get_post(); return $post ? citami_content_render_tldr($post->ID) : ''; }); add_shortcode('citami_faq', function () { $post = get_post(); return $post ? citami_content_render_faq($post->ID) : ''; }); /* ---------------------------------------------------------------- */ /* Schema tie-in */ /* ---------------------------------------------------------------- */ add_filter('citami_schema_graph', function ($graph, $ctx) { if (!is_array($graph) || empty($ctx['post'])) { return $graph; } $post = $ctx['post']; $tldr = citami_content_tldr($post->ID); // 2.63 — one FAQPage for the page: the questions it already has plus ours (no duplicates). $existing = citami_content_existing_faq(isset($post->post_content) ? (string) $post->post_content : ''); $faq = array_merge(array_values(array_filter($existing['items'], function ($it) { return $it['a'] !== ''; })), citami_content_faq_new_items($post->ID, $existing['items'])); if ($tldr === '' && empty($faq)) { return $graph; } $url = get_permalink($post); $page_idx = null; $article_idx = null; foreach ($graph as $i => $n) { if (!is_array($n) || empty($n['@id'])) { continue; } if ($n['@id'] === citami_schema_id('webpage', $url)) { $page_idx = $i; } if ($n['@id'] === citami_schema_id('article', $url)) { $article_idx = $i; } } if ($tldr !== '') { if ($article_idx !== null) { $graph[$article_idx]['abstract'] = $tldr; } elseif ($page_idx !== null) { $graph[$page_idx]['abstract'] = $tldr; } } if (!empty($faq)) { $main = array(); foreach ($faq as $it) { $main[] = array( '@type' => 'Question', 'name' => $it['q'], 'acceptedAnswer' => array('@type' => 'Answer', 'text' => wp_strip_all_tags($it['a'])), ); } // A page already typed FAQPage (by slug role) just receives its questions. $convertible = array('WebPage', 'ItemPage', 'CollectionPage', 'FAQPage'); if ($page_idx !== null && in_array($graph[$page_idx]['@type'], $convertible, true)) { $graph[$page_idx]['@type'] = 'FAQPage'; $graph[$page_idx]['mainEntity'] = $main; } else { $graph[] = array( '@type' => 'FAQPage', '@id' => citami_schema_id('faq', $url), 'url' => $url, 'isPartOf' => array('@id' => citami_schema_id('webpage', $url)), 'mainEntity' => $main, ); } } return $graph; }, 10, 2); /* ---------------------------------------------------------------- */ /* Editor fields (inside the SEO meta box) + save */ /* ---------------------------------------------------------------- */ add_action('citami_seo_meta_box_essentials', function ($post) { $tldr = citami_content_tldr($post->ID); $faq = citami_content_faq($post->ID); ?>


$it): ?>
$q) { $faq[] = array('q' => $q, 'a' => isset($as[$i]) ? $as[$i] : ''); } citami_content_save($post_id, isset($in['tldr']) ? $in['tldr'] : '', $faq); }, 10, 2); /* ---------------------------------------------------------------- */ /* Author profile fields */ /* ---------------------------------------------------------------- */ function citami_content_render_user_fields($user) { $uid = is_object($user) && isset($user->ID) ? (int) $user->ID : 0; if ($uid <= 0) { return; } $job = (string) get_user_meta($uid, 'citami_job_title', true); $sameas = implode("\n", citami_content_user_sameas($uid)); ?>

·

nonce before personal_options_update / edit_user_profile_update fire. if (isset($_POST['citami_job_title'])) { update_user_meta($user_id, 'citami_job_title', mb_substr(sanitize_text_field(wp_unslash((string) $_POST['citami_job_title'])), 0, 80)); } if (isset($_POST['citami_sameas'])) { $urls = array(); foreach (preg_split('/[\s,]+/', sanitize_textarea_field(wp_unslash((string) $_POST['citami_sameas']))) as $u) { if (preg_match('#^https?://#i', $u)) { $urls[] = esc_url_raw($u); } } update_user_meta($user_id, 'citami_sameas', implode("\n", array_unique(array_slice($urls, 0, 12)))); } // phpcs:enable WordPress.Security.NonceVerification.Missing } add_action('personal_options_update', 'citami_content_save_user_fields'); add_action('edit_user_profile_update', 'citami_content_save_user_fields'); /* ---------------------------------------------------------------- */ /* Settings rows (SEO tab) */ /* ---------------------------------------------------------------- */ add_action('citami_seo_settings_rows', function () { $s = citami_content_settings(); $opt = CITAMI_CONTENT_SETTINGS_OPT; ?> post_content); if ($ex['kind'] !== $kind) return array(); return citami_content_faq_new_items($post->ID, $ex['items']); } /** Yoast: the FAQ block makes the WebPage a FAQPage with Question pieces; ours join them. */ add_filter('wpseo_schema_graph', function ($graph) { if (!is_array($graph)) return $graph; $items = citami_content_extra_faq_for_schema('yoast'); if (empty($items)) return $graph; $url = get_permalink(); foreach ($graph as $i => $node) { $types = isset($node['@type']) ? (array) $node['@type'] : array(); if (!in_array('FAQPage', $types, true)) continue; $main = isset($node['mainEntity']) ? (array) $node['mainEntity'] : array(); foreach ($items as $n => $it) { $id = $url . '#citami-faq-' . ($n + 1); $main[] = array('@id' => $id); $graph[] = array('@type' => 'Question', '@id' => $id, 'url' => $id, 'name' => $it['q'], 'answerCount' => 1, 'acceptedAnswer' => array('@type' => 'Answer', 'text' => wp_strip_all_tags($it['a']))); } $graph[$i]['mainEntity'] = $main; break; } return $graph; }, 20); /** Rank Math: its FAQ block prints a FAQPage entity; ours join its mainEntity. */ add_filter('rank_math/json_ld', function ($data) { if (!is_array($data)) return $data; $items = citami_content_extra_faq_for_schema('rankmath'); if (empty($items)) return $data; foreach ($data as $k => $entity) { if (!is_array($entity)) continue; $types = isset($entity['@type']) ? (array) $entity['@type'] : array(); if (!in_array('FAQPage', $types, true)) continue; $main = isset($entity['mainEntity']) ? (array) $entity['mainEntity'] : array(); foreach ($items as $it) { $main[] = array('@type' => 'Question', 'name' => $it['q'], 'acceptedAnswer' => array('@type' => 'Answer', 'text' => wp_strip_all_tags($it['a']))); } $data[$k]['mainEntity'] = $main; break; } return $data; }, 20);