PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | breadcrumbs/class-metasync-breadcrumbs.php +70 -28 2.6.13 → 2.7.1 View file →
@@ -3,9 +3,8 @@
3 3 * MetaSync Breadcrumbs — core breadcrumb path resolver and HTML output.
4 4 *
5 5 * @package Metasync
6 6 * @subpackage Metasync/breadcrumbs
7 - * @since 2.9.0
8 7 */
9 8
10 9 if (!defined('ABSPATH')) {
11 10 exit;
@@ -46,9 +45,8 @@
46 45 $this->settings = $this->load_settings();
47 46
48 47 add_action('init', array($this, 'register_meta_fields'));
49 48 add_action('init', array($this, 'register_shortcode'));
50 - add_action('wp_enqueue_scripts', array($this, 'enqueue_styles'));
51 49 add_action('wp', array($this, 'maybe_override_woocommerce_breadcrumb'));
52 50
53 51 // Cross-write the per-post breadcrumb label override to Yoast / Rank Math
54 52 // so sites running those plugins stay in sync when editors use MetaSync.
@@ -56,20 +54,8 @@
56 54 add_action('added_post_meta', array($this, 'sync_breadcrumb_title_to_plugins'), 10, 4);
57 55 }
58 56
59 57 /**
60 - * Enqueue breadcrumb styles on the front-end.
61 - */
62 - public function enqueue_styles() {
63 - wp_enqueue_style(
64 - 'metasync-breadcrumbs',
65 - plugin_dir_url(dirname(__FILE__)) . 'breadcrumbs/css/metasync-breadcrumbs.css',
66 - array(),
67 - $this->version
68 - );
69 - }
70 -
71 - /**
72 58 * Replace WooCommerce breadcrumb with MetaSync's if the filter is enabled.
73 59 *
74 60 * Off by default — enable with:
75 61 * add_filter( 'metasync_override_woocommerce_breadcrumb', '__return_true' );
@@ -132,9 +118,16 @@
132 118 if (!is_array($saved)) {
133 119 $saved = array();
134 120 }
135 121
136 - return wp_parse_args($saved, $defaults);
122 + $settings = wp_parse_args($saved, $defaults);
123 +
124 + // Legacy accordion-era separators may be stored as HTML entities
125 + // ('»'). CSS and attribute contexts never decode entities, so
126 + // the trail would render the entity text literally — normalize here.
127 + $settings['separator'] = html_entity_decode((string) $settings['separator'], ENT_QUOTES, 'UTF-8');
128 +
129 + return $settings;
137 130 }
138 131
139 132 /**
140 133 * Cross-write `_metasync_breadcrumb_title` to Yoast / Rank Math meta keys.
@@ -163,16 +156,22 @@
163 156 }
164 157
165 158 $is_cross_writing = true;
166 159
167 - if (is_plugin_active('wordpress-seo/wp-seo.php') ||
168 - is_plugin_active('wordpress-seo-premium/wp-seo-premium.php')) {
169 - update_post_meta($post_id, '_yoast_wpseo_bctitle', $meta_value);
160 + // Consent gate and write-once backup — a breadcrumb title copied into
161 + // Yoast or Rank Math is a permanent write into another plugin's storage
162 + // like any other, so it asks the same permission and preserves whatever
163 + // the field held before.
164 + $may_write = class_exists('Metasync_Seo_Backup');
165 +
166 + if ($may_write && (is_plugin_active('wordpress-seo/wp-seo.php') ||
167 + is_plugin_active('wordpress-seo-premium/wp-seo-premium.php'))) {
168 + Metasync_Seo_Backup::write_post_meta($post_id, '_yoast_wpseo_bctitle', $meta_value);
170 169 }
171 170
172 - if (is_plugin_active('seo-by-rank-math/rank-math.php') ||
173 - is_plugin_active('seo-by-rankmath/rank-math.php')) {
174 - update_post_meta($post_id, 'rank_math_breadcrumb_title', $meta_value);
171 + if ($may_write && (is_plugin_active('seo-by-rank-math/rank-math.php') ||
172 + is_plugin_active('seo-by-rankmath/rank-math.php'))) {
173 + Metasync_Seo_Backup::write_post_meta($post_id, 'rank_math_breadcrumb_title', $meta_value);
175 174 }
176 175
177 176 $is_cross_writing = false;
178 177 }
@@ -357,10 +356,10 @@
357 356 }
358 357
359 358 // --- Search results ---
360 359 if (is_search()) {
361 - /* translators: %s: search query */
362 360 $trail[] = array(
361 + /* translators: %s: search query. */
363 362 'label' => sprintf(__('Search results for "%s"', 'metasync'), get_search_query()),
364 363 'url' => '',
365 364 );
366 365 return $trail;
@@ -384,11 +383,24 @@
384 383 // --- Taxonomy archive (generic) ---
385 384 if (is_tax()) {
386 385 $term = get_queried_object();
387 386 if ($term) {
388 - $taxonomy_obj = get_taxonomy($term->taxonomy);
389 - if ($taxonomy_obj) {
390 - $trail[] = array('label' => $taxonomy_obj->labels->name, 'url' => '');
387 + // Walk term ancestors so nested terms keep their parents, and so
388 + // every intermediate crumb carries a real URL. The taxonomy label
389 + // itself has no archive of its own to link to, so emitting it here
390 + // produced an intermediate ListItem with no `item` property, which
391 + // Google rejects.
392 + $ancestors = array_reverse(get_ancestors($term->term_id, $term->taxonomy, 'taxonomy'));
393 + foreach ($ancestors as $ancestor_id) {
394 + $ancestor = get_term($ancestor_id, $term->taxonomy);
395 + if (is_wp_error($ancestor) || !$ancestor) {
396 + continue;
397 + }
398 + $ancestor_link = get_term_link($ancestor);
399 + $trail[] = array(
400 + 'label' => $this->format_archive_label($ancestor->name),
401 + 'url' => is_wp_error($ancestor_link) ? '' : $ancestor_link,
402 + );
391 403 }
392 404 $trail[] = array('label' => $this->format_archive_label($term->name), 'url' => '');
393 405 }
394 406 return $trail;
@@ -406,9 +418,16 @@
406 418 private function resolve_woocommerce_trail($post_id = 0) {
407 419 $trail = array();
408 420
409 421 // Shop page.
422 + // wc_get_page_id() returns -1 (not 0) when the shop page is unset. -1 is
423 + // truthy, so an unguarded check yields get_the_title(-1) === '' and
424 + // get_permalink(-1) === false — an intermediate crumb with neither name
425 + // nor URL.
410 426 $shop_page_id = function_exists('wc_get_page_id') ? wc_get_page_id('shop') : 0;
427 + if ($shop_page_id < 1) {
428 + $shop_page_id = 0;
429 + }
411 430 $shop_label = $shop_page_id ? get_the_title($shop_page_id) : __('Shop', 'metasync');
412 431 $shop_url = $shop_page_id ? get_permalink($shop_page_id) : '';
413 432
414 433 // WooCommerce product category archive.
@@ -555,8 +574,25 @@
555 574 if (empty($trail)) {
556 575 return '';
557 576 }
558 577
578 + // Enqueue on render: the stylesheet only targets this markup, so it is
579 + // loaded here — after the enabled check and a confirmed trail — rather
580 + // than site-wide from wp_enqueue_scripts. Every render path (shortcode,
581 + // template function, WooCommerce override) funnels through this method,
582 + // and a late-enqueued style prints in the footer.
583 + // The template function and the schema module both instantiate this class
584 + // with the constructor's default-version instance ('1.0.0'), so whichever
585 + // render path runs first would otherwise decide the cache-bust version.
586 + // Prefer the plugin-wide constant so the version is consistent regardless
587 + // of which instance renders.
588 + wp_enqueue_style(
589 + 'metasync-breadcrumbs',
590 + plugin_dir_url(dirname(__FILE__)) . 'breadcrumbs/css/metasync-breadcrumbs.css',
591 + array(),
592 + defined('METASYNC_VERSION') ? METASYNC_VERSION : $this->version
593 + );
594 +
559 595 $separator = $this->settings['separator'];
560 596 $prefix_text = $this->settings['prefix_text'];
561 597 $show_current = $this->settings['show_current_page'];
562 598
@@ -597,11 +633,17 @@
597 633 if (!empty($prefix_text)) {
598 634 $prefix_html = '<span class="metasync-breadcrumb__prefix">' . esc_html($prefix_text) . '</span>';
599 635 }
600 636
601 - // Add dynamic separator CSS for the configured separator character
602 - $separator_escaped = esc_attr($separator);
603 - $separator_style = '<style>.metasync-breadcrumb__item[data-separator]::after { content: " ' . $separator_escaped . ' "; }</style>';
637 + // Add dynamic separator CSS for the configured separator character.
638 + // <style> is a raw-text element: HTML entities are not decoded there,
639 + // so esc_attr() would print "&gt;" literally for the ">" preset.
640 + // Escape for a CSS string (backslash + double quote) instead.
641 + $separator_css = str_replace(array('\\', '"'), array('\\\\', '\\"'), $separator);
642 + // A stored "</" sequence would close the raw-text <style> element
643 + // early. Break the token with a CSS escape ("<\/" still renders "</").
644 + $separator_css = preg_replace('#</#', '<\\/', $separator_css);
645 + $separator_style = '<style>.metasync-breadcrumb__item[data-separator]::after { content: " ' . $separator_css . ' "; }</style>';
604 646
605 647 $html = $separator_style
606 648 . '<nav class="metasync-breadcrumb" aria-label="breadcrumb">'
607 649 . $prefix_html