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/frontend/class-schema-graph.php +47 -9 2.8.0 → 2.10.0 View file →
@@ -641,11 +641,24 @@
641 641 if (!is_array($block)) {
642 642 continue;
643 643 }
644 644
645 - if (($block['blockName'] ?? '') === self::FAQ_BLOCK) {
646 - $attrs = $block['attrs'] ?? [];
645 + $block_name = (string) ($block['blockName'] ?? '');
646 + $attrs = is_array($block['attrs'] ?? null) ? $block['attrs'] : [];
647 647
648 + // A leftover Rank Math FAQ block is absorbed as if it were ours, so
649 + // an unmigrated post contributes its questions to the single
650 + // FAQPage rather than to nothing at all (#777). The fallback stays
651 + // silent while Rank Math is active and still emitting its own.
652 + if (\ThinkRank\Integrations\Rank_Math_Blocks::is_source_block($block_name)) {
653 + $fallback = \ThinkRank\Integrations\Rank_Math_Blocks::schema_fallback($block_name, $attrs);
654 + if (null !== $fallback) {
655 + $block_name = $fallback['name'];
656 + $attrs = $fallback['attrs'];
657 + }
658 + }
659 +
660 + if ($block_name === self::FAQ_BLOCK) {
648 661 // Mirrors Blocks_Manager: schema is on unless explicitly disabled.
649 662 $disabled = array_key_exists('outputSchema', $attrs) && false === $attrs['outputSchema'];
650 663
651 664 if (!$disabled) {
@@ -993,9 +1006,9 @@
993 1006 return;
994 1007 }
995 1008
996 1009 echo "<!-- ThinkRank Schema Graph -->\n";
997 - echo '<script type="application/ld+json">' . "\n";
1010 + echo '<script type="application/ld+json" data-thinkrank="schema-graph">' . "\n";
998 1011 echo $json . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode with JSON_HEX_* cannot break out of the script block.
999 1012 echo '</script>' . "\n";
1000 1013 echo "<!-- /ThinkRank Schema Graph -->\n";
1001 1014 }
@@ -1163,8 +1176,9 @@
1163 1176 $person_nodes = [];
1164 1177
1165 1178 foreach ($this->supporting as $index => $node) {
1166 1179 $type = $node['@type'] ?? '';
1180 + $slot = $this->site_level_slot($type);
1167 1181
1168 1182 if ('BreadcrumbList' === $type) {
1169 1183 $node = $this->assign_id($node, $base . '#breadcrumb', $used_ids);
1170 1184 $breadcrumb_id = $node['@id'];
@@ -1173,9 +1187,9 @@
1173 1187 $website_id = $node['@id'];
1174 1188 } elseif ('Organization' === $type) {
1175 1189 $node = $this->assign_id($node, home_url('/#organization'), $used_ids);
1176 1190 $organization_nodes[] = $node;
1177 - } elseif (in_array($type, self::SITE_LEVEL_TYPES, true)) {
1191 + } elseif ('' !== $slot) {
1178 1192 // Site-level entities describe the site, not the page, so their
1179 1193 // @id must be stable across URLs. Falling through to the
1180 1194 // page-scoped branch minted a fresh identity on every URL, so
1181 1195 // one business became N entities in a crawler's graph and
@@ -1185,9 +1199,9 @@
1185 1199 // arrive here claiming the same @id. assign_id() would resolve
1186 1200 // that collision by minting "#person-2", turning a duplicate
1187 1201 // into two competing entities that split the identity a
1188 1202 // knowledge graph is meant to consolidate (#479).
1189 - $duplicate_key = $this->find_same_entity($nodes, $type, $node);
1203 + $duplicate_key = $this->find_same_entity($nodes, $slot, $node);
1190 1204
1191 1205 if (null !== $duplicate_key) {
1192 1206 $nodes[$duplicate_key] = $this->merge_entity($nodes[$duplicate_key], $node);
1193 1207 continue;
@@ -1192,11 +1206,11 @@
1192 1206 $nodes[$duplicate_key] = $this->merge_entity($nodes[$duplicate_key], $node);
1193 1207 continue;
1194 1208 }
1195 1209
1196 - $node = $this->assign_id($node, home_url('/#' . strtolower($type)), $used_ids);
1210 + $node = $this->assign_id($node, home_url('/#' . strtolower($slot)), $used_ids);
1197 1211
1198 - if ('Person' === $type) {
1212 + if ('Person' === $slot) {
1199 1213 $person_nodes[] = $node;
1200 1214 }
1201 1215 } elseif (is_string($type) && $type !== '') {
1202 1216 $node = $this->assign_id($node, $base . '#' . strtolower($type), $used_ids);
@@ -1289,8 +1303,32 @@
1289 1303 return ['winner' => array_shift($kept), 'siblings' => array_values($kept)];
1290 1304 }
1291 1305
1292 1306 /**
1307 + * The site-level entity a node's @type makes it, or '' for none.
1308 + *
1309 + * A LocalBusiness is published under the subtype the site chose in Local
1310 + * SEO ("Dentist", "Restaurant"), but it is still the one business entity,
1311 + * so every subtype shares the LocalBusiness slot: the same home-scoped
1312 + * `/#localbusiness` @id it always had, and the same entity for
1313 + * find_same_entity() to fold a per-post copy into. Matching the literal
1314 + * type instead would have given a Dentist a fresh page-scoped @id on every
1315 + * URL, the exact split #471 closed.
1316 + *
1317 + * @since 2.10.0
1318 + *
1319 + * @param mixed $type Node @type.
1320 + * @return string 'LocalBusiness', 'Person' or ''.
1321 + */
1322 + private function site_level_slot($type): string {
1323 + if (\ThinkRank\Config\Local_Business_Types_Config::is_local_business($type)) {
1324 + return 'LocalBusiness';
1325 + }
1326 +
1327 + return (is_string($type) && in_array($type, self::SITE_LEVEL_TYPES, true)) ? $type : '';
1328 + }
1329 +
1330 + /**
1293 1331 * Find an already-placed node describing the same entity as $node.
1294 1332 *
1295 1333 * Identity is `email` when both carry one — two people can share a name,
1296 1334 * but not a mailbox — and a case-insensitive `name` match otherwise. A node
@@ -1299,9 +1337,9 @@
1299 1337 *
1300 1338 * @since 2.0.2
1301 1339 *
1302 1340 * @param array $nodes Nodes placed so far, keyed.
1303 - * @param string $type Schema type to match within.
1341 + * @param string $type Site-level slot to match within (see site_level_slot()).
1304 1342 * @param array $node Candidate node.
1305 1343 * @return string|null Key of the matching node, or null.
1306 1344 */
1307 1345 private function find_same_entity(array $nodes, string $type, array $node): ?string {
@@ -1312,9 +1350,9 @@
1312 1350 return null;
1313 1351 }
1314 1352
1315 1353 foreach ($nodes as $key => $placed) {
1316 - if (($placed['@type'] ?? '') !== $type) {
1354 + if ($this->site_level_slot($placed['@type'] ?? '') !== $type) {
1317 1355 continue;
1318 1356 }
1319 1357
1320 1358 $placed_email = isset($placed['email']) ? strtolower(trim((string) $placed['email'])) : '';