PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/seo/class-schema-builder.php +50 -14 2.3.0 → 2.10.0 View file →
@@ -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 `&amp;`.
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 "".