PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.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 1.27.0 1.26.0 1.25.0 All 54 releases
← All changes | includes/admin/importers/class-yoast-exporter.php +137 -11 2.2.0 → 2.13.0 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'] ?? '',
@@ -479,9 +480,12 @@
479 480 if ($post_id) {
480 481 $post = get_post($post_id);
481 482 if ($post) {
482 483 $replacements['%%title%%'] = $post->post_title;
483 - $replacements['%%excerpt%%'] = wp_trim_words($post->post_excerpt ?: wp_trim_words(wp_strip_all_tags($post->post_content), 55), 55);
484 + $replacements['%%excerpt%%'] = \ThinkRank\Core\Seo_Text::trim_words(
485 + $post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words(wp_strip_all_tags($post->post_content), 55),
486 + 55
487 + );
484 488 $replacements['%%date%%'] = get_the_date('', $post);
485 489 $replacements['%%modified%%'] = get_the_modified_date('', $post);
486 490 $replacements['%%id%%'] = (string) $post_id;
487 491 $replacements['%%name%%'] = get_the_author_meta('display_name', (int) $post->post_author);
@@ -510,8 +514,71 @@
510 514 return trim($value);
511 515 }
512 516
513 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 + /**
514 581 * Translate a Yoast title/description *template* into ThinkRank pattern syntax.
515 582 *
516 583 * Unlike convert_template_variables() — which resolves a per-post value to a
517 584 * literal string — post-type and taxonomy templates must stay templates so the
@@ -546,8 +613,15 @@
546 613 '%%modified%%' => '%modified%',
547 614 '%%name%%' => '%author%',
548 615 '%%category%%' => '%category%',
549 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%',
550 624 ];
551 625
552 626 $template = str_replace(array_keys($map), array_values($map), $template);
553 627
@@ -785,8 +859,22 @@
785 859 // Math tokens wrapped in Yoast's double-percent syntax — tokens Yoast
786 860 // itself cannot resolve either. Map them rather than stripping them,
787 861 // or the format silently loses its search term / term name.
788 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().
789 877 ];
790 878 if ($context_token !== '') {
791 879 $map['%%title%%'] = $context_token;
792 880 $map['%%term_title%%'] = $context_token;
@@ -989,6 +1077,44 @@
989 1077 }
990 1078 }
991 1079
992 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 '';
993 1119 }
994 1120 }