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-rankmath-exporter.php +134 -12 2.0.2 → 2.13.0 View file →
@@ -182,8 +182,17 @@
182 182 if ($log_count > 0) {
183 183 $types['404_logs'] = $log_count;
184 184 }
185 185
186 + // FAQ / HowTo blocks live in post_content, not in any meta table, so
187 + // they are invisible to every count above. Without this type they were
188 + // never migrated at all and simply broke when Rank Math went away
189 + // (#777).
190 + $block_count = Block_Converter::count_posts();
191 + if ($block_count > 0) {
192 + $types[Block_Converter::TYPE] = $block_count;
193 + }
194 +
186 195 foreach ($this->option_keys as $key) {
187 196 if (get_option($key, null) !== null) {
188 197 $types['settings'] = 1;
189 198 break;
@@ -253,10 +262,10 @@
253 262 'object_id' => $post_id,
254 263 'object_type' => 'post',
255 264 'source_plugin' => $this->plugin_slug,
256 265 'data' => [
257 - 'seo_title' => $this->convert_template_variables($meta['rank_math_title'] ?? '', $post_id),
258 - 'meta_description' => $this->convert_template_variables($meta['rank_math_description'] ?? '', $post_id),
266 + 'seo_title' => $this->convert_post_value($meta['rank_math_title'] ?? '', $post_id),
267 + 'meta_description' => $this->convert_post_value($meta['rank_math_description'] ?? '', $post_id),
259 268 'focus_keyword' => $primary_keyword,
260 269 // Full keyword list; the migrator dedupes, drops empties and
261 270 // caps at the ThinkRank maximum via Focus_Keywords.
262 271 'focus_keywords' => $focus_keywords,
@@ -268,16 +277,22 @@
268 277 'nosnippet' => isset($robots_flags['nosnippet']) ? 1 : 0,
269 278 'max_snippet' => $this->advanced_robot_int($advanced_robots, 'max-snippet'),
270 279 'max_video_preview' => $this->advanced_robot_int($advanced_robots, 'max-video-preview'),
271 280 'max_image_preview' => $this->advanced_robot_string($advanced_robots, 'max-image-preview'),
272 - 'og_title' => $this->convert_template_variables($meta['rank_math_facebook_title'] ?? '', $post_id),
273 - 'og_description' => $this->convert_template_variables($meta['rank_math_facebook_description'] ?? '', $post_id),
281 + 'og_title' => $this->convert_post_value($meta['rank_math_facebook_title'] ?? '', $post_id),
282 + 'og_description' => $this->convert_post_value($meta['rank_math_facebook_description'] ?? '', $post_id),
274 283 'og_image' => $meta['rank_math_facebook_image'] ?? '',
275 - 'twitter_title' => $this->convert_template_variables($meta['rank_math_twitter_title'] ?? '', $post_id),
276 - 'twitter_description' => $this->convert_template_variables($meta['rank_math_twitter_description'] ?? '', $post_id),
284 + 'twitter_title' => $this->convert_post_value($meta['rank_math_twitter_title'] ?? '', $post_id),
285 + 'twitter_description' => $this->convert_post_value($meta['rank_math_twitter_description'] ?? '', $post_id),
277 286 'twitter_image' => $meta['rank_math_twitter_image'] ?? '',
278 287 'primary_category' => (int) ($meta['rank_math_primary_category'] ?? 0),
279 288 'schema_type' => $this->resolve_schema_type($meta),
289 + // WooCommerce product identifier. Rank Math keeps it in its
290 + // own per-post meta; ThinkRank Pro reads the canonical field
291 + // this maps to, so a migrating store keeps the identifier
292 + // Google asks for instead of collecting a "missing
293 + // identifier" warning on every product (#715).
294 + 'product_identifier' => (string) ($meta['rank_math_gtin_code'] ?? ''),
280 295 // Rank Math pillar content maps directly to ThinkRank pillar content.
281 296 'pillar_content' => $this->normalize_pillar_content($meta['rank_math_pillar_content'] ?? ''),
282 297 ],
283 298 'extended' => [
@@ -334,16 +349,16 @@
334 349 'object_id' => $term_id,
335 350 'object_type' => 'term',
336 351 'source_plugin' => $this->plugin_slug,
337 352 'data' => [
338 - 'seo_title' => $this->convert_term_template_variables($meta['rank_math_title'] ?? '', $term_id),
339 - 'meta_description' => $this->convert_term_template_variables($meta['rank_math_description'] ?? '', $term_id),
353 + 'seo_title' => $this->convert_term_value($meta['rank_math_title'] ?? '', $term_id),
354 + 'meta_description' => $this->convert_term_value($meta['rank_math_description'] ?? '', $term_id),
340 355 'focus_keyword' => $primary_keyword,
341 356 'canonical_url' => $meta['rank_math_canonical_url'] ?? '',
342 357 'noindex' => $robots['noindex'],
343 358 'nofollow' => $robots['nofollow'],
344 - 'og_title' => $this->convert_term_template_variables($meta['rank_math_facebook_title'] ?? '', $term_id),
345 - 'og_description' => $this->convert_term_template_variables($meta['rank_math_facebook_description'] ?? '', $term_id),
359 + 'og_title' => $this->convert_term_value($meta['rank_math_facebook_title'] ?? '', $term_id),
360 + 'og_description' => $this->convert_term_value($meta['rank_math_facebook_description'] ?? '', $term_id),
346 361 ],
347 362 'extended' => [
348 363 'og_image' => $meta['rank_math_facebook_image'] ?? '',
349 364 'twitter_title' => $meta['rank_math_twitter_title'] ?? '',
@@ -750,8 +765,40 @@
750 765
751 766 /**
752 767 * {@inheritDoc}
753 768 *
769 + * `content_blocks` records carry post ids only. The blocks themselves stay
770 + * in `post_content` and are rewritten in place at migrate time — copying
771 + * the content into the snapshot would mean holding a second copy of every
772 + * FAQ page in `wp_options`, which is where snapshots live.
773 + */
774 + protected function export_custom_type_page(string $type, int $page): array {
775 + if (Block_Converter::TYPE !== $type) {
776 + return parent::export_custom_type_page($type, $page);
777 + }
778 +
779 + $post_ids = Block_Converter::get_post_ids($page, $this->chunk_size);
780 +
781 + // export_chunk() decides whether another page follows from the number
782 + // of rows the query returned, so report it even when nothing is emitted.
783 + $this->last_page_row_count = count($post_ids);
784 +
785 + $records = [];
786 + foreach ($post_ids as $post_id) {
787 + $records[] = [
788 + 'object_id' => $post_id,
789 + 'object_type' => 'post',
790 + 'source_plugin' => $this->plugin_slug,
791 + 'data' => ['post_id' => $post_id],
792 + ];
793 + }
794 +
795 + return $records;
796 + }
797 +
798 + /**
799 + * {@inheritDoc}
800 + *
754 801 * Rank Math's 404 monitor keeps one row per URI in `rank_math_404_logs`,
755 802 * which lines up with ThinkRank Pro's `thinkrank_404_logs`.
756 803 */
757 804 protected function export_404_logs_page(int $page): array {
@@ -949,8 +996,68 @@
949 996 return self::MODERN_SCHEMA_TYPE_MAP[$key] ?? '';
950 997 }
951 998
952 999 /**
1000 + * Rank Math per-post tokens ThinkRank resolves per request (#886): kept
1001 + * as tags rather than frozen into text. Same vocabulary as
1002 + * convert_template_tokens().
1003 + */
1004 + private const POST_TOKENS = [
1005 + '%title%' => '%title%',
1006 + '%sitename%' => '%sitename%',
1007 + '%sep%' => '%sep%',
1008 + '%excerpt%' => '%excerpt%',
1009 + '%date%' => '%date%',
1010 + '%modified%' => '%modified%',
1011 + '%name%' => '%author%',
1012 + '%category%' => '%category%',
1013 + '%wc_price%' => '%price%',
1014 + '%wc_sku%' => '%sku%',
1015 + '%wc_shortdesc%' => '%short_description%',
1016 + '%wc_brand%' => '%brand%',
1017 + ];
1018 +
1019 + /**
1020 + * Rank Math per-term tokens ThinkRank resolves on a term archive.
1021 + */
1022 + private const TERM_TOKENS = [
1023 + '%term%' => '%term%',
1024 + '%term_description%' => '%excerpt%',
1025 + '%sitename%' => '%sitename%',
1026 + '%sep%' => '%sep%',
1027 + ];
1028 +
1029 + /**
1030 + * A per-post Rank Math value: mapped tokens as ThinkRank tags, the rest literal.
1031 + *
1032 + * @param mixed $value Raw Rank Math value
1033 + * @param int $post_id Post ID
1034 + * @return string
1035 + */
1036 + private function convert_post_value($value, int $post_id): string {
1037 + return $this->tokenize_object_template(
1038 + $this->stringify_template_value($value),
1039 + self::literal_token_patterns(self::POST_TOKENS),
1040 + fn(string $rest): string => $this->convert_template_variables($rest, $post_id)
1041 + );
1042 + }
1043 +
1044 + /**
1045 + * A per-term Rank Math value: mapped tokens as ThinkRank tags, the rest literal.
1046 + *
1047 + * @param mixed $value Raw Rank Math value
1048 + * @param int $term_id Term ID
1049 + * @return string
1050 + */
1051 + private function convert_term_value($value, int $term_id): string {
1052 + return $this->tokenize_object_template(
1053 + $this->stringify_template_value($value),
1054 + self::literal_token_patterns(self::TERM_TOKENS),
1055 + fn(string $rest): string => $this->convert_term_template_variables($rest, $term_id)
1056 + );
1057 + }
1058 +
1059 + /**
953 1060 * Resolve a Rank Math TERM title/description value, replacing the
954 1061 * term-context tokens (%term%, %term_description%) Rank Math uses for term
955 1062 * archives before delegating to the shared variable resolver. Without this
956 1063 * the term name is stripped and titles render as "Archives - Site".
@@ -1002,9 +1109,12 @@
1002 1109 if ($post_id) {
1003 1110 $post = get_post($post_id);
1004 1111 if ($post) {
1005 1112 $replacements['%title%'] = $post->post_title;
1006 - $replacements['%excerpt%'] = wp_trim_words($post->post_excerpt ?: wp_trim_words(wp_strip_all_tags($post->post_content), 55), 55);
1113 + $replacements['%excerpt%'] = \ThinkRank\Core\Seo_Text::trim_words(
1114 + $post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words(wp_strip_all_tags($post->post_content), 55),
1115 + 55
1116 + );
1007 1117 $replacements['%date%'] = get_the_date('', $post);
1008 1118 $replacements['%modified%'] = get_the_modified_date('', $post);
1009 1119 $replacements['%id%'] = (string) $post_id;
1010 1120 $replacements['%name%'] = get_the_author_meta('display_name', (int) $post->post_author);
@@ -1057,13 +1167,25 @@
1057 1167
1058 1168 // Rank Math token => ThinkRank Global SEO token (structure preserved).
1059 1169 $token_map = [
1060 1170 '%name%' => '%author%', // Rank Math author display name token
1171 + // WooCommerce product variables. Rank Math lets a product title or
1172 + // description carry the price, SKU and stock status; every one of
1173 + // them used to be stripped by the pass below, so "Buy %title% for
1174 + // %wc_price%" imported as "Buy %title% for" (#715).
1175 + '%wc_price%' => '%price%',
1176 + '%wc_sku%' => '%sku%',
1177 + '%wc_shortdesc%' => '%short_description%',
1178 + '%wc_brand%' => '%brand%',
1061 1179 ];
1062 1180 $template = str_replace(array_keys($token_map), array_values($token_map), $template);
1063 1181
1064 1182 // Tokens ThinkRank's Global SEO engine resolves natively — keep as-is.
1065 - $supported = ['%title%', '%sitename%', '%sep%', '%excerpt%', '%date%', '%modified%', '%author%', '%category%'];
1183 + $supported = [
1184 + '%title%', '%sitename%', '%sep%', '%excerpt%', '%date%', '%modified%', '%author%', '%category%',
1185 + // Resolved by Pattern_Resolver on a product, empty elsewhere.
1186 + '%price%', '%sale_price%', '%sku%', '%stock_status%', '%short_description%', '%brand%',
1187 + ];
1066 1188
1067 1189 // Strip any token ThinkRank cannot resolve so it does not render literally.
1068 1190 $template = preg_replace_callback(
1069 1191 '/%[a-z0-9_]+%/i',