PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.0
2.14.0 2.13.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 All 55 releases
← All changes | includes/seo/class-global-seo-post-types.php +116 -5 1.30.0 → 2.14.0 View file →
@@ -34,9 +34,10 @@
34 34 * Whether a post type may receive Global SEO settings.
35 35 *
36 36 * Policy: the type must be public; a non-built-in type must also be
37 37 * front-end viewable (excludes builder/utility CPTs that register
38 - * `public => true` only for previews); and it must not be on the filterable
38 + * `public => true` only for previews); WordPress must actually render a page
39 + * for it; and it must not be on the filterable
39 40 * `thinkrank_global_seo_excluded_post_types` deny list.
40 41 *
41 42 * @param \WP_Post_Type|string $post_type Post type object or name.
42 43 * @return bool
@@ -49,14 +50,117 @@
49 50 }
50 51
51 52 // Builder/utility CPTs that register public => true for preview purposes
52 53 // but aren't front-end viewable content (e.g. Elementor's "Floating
53 - // Elements"). Built-in types (post/page/attachment) are always kept.
54 + // Elements"). Built-in types (post/page) are always kept.
54 55 if ($object->_builtin === false && !is_post_type_viewable($object)) {
55 56 return false;
56 57 }
57 58
58 - // Named deny list for viewable-but-unsuitable builder/utility CPTs.
59 + // Attachment is built-in and public, so the rule above keeps it, but on
60 + // a site that does not serve attachment pages there is no document for
61 + // any of these settings to reach (#843).
62 + if ('attachment' === $object->name && !self::renders_a_page($object)) {
63 + return false;
64 + }
65 +
66 + return !in_array($object->name, self::excluded_post_types($object), true);
67 + }
68 +
69 + /**
70 + * Whether WordPress renders an HTML page for this post type on this site.
71 + *
72 + * Only `attachment` can answer no. Core has redirected attachment URLs
73 + * straight to the file since WordPress 6.4, and does so by default on a new
74 + * install: the request 301s to the uploads path, so there is no <head> for a
75 + * title, a robots meta, a canonical or JSON-LD to appear in. Every Global
76 + * SEO control for the type is therefore inert, while the screen presents
77 + * itself exactly like the ones that work.
78 + *
79 + * Read from the option core itself consults rather than hardcoded, and kept
80 + * in one place so the admin nav, the REST validator and the ability
81 + * validators cannot disagree about it. A site with attachment pages enabled
82 + * is unaffected and keeps every setting it has.
83 + *
84 + * Deliberately not applied to any other type: `is_post_type_viewable()`
85 + * already covers the general case, and this is a core quirk specific to
86 + * attachments.
87 + *
88 + * @since 2.14.0
89 + *
90 + * @param \WP_Post_Type|string $post_type Post type object or name.
91 + * @return bool
92 + */
93 + public static function renders_a_page($post_type): bool {
94 + $object = is_string($post_type) ? get_post_type_object($post_type) : $post_type;
95 +
96 + if (!$object instanceof \WP_Post_Type) {
97 + return false;
98 + }
99 +
100 + if ('attachment' !== $object->name) {
101 + return true;
102 + }
103 +
104 + // The same test core's own redirect makes, in wp-includes/canonical.php:
105 + // if ( is_attachment() && ! get_option( 'wp_attachment_pages_enabled' ) )
106 + // so this agrees with what actually happens to the request. Core has no
107 + // accessor for it; several core files read the option directly, and two
108 + // of them compare against '1' rather than testing truthiness. The
109 + // redirect uses the truthy form, and the redirect is what decides
110 + // whether a document exists, so that is the form matched here.
111 + //
112 + // The default is false because core's is: schema.php seeds a fresh
113 + // install with 0, while upgrade.php sets 1 for a site upgrading from
114 + // before 6.4, which keeps its attachment pages and is unaffected.
115 + return (bool) get_option('wp_attachment_pages_enabled', false);
116 + }
117 +
118 + /**
119 + * Every post type that passes {@see self::is_allowed()}.
120 + *
121 + * The sitewide half of duplicate snippet detection needs the whole list
122 + * rather than one name, because a duplicate title is a duplicate whichever
123 + * post type the other page happens to be (#564).
124 + *
125 + * @since 2.10.0
126 + *
127 + * @return string[] Post type names.
128 + */
129 + public static function allowed(): array {
130 + $allowed = [];
131 +
132 + foreach (get_post_types(['public' => true], 'objects') as $object) {
133 + if (self::is_allowed($object)) {
134 + $allowed[] = $object->name;
135 + }
136 + }
137 +
138 + return $allowed;
139 + }
140 +
141 + /**
142 + * Builder and utility CPTs that are not optimizable content.
143 + *
144 + * These register `public => true` for preview purposes but are layout
145 + * fragments, not pages anyone gives a title and a meta description.
146 + *
147 + * Shared rather than duplicated: the per-post SEO metabox discovers post
148 + * types by the same public/`show_ui` rule and used to keep its own
149 + * exclusion list, which named only WordPress internals. The two policies
150 + * disagreed — a builder template CPT was denied Global SEO while still
151 + * getting a full metabox — so both now read this one list (#621).
152 + *
153 + * @since 2.2.1
154 + *
155 + * Deliberately untyped: this is passed straight to a public filter whose
156 + * existing contract accepts whatever the caller has (a `WP_Post_Type`, or
157 + * null when the list is wanted without a subject).
158 + *
159 + * @param mixed $post_type_object Post type being judged, passed to the filter.
160 + * @return string[] Post type names to exclude.
161 + */
162 + public static function excluded_post_types($post_type_object = null): array {
59 163 // Filterable so integrators can tune it without patching core. The
60 164 // post-type object is passed as the second argument (matches the admin
61 165 // discovery filter usage).
62 166 $excluded = apply_filters('thinkrank_global_seo_excluded_post_types', [
@@ -64,9 +168,16 @@
64 168 'e-floating-buttons', 'elementor_component',
65 169 // Divi: the library plus every Theme Builder template type.
66 170 'et_pb_layout', 'et_theme_builder', 'et_template',
67 171 'et_header_layout', 'et_body_layout', 'et_footer_layout',
68 - ], $object);
172 + // Beaver Builder: saved templates (rows, columns, modules) and
173 + // Beaver Themer's layouts. Same clutter Divi's Theme Builder
174 + // templates caused before they were listed here (#449).
175 + 'fl-builder-template', 'fl-theme-layout',
176 + // Bricks: saved templates plus the header/footer/section layouts
177 + // its Theme Builder stores as the same CPT (#257).
178 + 'bricks_template',
179 + ], $post_type_object);
69 180
70 - return !in_array($object->name, (array) $excluded, true);
181 + return array_values(array_filter((array) $excluded, 'is_string'));
71 182 }
72 183 }