PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.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 trunk All 53 releases
← All changes | includes/seo/class-schema-builder.php +47 -20 2.7.0 → 2.12.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.
@@ -827,17 +839,18 @@
827 839 '@type' => 'ImageObject',
828 840 'url' => $image_url
829 841 ];
830 842
831 - // Try to get image dimensions if it's a WordPress attachment
832 - $attachment_id = attachment_url_to_postid($image_url);
843 + // Try to get image dimensions if it's a WordPress attachment — those
844 + // of the file this URL names, which need not be the original upload.
845 + $attachment_id = Attachment_Lookup::id_from_url($image_url);
833 846 if ($attachment_id) {
834 - $image_data = wp_get_attachment_image_src($attachment_id, 'full');
847 + $image_file = Attachment_Lookup::describe($attachment_id, $image_url);
835 848 // SVGs report 0x0 — omit the dimensions rather than emitting
836 849 // zeroes, which invalidate the ImageObject.
837 - if ($image_data && (int) $image_data[1] > 0 && (int) $image_data[2] > 0) {
838 - $image_schema['width'] = (int) $image_data[1];
839 - $image_schema['height'] = (int) $image_data[2];
850 + if ($image_file['width'] > 0 && $image_file['height'] > 0) {
851 + $image_schema['width'] = $image_file['width'];
852 + $image_schema['height'] = $image_file['height'];
840 853 }
841 854 }
842 855
843 856 return $image_schema;
@@ -1055,9 +1068,13 @@
1055 1068 $schema['description'] = $this->truncate_text($data['site_data']['website_description'], 160);
1056 1069 } elseif (!empty($data['content'])) {
1057 1070 $schema['description'] = $this->truncate_text($data['content'], 160);
1058 1071 } else {
1059 - $schema['description'] = get_bloginfo('description');
1072 + // The tagline is stored esc_html()'d by sanitize_option(), so an
1073 + // ampersand in it arrives as `&amp;`.
1074 + $schema['description'] = \ThinkRank\Core\Seo_Text::normalize_schema_text(
1075 + (string) get_bloginfo('description')
1076 + );
1060 1077 }
1061 1078
1062 1079 // Author - use organization or person data
1063 1080 if (!empty($data['site_data']['organization_name'])) {
@@ -1289,8 +1306,18 @@
1289 1306 */
1290 1307 private function populate_local_business_schema(array $schema, array $data, string $context): array {
1291 1308 // Get business data from Site Identity Business Info (single source of truth)
1292 1309 $business_data = $this->get_business_data_from_site_identity();
1310 +
1311 + // The business type chosen in Local SEO is what this node IS. It was
1312 + // read into $business_data and then never used, so every site published
1313 + // "@type": "LocalBusiness" whatever it had picked, and the ~150-type
1314 + // selector (#623) changed nothing but a meta tag. create_base_schema()
1315 + // has already put @type in place, so reassigning keeps key order and a
1316 + // site on the default publishes exactly what it did before.
1317 + $schema['@type'] = \ThinkRank\Config\Local_Business_Types_Config::schema_type(
1318 + $business_data['business_type'] ?? ''
1319 + );
1293 1320
1294 1321 // Required properties - use business name from Business Info.
1295 1322 // `name` is required for LocalBusiness, so an empty saved value must fall
1296 1323 // through to the next source rather than emit "".