PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.2
2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 All 57 releases
← All changes | includes/admin/importers/class-yoast-exporter.php +133 -10 2.7.0 → 2.14.2 View file →
@@ -128,10 +128,10 @@
128 128 'object_id' => $post_id,
129 129 'object_type' => 'post',
130 130 'source_plugin' => $this->plugin_slug,
131 131 'data' => [
132 - 'seo_title' => $this->convert_template_variables($meta['_yoast_wpseo_title'] ?? '', $post_id),
133 - 'meta_description' => $this->convert_template_variables($meta['_yoast_wpseo_metadesc'] ?? '', $post_id),
132 + 'seo_title' => $this->convert_post_value($meta['_yoast_wpseo_title'] ?? '', $post_id),
133 + 'meta_description' => $this->convert_post_value($meta['_yoast_wpseo_metadesc'] ?? '', $post_id),
134 134 'focus_keyword' => $meta['_yoast_wpseo_focuskw'] ?? '',
135 135 // Full keyword list (primary + Yoast Premium's additional
136 136 // keyphrases) for ThinkRank's keyword array — without this
137 137 // the migrator falls back to the single focus_keyword and
@@ -148,15 +148,16 @@
148 148 'nofollow' => $robots['nofollow'],
149 149 'noarchive' => $advanced_robots['noarchive'],
150 150 'noimageindex' => $advanced_robots['noimageindex'],
151 151 'nosnippet' => $advanced_robots['nosnippet'],
152 - 'og_title' => $this->convert_template_variables($meta['_yoast_wpseo_opengraph-title'] ?? '', $post_id),
153 - 'og_description' => $this->convert_template_variables($meta['_yoast_wpseo_opengraph-description'] ?? '', $post_id),
152 + 'og_title' => $this->convert_post_value($meta['_yoast_wpseo_opengraph-title'] ?? '', $post_id),
153 + 'og_description' => $this->convert_post_value($meta['_yoast_wpseo_opengraph-description'] ?? '', $post_id),
154 154 'og_image' => $meta['_yoast_wpseo_opengraph-image'] ?? '',
155 - 'twitter_title' => $this->convert_template_variables($meta['_yoast_wpseo_twitter-title'] ?? '', $post_id),
156 - 'twitter_description' => $this->convert_template_variables($meta['_yoast_wpseo_twitter-description'] ?? '', $post_id),
155 + 'twitter_title' => $this->convert_post_value($meta['_yoast_wpseo_twitter-title'] ?? '', $post_id),
156 + 'twitter_description' => $this->convert_post_value($meta['_yoast_wpseo_twitter-description'] ?? '', $post_id),
157 157 'twitter_image' => $meta['_yoast_wpseo_twitter-image'] ?? '',
158 158 'primary_category' => (int) ($meta['_yoast_wpseo_primary_category'] ?? 0),
159 + 'product_identifier' => $this->extract_product_identifier($meta),
159 160 'schema_type' => $meta['_yoast_wpseo_schema_page_type'] ?? '',
160 161 // Yoast "cornerstone content" maps to ThinkRank pillar content.
161 162 'pillar_content' => (int) ($meta['_yoast_wpseo_is_cornerstone'] ?? 0),
162 163 ],
@@ -208,16 +209,16 @@
208 209 'object_id' => $term_id,
209 210 'object_type' => 'term',
210 211 'source_plugin' => $this->plugin_slug,
211 212 'data' => [
212 - 'seo_title' => $this->convert_template_variables($meta['wpseo_title'] ?? ''),
213 - 'meta_description' => $this->convert_template_variables($meta['wpseo_desc'] ?? ''),
213 + 'seo_title' => $this->convert_term_value($meta['wpseo_title'] ?? ''),
214 + 'meta_description' => $this->convert_term_value($meta['wpseo_desc'] ?? ''),
214 215 'focus_keyword' => $meta['wpseo_focuskw'] ?? '',
215 216 'canonical_url' => $meta['wpseo_canonical'] ?? '',
216 217 'noindex' => $noindex,
217 218 'nofollow' => 0,
218 - 'og_title' => $this->convert_template_variables($meta['wpseo_opengraph-title'] ?? ''),
219 - 'og_description' => $this->convert_template_variables($meta['wpseo_opengraph-description'] ?? ''),
219 + 'og_title' => $this->convert_term_value($meta['wpseo_opengraph-title'] ?? ''),
220 + 'og_description' => $this->convert_term_value($meta['wpseo_opengraph-description'] ?? ''),
220 221 ],
221 222 'extended' => [
222 223 'breadcrumb_title' => $meta['wpseo_bctitle'] ?? '',
223 224 'og_image' => $meta['wpseo_opengraph-image'] ?? '',
@@ -513,8 +514,71 @@
513 514 return trim($value);
514 515 }
515 516
516 517 /**
518 + * Yoast per-post tokens ThinkRank resolves per request (#886): kept as
519 + * tags rather than frozen into text. Mirrors convert_template_pattern().
520 + */
521 + private const POST_TOKENS = [
522 + '%%title%%' => '%title%',
523 + '%%sitename%%' => '%sitename%',
524 + '%%sep%%' => '%sep%',
525 + '%%excerpt%%' => '%excerpt%',
526 + '%%date%%' => '%date%',
527 + '%%modified%%' => '%modified%',
528 + '%%name%%' => '%author%',
529 + '%%category%%' => '%category%',
530 + '%%primary_category%%' => '%category%',
531 + '%%wc_price%%' => '%price%',
532 + '%%wc_sku%%' => '%sku%',
533 + '%%wc_shortdesc%%' => '%short_description%',
534 + '%%wc_brand%%' => '%brand%',
535 + ];
536 +
537 + /**
538 + * Yoast per-term tokens ThinkRank resolves on a term archive. The term's
539 + * own name and description used to be stripped outright, since the
540 + * literal resolver only knows post context.
541 + */
542 + private const TERM_TOKENS = [
543 + '%%term_title%%' => '%term%',
544 + '%%term_description%%' => '%excerpt%',
545 + '%%category_description%%' => '%excerpt%',
546 + '%%tag_description%%' => '%excerpt%',
547 + '%%sitename%%' => '%sitename%',
548 + '%%sep%%' => '%sep%',
549 + ];
550 +
551 + /**
552 + * A per-post Yoast value: mapped tokens as ThinkRank tags, the rest literal.
553 + *
554 + * @param mixed $value Raw Yoast value
555 + * @param int $post_id Post ID
556 + * @return string
557 + */
558 + private function convert_post_value($value, int $post_id): string {
559 + return $this->tokenize_object_template(
560 + $this->stringify_template_value($value),
561 + self::literal_token_patterns(self::POST_TOKENS),
562 + fn(string $rest): string => $this->convert_template_variables($rest, $post_id)
563 + );
564 + }
565 +
566 + /**
567 + * A per-term Yoast value: mapped tokens as ThinkRank tags, the rest literal.
568 + *
569 + * @param mixed $value Raw Yoast value
570 + * @return string
571 + */
572 + private function convert_term_value($value): string {
573 + return $this->tokenize_object_template(
574 + $this->stringify_template_value($value),
575 + self::literal_token_patterns(self::TERM_TOKENS),
576 + fn(string $rest): string => $this->convert_template_variables($rest)
577 + );
578 + }
579 +
580 + /**
517 581 * Translate a Yoast title/description *template* into ThinkRank pattern syntax.
518 582 *
519 583 * Unlike convert_template_variables() — which resolves a per-post value to a
520 584 * literal string — post-type and taxonomy templates must stay templates so the
@@ -549,8 +613,15 @@
549 613 '%%modified%%' => '%modified%',
550 614 '%%name%%' => '%author%',
551 615 '%%category%%' => '%category%',
552 616 '%%primary_category%%' => '%category%',
617 + // WooCommerce SEO product variables. Stripped rather than mapped
618 + // until #715, so a product description template imported as "Buy
619 + // %title% for" with the price and stock gone.
620 + '%%wc_price%%' => '%price%',
621 + '%%wc_sku%%' => '%sku%',
622 + '%%wc_shortdesc%%' => '%short_description%',
623 + '%%wc_brand%%' => '%brand%',
553 624 ];
554 625
555 626 $template = str_replace(array_keys($map), array_values($map), $template);
556 627
@@ -788,8 +859,22 @@
788 859 // Math tokens wrapped in Yoast's double-percent syntax — tokens Yoast
789 860 // itself cannot resolve either. Map them rather than stripping them,
790 861 // or the format silently loses its search term / term name.
791 862 '%%search_query%%' => '%search_term%',
863 + // NOT the WooCommerce product variables, deliberately. This
864 + // converter writes Site Identity title formats, and those are
865 + // resolved by SEO_Manager::process_title_template() against its own
866 + // vocabulary — %site_title%, %separator%, %search_term% and the
867 + // rest — which has no %price%/%sku%/%brand% and, unlike
868 + // Pattern_Resolver, does not strip what it cannot resolve. Mapping
869 + // them here published "Search pennant - %price% - Media Wipe" in a
870 + // live <title>: a silent loss traded for the literal leak #715 is
871 + // about. The formats this feeds are the homepage, search, archive,
872 + // author, category and tag titles, where there is no one product
873 + // for a price to belong to either way, so the strip below is the
874 + // right answer. The per-post converter keeps them, because
875 + // Pattern_Resolver does resolve them: see
876 + // convert_template_pattern().
792 877 ];
793 878 if ($context_token !== '') {
794 879 $map['%%title%%'] = $context_token;
795 880 $map['%%term_title%%'] = $context_token;
@@ -992,6 +1077,44 @@
992 1077 }
993 1078 }
994 1079
995 1080 return $settings;
1081 + }
1082 +
1083 + /**
1084 + * A product's identifier from Yoast WooCommerce SEO.
1085 + *
1086 + * Yoast stores every identifier type in one serialised map keyed by type
1087 + * (`gtin8`, `gtin12`, `gtin13`, `gtin14`, `isbn`, `mpn`) rather than in a
1088 + * field per type. ThinkRank Pro holds a single identifier, so the first
1089 + * non-empty one wins, in the order Google prefers: a GTIN identifies the
1090 + * product itself, an MPN only identifies it within one manufacturer, and
1091 + * an ISBN applies to books alone.
1092 + *
1093 + * @since 2.10.1
1094 + *
1095 + * @param array $meta Post meta for the product.
1096 + * @return string Identifier, or '' when the product has none.
1097 + */
1098 + private function extract_product_identifier(array $meta): string {
1099 + $raw = $meta['wpseo_global_identifier_values'] ?? '';
1100 +
1101 + if (is_string($raw) && '' !== $raw) {
1102 + $decoded = maybe_unserialize($raw);
1103 + $raw = is_array($decoded) ? $decoded : [];
1104 + }
1105 +
1106 + if (!is_array($raw) || empty($raw)) {
1107 + return '';
1108 + }
1109 +
1110 + foreach (['gtin13', 'gtin14', 'gtin12', 'gtin8', 'mpn', 'isbn'] as $type) {
1111 + $value = trim((string) ($raw[$type] ?? ''));
1112 +
1113 + if ('' !== $value) {
1114 + return $value;
1115 + }
1116 + }
1117 +
1118 + return '';
996 1119 }
997 1120 }