← 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 | } |