| @@ -91,8 +91,14 @@ | ||
| 91 | 91 | case 'WebSite': |
| 92 | 92 | $schema = $this->populate_website_schema($schema, $data, $context); |
| 93 | 93 | break; |
| 94 | 94 | case 'WebPage': |
| 95 | + case 'AboutPage': | |
| 96 | + case 'ContactPage': | |
| 97 | + case 'ProfilePage': | |
| 98 | + // All four carry the same WebPage properties; only @type | |
| 99 | + // differs, and create_base_schema() has already set it from the | |
| 100 | + // requested type (#624). | |
| 95 | 101 | $schema = $this->populate_webpage_schema($schema, $data, $context); |
| 96 | 102 | break; |
| 97 | 103 | case 'FAQPage': |
| 98 | 104 | $schema = $this->populate_faq_schema($schema, $data, $context); |
| @@ -722,12 +728,25 @@ | ||
| 722 | 728 | return $schema; |
| 723 | 729 | } |
| 724 | 730 | |
| 725 | 731 | /** |
| 726 | - * Truncate text to specified length | |
| 727 | - * PRESERVED: Exact same method logic from original Schema_Generator | |
| 732 | + * Normalise text for JSON-LD and truncate it to a length. | |
| 728 | 733 | * |
| 734 | + * Every description, headline and reviewBody this class emits passes | |
| 735 | + * through here, so this is the one place the deployed nodes get the same | |
| 736 | + * treatment the automatic ones have had since #766. Before, it only | |
| 737 | + * stripped tags: the block editor stores `&` as `&`, which was then | |
| 738 | + * published literally (nothing decodes JSON-LD), and the cut landed | |
| 739 | + * mid-word with "..." glued on. A deployed node outranks the automatic | |
| 740 | + * one, so deploying a page type made its description worse than not | |
| 741 | + * deploying anything. | |
| 742 | + * | |
| 743 | + * Seo_Text::trim_to_length() is multibyte-aware, which the #473 fix here | |
| 744 | + * already required (a byte cut mojibaked every non-Latin description), | |
| 745 | + * and falls back to a character cut for scripts without spaces. | |
| 746 | + * | |
| 729 | 747 | * @since 1.0.0 |
| 748 | + * @since 2.10.0 Decodes entities and cuts on a word boundary. | |
| 730 | 749 | * |
| 731 | 750 | * @param string $text Text to truncate |
| 732 | 751 | * @param int $length Maximum length |
| 733 | 752 | * @return string Truncated text |
| @@ -732,19 +751,12 @@ | ||
| 732 | 751 | * @param int $length Maximum length |
| 733 | 752 | * @return string Truncated text |
| 734 | 753 | */ |
| 735 | 754 | private function truncate_text(string $text, int $length): string { |
| 736 | - $text = wp_strip_all_tags($text); | |
| 737 | - | |
| 738 | - // Multibyte-aware. strlen()/substr() count bytes, so a cut landing | |
| 739 | - // mid-character produced invalid UTF-8 — wp_json_encode()'s sanity | |
| 740 | - // check then replaced the tail with "?", mojibaking every non-Latin | |
| 741 | - // site's description and headline (#473). | |
| 742 | - if (mb_strlen($text) <= $length) { | |
| 743 | - return $text; | |
| 744 | - } | |
| 745 | - | |
| 746 | - return mb_substr($text, 0, max(0, $length - 3)) . '...'; | |
| 755 | + return \ThinkRank\Core\Seo_Text::trim_to_length( | |
| 756 | + \ThinkRank\Core\Seo_Text::normalize_schema_text($text), | |
| 757 | + $length | |
| 758 | + ); | |
| 747 | 759 | } |
| 748 | 760 | |
| 749 | 761 | /** |
| 750 | 762 | * Normalise a date into ISO 8601 with a timezone offset. |
| @@ -1039,8 +1051,18 @@ | ||
| 1039 | 1051 | $data['url'] ?? '', |
| 1040 | 1052 | home_url() |
| 1041 | 1053 | ); |
| 1042 | 1054 | |
| 1055 | + // Both WebSite producers have to carry this or the deployed node and the | |
| 1056 | + // default one disagree about the same site — the shape of failure #688 | |
| 1057 | + // documents. The default node is generate_website_schema() (#692). | |
| 1058 | + $alternate_name = \ThinkRank\SEO\Site_Identity_Manager::alternate_name_for_schema( | |
| 1059 | + $data['site_data']['alternate_name'] ?? null | |
| 1060 | + ); | |
| 1061 | + if (null !== $alternate_name) { | |
| 1062 | + $schema['alternateName'] = $alternate_name; | |
| 1063 | + } | |
| 1064 | + | |
| 1043 | 1065 | // Recommended properties - prioritize user-configured Website schema description |
| 1044 | 1066 | if (!empty($data['site_data']['website_description'])) { |
| 1045 | 1067 | $schema['description'] = $this->truncate_text($data['site_data']['website_description'], 160); |
| 1046 | 1068 | } elseif (!empty($data['content'])) { |
| @@ -1045,9 +1067,13 @@ | ||
| 1045 | 1067 | $schema['description'] = $this->truncate_text($data['site_data']['website_description'], 160); |
| 1046 | 1068 | } elseif (!empty($data['content'])) { |
| 1047 | 1069 | $schema['description'] = $this->truncate_text($data['content'], 160); |
| 1048 | 1070 | } else { |
| 1049 | - $schema['description'] = get_bloginfo('description'); | |
| 1071 | + // The tagline is stored esc_html()'d by sanitize_option(), so an | |
| 1072 | + // ampersand in it arrives as `&`. | |
| 1073 | + $schema['description'] = \ThinkRank\Core\Seo_Text::normalize_schema_text( | |
| 1074 | + (string) get_bloginfo('description') | |
| 1075 | + ); | |
| 1050 | 1076 | } |
| 1051 | 1077 | |
| 1052 | 1078 | // Author - use organization or person data |
| 1053 | 1079 | if (!empty($data['site_data']['organization_name'])) { |
| @@ -1279,8 +1305,18 @@ | ||
| 1279 | 1305 | */ |
| 1280 | 1306 | private function populate_local_business_schema(array $schema, array $data, string $context): array { |
| 1281 | 1307 | // Get business data from Site Identity Business Info (single source of truth) |
| 1282 | 1308 | $business_data = $this->get_business_data_from_site_identity(); |
| 1309 | + | |
| 1310 | + // The business type chosen in Local SEO is what this node IS. It was | |
| 1311 | + // read into $business_data and then never used, so every site published | |
| 1312 | + // "@type": "LocalBusiness" whatever it had picked, and the ~150-type | |
| 1313 | + // selector (#623) changed nothing but a meta tag. create_base_schema() | |
| 1314 | + // has already put @type in place, so reassigning keeps key order and a | |
| 1315 | + // site on the default publishes exactly what it did before. | |
| 1316 | + $schema['@type'] = \ThinkRank\Config\Local_Business_Types_Config::schema_type( | |
| 1317 | + $business_data['business_type'] ?? '' | |
| 1318 | + ); | |
| 1283 | 1319 | |
| 1284 | 1320 | // Required properties - use business name from Business Info. |
| 1285 | 1321 | // `name` is required for LocalBusiness, so an empty saved value must fall |
| 1286 | 1322 | // through to the next source rather than emit "". |