PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.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-global-seo-schema-output.php +86 -9 2.1.0 → 2.9.0 View file →
@@ -104,8 +104,22 @@
104 104 }
105 105
106 106 $settings = $this->get_global_seo_settings($post_type);
107 107 if (($settings['schema_type'] ?? '') === 'Product') {
108 + // ...but only if this class is actually going to emit it. The
109 + // per-content-type Schema switch (#660) makes
110 + // output_global_seo_schema() return before it builds anything, so
111 + // claiming the entity here as well left the page with NO product
112 + // structured data at all — strictly worse than the duplicate this
113 + // method exists to prevent, and the opposite of what the docblock
114 + // above promises for "ThinkRank's product schema disabled".
115 + if (!\ThinkRank\SEO\Content_Type_Settings::is_enabled_for_current(
116 + \ThinkRank\SEO\Content_Type_Settings::FEATURE_SCHEMA,
117 + true
118 + )) {
119 + return $markup;
120 + }
121 +
108 122 return [];
109 123 }
110 124
111 125 // A per-post DEPLOYED Product schema duplicates WooCommerce's markup
@@ -153,8 +167,17 @@
153 167 * @since 1.0.0
154 168 * @return void
155 169 */
156 170 public function output_global_seo_schema(): void {
171 + // Per-content-type schema switch. 'inherit' (the default) keeps schema
172 + // on, exactly as before the matrix existed (#660).
173 + if (!\ThinkRank\SEO\Content_Type_Settings::is_enabled_for_current(
174 + \ThinkRank\SEO\Content_Type_Settings::FEATURE_SCHEMA,
175 + true
176 + )) {
177 + return;
178 + }
179 +
157 180 // Archives get a CollectionPage schema instead of the per-post-type one
158 181 if (!is_singular()) {
159 182 $this->output_archive_schema();
160 183 return;
@@ -327,8 +350,16 @@
327 350 * @param string $post_type Post type slug.
328 351 * @return bool True when structured data would be output for this post type.
329 352 */
330 353 public function would_output_schema(string $post_type): bool {
354 + if (!\ThinkRank\SEO\Content_Type_Settings::is_enabled(
355 + \ThinkRank\SEO\Content_Type_Settings::FEATURE_SCHEMA,
356 + $post_type,
357 + true
358 + )) {
359 + return false;
360 + }
361 +
331 362 $settings = $this->get_global_seo_settings($post_type);
332 363
333 364 return !empty($settings['schema_type']);
334 365 }
@@ -333,8 +364,27 @@
333 364 return !empty($settings['schema_type']);
334 365 }
335 366
336 367 /**
368 + * Whether this post type has a SAVED schema type, ignoring the built-in
369 + * per-post-type default.
370 + *
371 + * would_output_schema() answers "will JSON-LD be emitted?", which the
372 + * fallback in get_global_seo_settings() makes true for every public post
373 + * type. The audit needs the different question "has the user configured
374 + * anything?", so this reads the stored option without the default merge.
375 + *
376 + * @since 2.2.0
377 + * @param string $post_type Post type.
378 + * @return bool True when an explicit schema_type is stored for this type.
379 + */
380 + public function has_explicit_schema_type(string $post_type): bool {
381 + $all_settings = get_option(self::OPTION_NAME, []);
382 +
383 + return !empty($all_settings[$post_type]['schema_type']);
384 + }
385 +
386 + /**
337 387 * Get Global SEO settings for a specific post type
338 388 *
339 389 * @since 1.0.0
340 390 * @param string $post_type Post type
@@ -473,10 +523,11 @@
473 523 'datePublished' => get_the_date('c', $post),
474 524 'dateModified' => get_the_modified_date('c', $post),
475 525 ];
476 526
477 - // Add description
478 - $excerpt = get_the_excerpt($post);
527 + // Add description. A Bricks page's stored `post_content` is not on the
528 + // page, so core's derived excerpt must not describe it (#651).
529 + $excerpt = $this->post_excerpt_text($post);
479 530 if (!empty($excerpt)) {
480 531 $schema['description'] = wp_strip_all_tags($excerpt);
481 532 }
482 533
@@ -547,9 +598,9 @@
547 598 }
548 599 require_once $builder_file;
549 600 }
550 601
551 - $excerpt = get_the_excerpt($post);
602 + $excerpt = $this->post_excerpt_text($post);
552 603
553 604 $builder = new \ThinkRank\SEO\Schema_Builder();
554 605 $schema = $builder->build_schema(
555 606 'FAQPage',
@@ -554,9 +605,9 @@
554 605 $schema = $builder->build_schema(
555 606 'FAQPage',
556 607 [
557 608 'title' => get_the_title($post),
558 - 'content' => $post->post_content,
609 + 'content' => \ThinkRank\SEO\Builder_Content::visible_content($post),
559 610 'excerpt' => $excerpt ? wp_strip_all_tags($excerpt) : '',
560 611 'url' => get_permalink($post),
561 612 ],
562 613 get_post_type($post) === 'page' ? 'page' : 'post'
@@ -627,9 +678,9 @@
627 678 'dateModified' => get_the_modified_date('c', $post),
628 679 ];
629 680
630 681 // Add description
631 - $excerpt = get_the_excerpt($post);
682 + $excerpt = $this->post_excerpt_text($post);
632 683 if (!empty($excerpt)) {
633 684 $schema['description'] = wp_strip_all_tags($excerpt);
634 685 }
635 686
@@ -697,9 +748,9 @@
697 748 'url' => get_permalink($post),
698 749 ];
699 750
700 751 // Add description
701 - $description = get_the_excerpt($post);
752 + $description = $this->post_excerpt_text($post);
702 753 if (empty($description)) {
703 754 $caption = wp_get_attachment_caption($post->ID);
704 755 if (!empty($caption)) {
705 756 $description = $caption;
@@ -863,15 +914,20 @@
863 914 private function get_product_description(\WP_Post $post): string {
864 915 // Try custom meta field first
865 916 $description = get_post_meta($post->ID, '_thinkrank_product_description', true);
866 917
867 - // Fallback to excerpt or content
918 + // Fallback to excerpt or content. On a Bricks page the excerpt core
919 + // derives comes from discarded `post_content`, so the visible body is
920 + // used instead (#651).
868 921 if (empty($description)) {
869 - $description = get_the_excerpt($post);
922 + $description = $this->post_excerpt_text($post);
870 923 }
871 924
872 925 if (empty($description)) {
873 - $description = \ThinkRank\SEO\Pattern_Resolver::derive_excerpt((string) $post->post_content, 30);
926 + $description = \ThinkRank\SEO\Pattern_Resolver::derive_excerpt(
927 + \ThinkRank\SEO\Builder_Content::visible_content($post),
928 + 30
929 + );
874 930 }
875 931
876 932 return wp_strip_all_tags($description);
877 933 }
@@ -1135,8 +1191,29 @@
1135 1191 }
1136 1192 }
1137 1193
1138 1194 return $reviews;
1195 + }
1196 +
1197 + /**
1198 + * The post's excerpt, taken from content the page actually renders.
1199 + *
1200 + * `get_the_excerpt()` falls back to trimming `post_content`, which a Bricks
1201 + * page discards — so on one of those it describes text no visitor sees. A
1202 + * hand-written excerpt is the author's own summary and still wins, because
1203 + * `superseding_excerpt_source()` yields nothing for a post that has one
1204 + * (#651).
1205 + *
1206 + * @since 2.3.1
1207 + * @param \WP_Post $post Post being described.
1208 + * @return string
1209 + */
1210 + private function post_excerpt_text(\WP_Post $post): string {
1211 + $superseding = \ThinkRank\SEO\Builder_Content::superseding_excerpt_source($post);
1212 +
1213 + return '' !== $superseding
1214 + ? \ThinkRank\SEO\Pattern_Resolver::derive_excerpt($superseding, 30)
1215 + : (string) get_the_excerpt($post);
1139 1216 }
1140 1217
1141 1218 /**
1142 1219 * Register generated schema with the request's schema graph.