with @graph and stable */ /* @ids. Deference like meta: off automatically when Yoast / Rank */ /* Math is active (they print their own graph). */ /* ────────────────────────────────────────────────────────────────── */ define('CITAMI_SCHEMA_SETTINGS_OPT', 'citami_schema_settings'); define('CITAMI_META_SCHEMA_TYPE', '_citami_schema_type'); define('CITAMI_META_SCHEMA_OFF', '_citami_schema_off'); /** Schema types offered per post type (first = default). */ function citami_schema_type_choices($post_type) { if ($post_type === 'post') { return array('Article', 'BlogPosting', 'NewsArticle', 'TechArticle', 'WebPage', 'None'); } if ($post_type === 'product') { return array('Product', 'WebPage', 'None'); } if ($post_type === 'page') { return array('WebPage', 'AboutPage', 'ContactPage', 'FAQPage', 'CollectionPage', 'Article', 'None'); } return array('WebPage', 'Article', 'ItemPage', 'None'); } function citami_schema_default_settings() { return array( 'enabled' => 'auto', 'defaults' => array('post' => 'Article', 'page' => 'WebPage', 'product' => 'Product'), 'entity' => array( 'type' => 'Organization', 'name' => '', 'url' => '', 'logo' => '', 'telephone' => '', 'email' => '', 'sameas' => '', 'street' => '', 'city' => '', 'postal' => '', 'region' => '', 'country' => '', // WP6 — entity manager extras 'wikidata' => '', 'description' => '', 'founding_date' => '', 'founders' => '', ), 'breadcrumb_home' => '', ); } /** WP6 — Wikidata / founders / founding date / description merged into the * identity node WITHOUT overriding what the platform already provides. */ function citami_schema_entity_extras(array $node) { $e = citami_schema_settings()['entity']; if ($e['wikidata'] !== '') { $qid = strtoupper(preg_replace('/[^Qq0-9]/', '', $e['wikidata'])); if (preg_match('/^Q\d+$/', $qid)) { $url = 'https://www.wikidata.org/wiki/' . $qid; $same = isset($node['sameAs']) ? (is_array($node['sameAs']) ? $node['sameAs'] : array($node['sameAs'])) : array(); if (!in_array($url, $same, true)) { $same[] = $url; } $node['sameAs'] = array_values($same); } } if ($e['description'] !== '' && empty($node['description'])) { $node['description'] = $e['description']; } if ($e['founding_date'] !== '' && empty($node['foundingDate'])) { $node['foundingDate'] = $e['founding_date']; } if ($e['founders'] !== '' && empty($node['founder'])) { $founders = array(); foreach (preg_split('/\s*,\s*/', $e['founders']) as $name) { $name = trim($name); if ($name !== '') { $founders[] = array('@type' => 'Person', 'name' => $name); } } if (!empty($founders)) { $node['founder'] = $founders; } } return $node; } function citami_schema_settings() { $d = citami_schema_default_settings(); $s = get_option(CITAMI_SCHEMA_SETTINGS_OPT, array()); if (!is_array($s)) { $s = array(); } $out = $d; if (isset($s['enabled']) && in_array($s['enabled'], array('auto', 'on', 'off'), true)) { $out['enabled'] = $s['enabled']; } if (isset($s['defaults']) && is_array($s['defaults'])) { foreach ($s['defaults'] as $type => $schema) { if (is_string($type) && is_string($schema) && $schema !== '') { $out['defaults'][sanitize_key($type)] = $schema; } } } if (isset($s['entity']) && is_array($s['entity'])) { foreach ($d['entity'] as $k => $v) { if (isset($s['entity'][$k]) && is_string($s['entity'][$k])) { $out['entity'][$k] = $s['entity'][$k]; } } } if (isset($s['breadcrumb_home']) && is_string($s['breadcrumb_home'])) { $out['breadcrumb_home'] = $s['breadcrumb_home']; } return $out; } function citami_sanitize_schema_settings($value) { $in = is_array($value) ? $value : array(); $d = citami_schema_default_settings(); $out = array('enabled' => 'auto', 'defaults' => array(), 'entity' => array(), 'breadcrumb_home' => ''); if (isset($in['enabled']) && in_array($in['enabled'], array('auto', 'on', 'off'), true)) { $out['enabled'] = $in['enabled']; } if (isset($in['defaults']) && is_array($in['defaults'])) { foreach ($in['defaults'] as $type => $schema) { $type = sanitize_key((string) $type); $schema = preg_replace('/[^A-Za-z]/', '', (string) $schema); if ($type !== '' && $schema !== '' && in_array($schema, citami_schema_type_choices($type), true)) { $out['defaults'][$type] = $schema; } } } foreach ($d['entity'] as $k => $v) { $raw = isset($in['entity'][$k]) ? (string) $in['entity'][$k] : ''; if ($k === 'type') { $raw = preg_replace('/[^A-Za-z]/', '', $raw); $out['entity'][$k] = $raw === '' ? 'Organization' : $raw; } elseif ($k === 'url' || $k === 'logo') { $out['entity'][$k] = preg_match('#^https?://#i', $raw) ? esc_url_raw($raw) : ''; } elseif ($k === 'wikidata') { $q = strtoupper(preg_replace('/[^Qq0-9]/', '', $raw)); $out['entity'][$k] = preg_match('/^Q\d+$/', $q) ? $q : ''; } elseif ($k === 'founding_date') { $out['entity'][$k] = preg_match('/^\d{4}(-\d{2}(-\d{2})?)?$/', trim($raw)) ? trim($raw) : ''; } elseif ($k === 'description') { $out['entity'][$k] = mb_substr(sanitize_text_field($raw), 0, 500); } elseif ($k === 'sameas') { $urls = array(); foreach (preg_split('/[\s,]+/', $raw) as $u) { if (preg_match('#^https?://#i', $u)) { $urls[] = esc_url_raw($u); } } $out['entity'][$k] = implode("\n", array_unique($urls)); } else { $out['entity'][$k] = mb_substr(sanitize_text_field($raw), 0, 200); } } $out['breadcrumb_home'] = mb_substr(sanitize_text_field(isset($in['breadcrumb_home']) ? (string) $in['breadcrumb_home'] : ''), 0, 60); return $out; } function citami_schema_enabled() { $s = citami_schema_settings(); if ($s['enabled'] === 'on') { return true; } if ($s['enabled'] === 'off') { return false; } return citami_active_seo_plugin() === null; } /* ---------------------------------------------------------------- */ /* @ids + nodes */ /* ---------------------------------------------------------------- */ function citami_schema_home_url() { return trailingslashit(home_url('/')); } function citami_schema_id($fragment, $base = null) { return ($base === null ? citami_schema_home_url() : trailingslashit($base)) . '#' . $fragment; } /** * Identity node: the platform's verified block (stored by apply-fix) * wins; else the entity settings; else a minimal Organization from the * site name. Always carries the conventional @id {home}/#organization. */ function citami_schema_organization_node() { $node = citami_get_home_schema_node(); if (!is_array($node)) { $e = citami_schema_settings()['entity']; $node = array( '@type' => $e['type'] !== '' ? $e['type'] : 'Organization', 'name' => $e['name'] !== '' ? $e['name'] : get_bloginfo('name'), 'url' => $e['url'] !== '' ? $e['url'] : citami_schema_home_url(), ); if ($e['logo'] !== '') { $node['logo'] = array('@type' => 'ImageObject', 'url' => $e['logo']); } if ($e['telephone'] !== '') { $node['telephone'] = $e['telephone']; } if ($e['email'] !== '') { $node['email'] = $e['email']; } $sameas = array_values(array_filter(array_map('trim', preg_split('/\s+/', $e['sameas'])))); if (!empty($sameas)) { $node['sameAs'] = $sameas; } if ($e['street'] !== '' || $e['city'] !== '') { $addr = array('@type' => 'PostalAddress'); if ($e['street'] !== '') $addr['streetAddress'] = $e['street']; if ($e['city'] !== '') $addr['addressLocality'] = $e['city']; if ($e['postal'] !== '') $addr['postalCode'] = $e['postal']; if ($e['region'] !== '') $addr['addressRegion'] = $e['region']; if ($e['country'] !== '') $addr['addressCountry'] = $e['country']; $node['address'] = $addr; } } // 2.48 — the truth sheet is the identity: when it has a name, its node // wins (name, aliases, profiles, Wikidata, founders, address) and the // platform block / settings only add fields the sheet does not carry. // One Organization on the home, never two. $sheet_node = citami_schema_sheet_node(); if (is_array($sheet_node)) { foreach ($node as $k => $v) { if ($k !== '' && $k[0] !== '@' && !isset($sheet_node[$k])) { $sheet_node[$k] = $v; } } $node = $sheet_node; } unset($node['@context']); $node['@id'] = citami_schema_id('organization'); if (empty($node['url'])) { $node['url'] = citami_schema_home_url(); } return citami_schema_entity_extras($node); } /** The sheet's Organization node, or null when the sheet has no name yet. */ function citami_schema_sheet_node() { if (!function_exists('citami_truth_sheet') || !function_exists('citami_truth_org_node')) { return null; } $sheet = citami_truth_sheet(); if (!is_array($sheet) || !isset($sheet['name']) || trim((string) $sheet['name']) === '') { return null; } $node = citami_truth_org_node(); return is_array($node) ? $node : null; } function citami_schema_website_node() { $node = array( '@type' => 'WebSite', '@id' => citami_schema_id('website'), 'url' => citami_schema_home_url(), 'name' => get_bloginfo('name'), 'publisher' => array('@id' => citami_schema_id('organization')), 'inLanguage' => str_replace('_', '-', get_locale()), ); $tagline = get_bloginfo('description'); if ($tagline !== '') { $node['description'] = $tagline; } $node['potentialAction'] = array( '@type' => 'SearchAction', 'target' => array('@type' => 'EntryPoint', 'urlTemplate' => citami_schema_home_url() . '?s={search_term_string}'), 'query-input' => 'required name=search_term_string', ); return $node; } /** Effective schema type for a post (meta override → post-type default). */ function citami_schema_type_for_post($post) { $override = (string) get_post_meta($post->ID, CITAMI_META_SCHEMA_TYPE, true); $choices = citami_schema_type_choices($post->post_type); if ($override !== '' && in_array($override, $choices, true)) { return $override; } $defaults = citami_schema_settings()['defaults']; if (isset($defaults[$post->post_type]) && in_array($defaults[$post->post_type], $choices, true)) { return $defaults[$post->post_type]; } return $choices[0]; } /** Page role from the platform's routing (about/faq slugs). */ function citami_schema_page_role_type($post, $chosen) { if ($chosen !== 'WebPage' || $post->post_type !== 'page') { return $chosen; } $slug = (string) $post->post_name; if (in_array($slug, citami_about_page_slugs(), true)) { return 'AboutPage'; } if (in_array($slug, citami_faq_page_slugs(), true)) { return 'FAQPage'; } if (in_array($slug, array('contatti', 'contact', 'contact-us', 'contattaci'), true)) { return 'ContactPage'; } return 'WebPage'; } function citami_schema_breadcrumb_node($post, $url) { // WP7 — same trail as the visible breadcrumb (includes/breadcrumb.php). $items = array(); $pos = 1; foreach (citami_breadcrumb_items($post) as $it) { $li = array('@type' => 'ListItem', 'position' => $pos++, 'name' => $it['name']); if ($it['url'] !== '') { $li['item'] = $it['url']; } $items[] = $li; } return array('@type' => 'BreadcrumbList', '@id' => citami_schema_id('breadcrumb', $url), 'itemListElement' => $items); } function citami_schema_person_node($author_id) { $author_id = (int) $author_id; if ($author_id <= 0) { return null; } $name = function_exists('get_the_author_meta') ? (string) get_the_author_meta('display_name', $author_id) : ''; if ($name === '') { return null; } $node = array( '@type' => 'Person', '@id' => citami_schema_home_url() . '#/schema/person/' . $author_id, 'name' => $name, ); $desc = function_exists('get_the_author_meta') ? (string) get_the_author_meta('description', $author_id) : ''; if ($desc !== '') { $node['description'] = wp_strip_all_tags($desc); } if (function_exists('get_author_posts_url')) { $node['url'] = get_author_posts_url($author_id); } // WP6 fills these from the author box settings; read if present. $sameas = function_exists('get_user_meta') ? get_user_meta($author_id, 'citami_sameas', true) : ''; if (is_string($sameas) && trim($sameas) !== '') { $node['sameAs'] = array_values(array_filter(array_map('trim', preg_split('/[\s,]+/', $sameas)))); } elseif (is_array($sameas) && !empty($sameas)) { $node['sameAs'] = array_values($sameas); } $job = function_exists('get_user_meta') ? get_user_meta($author_id, 'citami_job_title', true) : ''; if (is_string($job) && $job !== '') { $node['jobTitle'] = $job; } return $node; } function citami_schema_featured_image($post) { if (!function_exists('get_the_post_thumbnail_url')) { return ''; } $url = get_the_post_thumbnail_url($post->ID, 'full'); return is_string($url) ? $url : ''; } /** WebPage-family node for a singular post. */ function citami_schema_webpage_node($post, $type, $url, $title, $description) { $node = array( '@type' => $type, '@id' => citami_schema_id('webpage', $url), 'url' => $url, 'name' => $title, 'isPartOf' => array('@id' => citami_schema_id('website')), 'about' => array('@id' => citami_schema_id('organization')), 'inLanguage' => str_replace('_', '-', get_locale()), 'breadcrumb' => array('@id' => citami_schema_id('breadcrumb', $url)), ); if ($description !== '') { $node['description'] = $description; } if (!empty($post->post_date)) { $node['datePublished'] = citami_schema_iso_date($post->post_date); } if (!empty($post->post_modified)) { $node['dateModified'] = citami_schema_iso_date($post->post_modified); } $img = citami_schema_featured_image($post); if ($img !== '') { $node['primaryImageOfPage'] = array('@type' => 'ImageObject', 'url' => $img); } return $node; } function citami_schema_article_node($post, $type, $url, $title, $description, $person) { $node = array( '@type' => $type, '@id' => citami_schema_id('article', $url), 'headline' => mb_substr($title, 0, 110), 'mainEntityOfPage' => array('@id' => citami_schema_id('webpage', $url)), 'isPartOf' => array('@id' => citami_schema_id('webpage', $url)), 'publisher' => array('@id' => citami_schema_id('organization')), 'inLanguage' => str_replace('_', '-', get_locale()), ); if ($description !== '') { $node['description'] = $description; } if (!empty($post->post_date)) { $node['datePublished'] = citami_schema_iso_date($post->post_date); } if (!empty($post->post_modified)) { $node['dateModified'] = citami_schema_iso_date($post->post_modified); } if ($person !== null) { $node['author'] = array('@id' => $person['@id']); } else { $node['author'] = array('@id' => citami_schema_id('organization')); } $img = citami_schema_featured_image($post); if ($img !== '') { $node['image'] = array('@type' => 'ImageObject', 'url' => $img); } $body_html = citami_post_content_html($post); if ($body_html !== '') { $words = str_word_count(wp_strip_all_tags($body_html)); if ($words > 0) { $node['wordCount'] = $words; } } if (function_exists('get_the_category')) { $cats = get_the_category($post->ID); if (is_array($cats) && !empty($cats)) { $node['articleSection'] = array_values(array_filter(array_map(function ($c) { return isset($c->name) ? (string) $c->name : ''; }, $cats))); } } return $node; } /** WooCommerce product node (only when WooCommerce is present). */ function citami_schema_product_node($post, $url, $title, $description) { if (!function_exists('wc_get_product')) { return null; } $product = wc_get_product($post->ID); if (!$product) { return null; } $node = array( '@type' => 'Product', '@id' => citami_schema_id('product', $url), 'name' => $title, 'url' => $url, 'mainEntityOfPage' => array('@id' => citami_schema_id('webpage', $url)), ); if ($description !== '') { $node['description'] = $description; } $sku = method_exists($product, 'get_sku') ? (string) $product->get_sku() : ''; if ($sku !== '') { $node['sku'] = $sku; } $img = citami_schema_featured_image($post); if ($img !== '') { $node['image'] = $img; } $price = method_exists($product, 'get_price') ? (string) $product->get_price() : ''; if ($price !== '' && function_exists('get_woocommerce_currency')) { $node['offers'] = array( '@type' => 'Offer', 'url' => $url, 'price' => $price, 'priceCurrency' => get_woocommerce_currency(), 'availability' => (method_exists($product, 'is_in_stock') && $product->is_in_stock()) ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock', 'seller' => array('@id' => citami_schema_id('organization')), ); } return $node; } function citami_schema_iso_date($mysql) { $ts = strtotime((string) $mysql); return $ts ? gmdate('c', $ts) : (string) $mysql; } /** Rank Math nodes staged by the importer, normalised for the graph. */ function citami_schema_imported_nodes($post, $url) { $raw = (string) get_post_meta($post->ID, CITAMI_META_SCHEMA_IMPORTED, true); if ($raw === '') { return array(); } $nodes = json_decode($raw, true); if (!is_array($nodes)) { return array(); } $out = array(); foreach ($nodes as $i => $n) { if (!is_array($n) || empty($n['@type'])) { continue; } unset($n['@context'], $n['metadata']); if (empty($n['@id'])) { $n['@id'] = citami_schema_id('imported-' . ($i + 1), $url); } $out[] = $n; } return $out; } /* ---------------------------------------------------------------- */ /* Graph */ /* ---------------------------------------------------------------- */ /** The @graph for the current request (empty array = print nothing). */ function citami_schema_graph($ctx = null) { $ctx = $ctx === null ? citami_seo_context() : $ctx; $graph = array(); if ($ctx['kind'] === 'home') { $graph[] = citami_schema_website_node(); $graph[] = citami_schema_organization_node(); // WP9 — modules (locations) enrich the home graph too. return apply_filters('citami_schema_graph', $graph, $ctx); } if ($ctx['kind'] !== 'singular' || !$ctx['post']) { return $graph; } $post = $ctx['post']; if ((string) get_post_meta($post->ID, CITAMI_META_SCHEMA_OFF, true) === '1') { return $graph; } $chosen = citami_schema_type_for_post($post); if ($chosen === 'None') { return $graph; } $url = get_permalink($post); $title = citami_seo_title($ctx); if ($title === null || $title === '') { $title = (string) $post->post_title; } $description = citami_seo_description($ctx); $graph[] = citami_schema_website_node(); $graph[] = citami_schema_organization_node(); $article_types = array('Article', 'BlogPosting', 'NewsArticle', 'TechArticle'); $page_types = array('WebPage', 'AboutPage', 'ContactPage', 'FAQPage', 'CollectionPage', 'ItemPage'); $person = null; if (in_array($chosen, $article_types, true)) { $person = citami_schema_person_node(isset($post->post_author) ? $post->post_author : 0); $graph[] = citami_schema_webpage_node($post, 'WebPage', $url, $title, $description); $graph[] = citami_schema_article_node($post, $chosen, $url, $title, $description, $person); if ($person !== null) { $graph[] = $person; } } elseif ($chosen === 'Product') { $graph[] = citami_schema_webpage_node($post, 'ItemPage', $url, $title, $description); $product = citami_schema_product_node($post, $url, $title, $description); if ($product !== null) { $graph[] = $product; } } elseif (in_array($chosen, $page_types, true)) { $type = citami_schema_page_role_type($post, $chosen); $graph[] = citami_schema_webpage_node($post, $type, $url, $title, $description); } else { $graph[] = citami_schema_webpage_node($post, 'WebPage', $url, $title, $description); } $graph[] = citami_schema_breadcrumb_node($post, $url); foreach (citami_schema_imported_nodes($post, $url) as $n) { $graph[] = $n; } return apply_filters('citami_schema_graph', $graph, $ctx); } function citami_schema_print() { if (!citami_schema_enabled()) { return; } $graph = citami_schema_graph(); if (empty($graph)) { return; } $json = wp_json_encode(array('@context' => 'https://schema.org', '@graph' => array_values($graph)), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG); if (!is_string($json)) { return; } wp_print_inline_script_tag($json, array('type' => 'application/ld+json', 'class' => 'citami-schema-graph')); } add_action('wp_head', 'citami_schema_print', 6); /* ---------------------------------------------------------------- */ /* Meta box fields + save (hooked into the SEO meta box) */ /* ---------------------------------------------------------------- */ add_action('citami_seo_meta_box_fields', function ($post) { $choices = citami_schema_type_choices($post->post_type); $current = (string) get_post_meta($post->ID, CITAMI_META_SCHEMA_TYPE, true); $off = (string) get_post_meta($post->ID, CITAMI_META_SCHEMA_OFF, true) === '1'; $default = citami_schema_settings()['defaults']; $default_type = isset($default[$post->post_type]) ? $default[$post->post_type] : $choices[0]; ?>
Schema
ID, CITAMI_META_SCHEMA_IMPORTED, true); if ($imported !== ''): ?>
|
|
|