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-management-system.php +79 -5 2.3.0 → 2.10.0 View file →
@@ -201,13 +201,43 @@
201 201 'WebPage' => [
202 202 'name' => 'WebPage',
203 203 'description' => 'Individual web pages',
204 204 'required_properties' => ['name', 'url'],
205 + // 'post' as well as 'page': the per-page selector reaches this for
206 + // any post type, and a registry limited to 'page' silently produced
207 + // nothing for the rest (#624).
205 208 'recommended_properties' => ['description', 'author', 'datePublished', 'breadcrumb'],
206 209 'rich_snippets' => ['webpage', 'breadcrumb'],
207 - 'context_types' => ['page'],
210 + 'context_types' => ['page', 'post'],
208 211 'priority' => 'medium'
209 212 ],
213 + 'AboutPage' => [
214 + 'name' => 'AboutPage',
215 + 'description' => 'A page describing the organisation or person behind the site',
216 + 'required_properties' => ['name', 'url'],
217 + 'recommended_properties' => ['description', 'author', 'datePublished', 'breadcrumb'],
218 + 'rich_snippets' => ['webpage', 'breadcrumb'],
219 + 'context_types' => ['page', 'post'],
220 + 'priority' => 'medium'
221 + ],
222 + 'ContactPage' => [
223 + 'name' => 'ContactPage',
224 + 'description' => 'A page giving contact details',
225 + 'required_properties' => ['name', 'url'],
226 + 'recommended_properties' => ['description', 'author', 'datePublished', 'breadcrumb'],
227 + 'rich_snippets' => ['webpage', 'breadcrumb'],
228 + 'context_types' => ['page', 'post'],
229 + 'priority' => 'medium'
230 + ],
231 + 'ProfilePage' => [
232 + 'name' => 'ProfilePage',
233 + 'description' => 'A page about a single person or organisation',
234 + 'required_properties' => ['name', 'url'],
235 + 'recommended_properties' => ['description', 'author', 'datePublished', 'breadcrumb'],
236 + 'rich_snippets' => ['webpage', 'breadcrumb'],
237 + 'context_types' => ['page', 'post'],
238 + 'priority' => 'medium'
239 + ],
210 240 'FAQPage' => [
211 241 'name' => 'FAQPage',
212 242 'description' => 'Frequently Asked Questions pages',
213 243 'required_properties' => ['mainEntity'],
@@ -397,9 +427,25 @@
397 427 'business_phone', 'business_email', 'business_hours',
398 428 'business_latitude', 'business_longitude', 'business_price_range',
399 429 ];
400 430
401 - if (empty(array_intersect_key($settings, array_flip($business_keys)))) {
431 + // Which deployed types a Site Identity key can invalidate. The business
432 + // block feeds LocalBusiness and Organization; alternate_name feeds the
433 + // WebSite node, which had no entry here at all — so editing it left the
434 + // deployed schema showing the previous value until something else
435 + // happened to redeploy (#692).
436 + $refresh_types = [];
437 +
438 + if (!empty(array_intersect_key($settings, array_flip($business_keys)))) {
439 + $refresh_types[] = 'LocalBusiness';
440 + $refresh_types[] = 'Organization';
441 + }
442 +
443 + if (array_key_exists('alternate_name', $settings)) {
444 + $refresh_types[] = 'WebSite';
445 + }
446 +
447 + if (empty($refresh_types)) {
402 448 return;
403 449 }
404 450
405 451 $schema_settings = $this->get_settings($context_type, $context_id);
@@ -409,9 +455,9 @@
409 455
410 456 // Only refresh types that are actually deployed, so this never adds a
411 457 // type the admin did not enable.
412 458 $deployed = array_keys((array) $this->get_deployed_schemas($context_type, $context_id));
413 - $affected = array_values(array_intersect($deployed, ['LocalBusiness', 'Organization']));
459 + $affected = array_values(array_intersect($deployed, $refresh_types));
414 460
415 461 if (empty($affected)) {
416 462 return;
417 463 }
@@ -1586,9 +1632,9 @@
1586 1632 if ($post) {
1587 1633 $content_data = [
1588 1634 'title' => $post->post_title,
1589 1635 'url' => get_permalink($post->ID),
1590 - 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 30),
1636 + 'excerpt' => $post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words($post->post_content, 30),
1591 1637 'content' => $post->post_content,
1592 1638 'author' => [
1593 1639 'name' => get_the_author_meta('display_name', $post->post_author),
1594 1640 'url' => get_author_posts_url($post->post_author)
@@ -2242,9 +2288,12 @@
2242 2288 }
2243 2289
2244 2290 $type = $schema['@type'] ?? '';
2245 2291 $type = is_array($type) ? reset($type) : $type;
2246 - $is_entity = in_array((string) $type, self::ENTITY_URL_TYPES, true);
2292 + // A LocalBusiness is deployed under the subtype the site chose, so the
2293 + // exemption has to cover every subtype, not only the literal root.
2294 + $is_entity = in_array((string) $type, self::ENTITY_URL_TYPES, true)
2295 + || \ThinkRank\Config\Local_Business_Types_Config::is_local_business($type);
2247 2296
2248 2297 if (isset($schema['url']) && !$is_entity) {
2249 2298 $schema['url'] = $permalink;
2250 2299 }
@@ -2272,9 +2321,15 @@
2272 2321 * Covers non-ISO-8601 dates (#465) and WP locales in inLanguage, which must
2273 2322 * be a BCP-47 tag — en-US, not en_US (#473). Walks nested nodes so values
2274 2323 * inside author/publisher/@graph entries are covered too.
2275 2324 *
2325 + * Also decodes HTML entities in plain-text properties. Schema_Builder
2326 + * stored the block editor's `&` as-is until 2.10.0, and nothing
2327 + * decodes JSON-LD downstream, so every deployed node built from post text
2328 + * published the entity literally.
2329 + *
2276 2330 * @since 1.16.0
2331 + * @since 2.10.0 Decodes entities in plain-text properties.
2277 2332 *
2278 2333 * @param array $schema Decoded schema data.
2279 2334 * @return array Normalised schema.
2280 2335 */
@@ -2283,8 +2338,16 @@
2283 2338 'datePublished', 'dateModified', 'dateCreated', 'uploadDate',
2284 2339 'startDate', 'endDate', 'validFrom', 'validThrough', 'expires',
2285 2340 ];
2286 2341
2342 + // Plain text in schema.org. Answer/HowToStep `text` is deliberately
2343 + // absent: Google reads Answer.text as HTML, where an entity is correct
2344 + // and decoding `<` would turn escaped text into live markup.
2345 + static $text_keys = [
2346 + 'name', 'headline', 'alternativeHeadline', 'description',
2347 + 'reviewBody', 'about', 'abstract', 'caption',
2348 + ];
2349 +
2287 2350 foreach ($schema as $key => $value) {
2288 2351 if (is_array($value)) {
2289 2352 $schema[$key] = $this->normalize_stored_schema($value);
2290 2353 continue;
@@ -2294,8 +2357,16 @@
2294 2357 $schema[$key] = str_replace('_', '-', $value);
2295 2358 continue;
2296 2359 }
2297 2360
2361 + // Decode only: a snapshot already truncated with an ellipsis must
2362 + // keep it, which the full Seo_Text::normalize_schema_text() would
2363 + // strip as an excerpt marker.
2364 + if (in_array($key, $text_keys, true) && is_string($value) && '' !== $value) {
2365 + $schema[$key] = \ThinkRank\Core\Seo_Text::decode_schema_entities($value);
2366 + continue;
2367 + }
2368 +
2298 2369 if (!in_array($key, $date_keys, true) || !is_string($value) || '' === $value) {
2299 2370 continue;
2300 2371 }
2301 2372
@@ -2598,8 +2669,11 @@
2598 2669 'site_url' => home_url(),
2599 2670 'admin_email' => get_option('admin_email'),
2600 2671 'language' => get_locale(),
2601 2672 'timezone' => get_option('timezone_string'),
2673 + // Read by populate_website_schema(), so the deployed WebSite node
2674 + // carries the same alternateName as the default one (#692).
2675 + 'alternate_name' => $site_identity_settings['alternate_name'] ?? '',
2602 2676 'founded_date' => $site_identity_settings['founded_date'] ?? '',
2603 2677 'founder_name' => $site_identity_settings['founder_name'] ?? '',
2604 2678 'company_type' => $site_identity_settings['company_type'] ?? 'Organization',
2605 2679 // Site Identity assets