| @@ -155,8 +155,9 @@ | ||
| 155 | 155 | 'twitter_title' => $this->convert_template_variables($meta['_yoast_wpseo_twitter-title'] ?? '', $post_id), |
| 156 | 156 | 'twitter_description' => $this->convert_template_variables($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 | ], |
| @@ -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); |
| @@ -546,8 +550,15 @@ | ||
| 546 | 550 | '%%modified%%' => '%modified%', |
| 547 | 551 | '%%name%%' => '%author%', |
| 548 | 552 | '%%category%%' => '%category%', |
| 549 | 553 | '%%primary_category%%' => '%category%', |
| 554 | + // WooCommerce SEO product variables. Stripped rather than mapped | |
| 555 | + // until #715, so a product description template imported as "Buy | |
| 556 | + // %title% for" with the price and stock gone. | |
| 557 | + '%%wc_price%%' => '%price%', | |
| 558 | + '%%wc_sku%%' => '%sku%', | |
| 559 | + '%%wc_shortdesc%%' => '%short_description%', | |
| 560 | + '%%wc_brand%%' => '%brand%', | |
| 550 | 561 | ]; |
| 551 | 562 | |
| 552 | 563 | $template = str_replace(array_keys($map), array_values($map), $template); |
| 553 | 564 | |
| @@ -785,8 +796,22 @@ | ||
| 785 | 796 | // Math tokens wrapped in Yoast's double-percent syntax — tokens Yoast |
| 786 | 797 | // itself cannot resolve either. Map them rather than stripping them, |
| 787 | 798 | // or the format silently loses its search term / term name. |
| 788 | 799 | '%%search_query%%' => '%search_term%', |
| 800 | + // NOT the WooCommerce product variables, deliberately. This | |
| 801 | + // converter writes Site Identity title formats, and those are | |
| 802 | + // resolved by SEO_Manager::process_title_template() against its own | |
| 803 | + // vocabulary — %site_title%, %separator%, %search_term% and the | |
| 804 | + // rest — which has no %price%/%sku%/%brand% and, unlike | |
| 805 | + // Pattern_Resolver, does not strip what it cannot resolve. Mapping | |
| 806 | + // them here published "Search pennant - %price% - Media Wipe" in a | |
| 807 | + // live <title>: a silent loss traded for the literal leak #715 is | |
| 808 | + // about. The formats this feeds are the homepage, search, archive, | |
| 809 | + // author, category and tag titles, where there is no one product | |
| 810 | + // for a price to belong to either way, so the strip below is the | |
| 811 | + // right answer. The per-post converter keeps them, because | |
| 812 | + // Pattern_Resolver does resolve them: see | |
| 813 | + // convert_template_pattern(). | |
| 789 | 814 | ]; |
| 790 | 815 | if ($context_token !== '') { |
| 791 | 816 | $map['%%title%%'] = $context_token; |
| 792 | 817 | $map['%%term_title%%'] = $context_token; |
| @@ -989,6 +1014,44 @@ | ||
| 989 | 1014 | } |
| 990 | 1015 | } |
| 991 | 1016 | |
| 992 | 1017 | return $settings; |
| 1018 | + } | |
| 1019 | + | |
| 1020 | + /** | |
| 1021 | + * A product's identifier from Yoast WooCommerce SEO. | |
| 1022 | + * | |
| 1023 | + * Yoast stores every identifier type in one serialised map keyed by type | |
| 1024 | + * (`gtin8`, `gtin12`, `gtin13`, `gtin14`, `isbn`, `mpn`) rather than in a | |
| 1025 | + * field per type. ThinkRank Pro holds a single identifier, so the first | |
| 1026 | + * non-empty one wins, in the order Google prefers: a GTIN identifies the | |
| 1027 | + * product itself, an MPN only identifies it within one manufacturer, and | |
| 1028 | + * an ISBN applies to books alone. | |
| 1029 | + * | |
| 1030 | + * @since 2.10.1 | |
| 1031 | + * | |
| 1032 | + * @param array $meta Post meta for the product. | |
| 1033 | + * @return string Identifier, or '' when the product has none. | |
| 1034 | + */ | |
| 1035 | + private function extract_product_identifier(array $meta): string { | |
| 1036 | + $raw = $meta['wpseo_global_identifier_values'] ?? ''; | |
| 1037 | + | |
| 1038 | + if (is_string($raw) && '' !== $raw) { | |
| 1039 | + $decoded = maybe_unserialize($raw); | |
| 1040 | + $raw = is_array($decoded) ? $decoded : []; | |
| 1041 | + } | |
| 1042 | + | |
| 1043 | + if (!is_array($raw) || empty($raw)) { | |
| 1044 | + return ''; | |
| 1045 | + } | |
| 1046 | + | |
| 1047 | + foreach (['gtin13', 'gtin14', 'gtin12', 'gtin8', 'mpn', 'isbn'] as $type) { | |
| 1048 | + $value = trim((string) ($raw[$type] ?? '')); | |
| 1049 | + | |
| 1050 | + if ('' !== $value) { | |
| 1051 | + return $value; | |
| 1052 | + } | |
| 1053 | + } | |
| 1054 | + | |
| 1055 | + return ''; | |
| 993 | 1056 | } |
| 994 | 1057 | } |