PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/abilities/settings/class-settings-key-map.php +99 -3 2.3.02.7.0 View file →
@@ -74,9 +74,9 @@
74 74 'enabled' => self::boolean( __( 'Whether site identity management is active.', 'thinkrank' ) ),
75 75 'site_name' => self::string( __( 'Official name of the site, used in titles and schema.', 'thinkrank' ) ),
76 76 'site_description' => self::string( __( 'Short description of the site.', 'thinkrank' ) ),
77 77 'tagline' => self::string( __( 'Site tagline.', 'thinkrank' ) ),
78 - 'alternate_name' => self::string( __( 'Alternate or former name of the site, published as schema alternateName.', 'thinkrank' ) ),
78 + 'alternate_name' => self::alternate_name(),
79 79 'identity_type' => self::string( __( 'What the site is, e.g. "blog", "business", "portfolio".', 'thinkrank' ) ),
80 80 'represents' => self::string( __( 'Whether the site represents a "person" or an "organization".', 'thinkrank' ) ),
81 81 'default_meta_description' => self::string( __( 'Meta description used where no more specific one is set.', 'thinkrank' ) ),
82 82 'default_social_image' => self::string( __( 'URL of the fallback social sharing image.', 'thinkrank' ) ),
@@ -103,8 +103,9 @@
103 103 'breadcrumb_home_text' => self::string( __( 'Label for the home link in breadcrumbs.', 'thinkrank' ) ),
104 104 'breadcrumb_separator' => self::string( __( 'Separator drawn between breadcrumb items.', 'thinkrank' ) ),
105 105 'breadcrumb_prefix' => self::string( __( 'Text shown before the breadcrumb trail.', 'thinkrank' ) ),
106 106 'show_current_page' => self::boolean( __( 'Whether the current page appears in its own breadcrumb trail.', 'thinkrank' ) ),
107 + 'breadcrumb_use_seo_title' => self::boolean( __( 'Whether breadcrumb labels use the SEO title of a post or term when one is set, instead of its raw title.', 'thinkrank' ) ),
107 108
108 109 // Brand imagery.
109 110 'logo_url' => self::string( __( 'URL of the site logo.', 'thinkrank' ) ),
110 111 'favicon_url' => self::string( __( 'URL of the site favicon.', 'thinkrank' ) ),
@@ -134,9 +135,21 @@
134 135 'business_hours' => self::business_hours(),
135 136
136 137 // Indexing.
137 138 'allow_search_engines' => self::boolean( __( 'Whether search engines are allowed to index the site.', 'thinkrank' ) ),
139 + 'query_protection' => self::boolean( __( 'Whether a URL whose content selector resolved to nothing (for example ?post_type=nosuchtype) answers 404 instead of serving the blog listing at 200.', 'thinkrank' ) ),
140 + 'canonical_scheme' => [
141 + 'type' => 'string',
142 + 'description' => __( 'Scheme for canonical URLs, og:url, schema @ids and sitemap entries. "automatic" follows WordPress; the other two override it, for a site behind a proxy that terminates TLS and leaves WordPress reporting the wrong scheme.', 'thinkrank' ),
143 + 'enum' => \ThinkRank\SEO\Url_Scheme::MODES,
144 + ],
138 145 'robots_txt_enabled' => self::boolean( __( 'Whether ThinkRank manages robots.txt. Its contents are set with thinkrank/update-robots-txt.', 'thinkrank' ) ),
146 +
147 + // RSS feeds.
148 + 'feed_excerpt_only' => self::boolean( __( 'Whether feed entries are shortened to an excerpt instead of carrying the full post.', 'thinkrank' ) ),
149 + 'feed_source_link' => self::boolean( __( 'Whether each feed entry is signed with a link back to the original post and the site.', 'thinkrank' ) ),
150 + 'feed_noindex' => self::boolean( __( 'Whether feeds are served with X-Robots-Tag: noindex. Leave off for a podcast feed, which needs to be indexable.', 'thinkrank' ) ),
151 + 'ai_crawler_rules' => self::ai_crawler_rules(),
139 152 ];
140 153 }
141 154
142 155 /**
@@ -158,8 +171,14 @@
158 171 'custom_url_pattern' => self::string( __( 'Filename pattern for generated sitemaps, e.g. "sitemap-{type}.xml".', 'thinkrank' ) ),
159 172 'enable_styling' => self::boolean( __( 'Whether an XSL stylesheet is attached so the sitemap is readable in a browser.', 'thinkrank' ) ),
160 173 'ping_search_engines' => self::boolean( __( 'Whether search engines are notified after the sitemap is regenerated.', 'thinkrank' ) ),
161 174
175 + // How the styled sitemap looks. All four need enable_styling on.
176 + 'styling_logo' => self::boolean( __( 'Whether a logo is shown above the sitemap heading. Requires enable_styling.', 'thinkrank' ) ),
177 + 'styling_logo_url' => self::string( __( 'URL of the sitemap logo image. Empty falls back to the site icon.', 'thinkrank' ) ),
178 + 'styling_color_main' => self::string( __( 'Hex colour ("#rrggbb") for the sitemap header, links and table head. Empty keeps the stock palette.', 'thinkrank' ) ),
179 + 'styling_color_accent' => self::string( __( 'Hex colour ("#rrggbb") for the header gradient end and link hovers. Empty keeps the stock palette.', 'thinkrank' ) ),
180 +
162 181 // What goes in.
163 182 'include_posts' => self::boolean( __( 'Whether posts are included.', 'thinkrank' ) ),
164 183 'include_pages' => self::boolean( __( 'Whether pages are included.', 'thinkrank' ) ),
165 184 'include_categories' => self::boolean( __( 'Whether category archives are included.', 'thinkrank' ) ),
@@ -190,9 +209,21 @@
190 209
191 210 foreach ( $properties as $key => $property ) {
192 211 $value = $stored[ $key ] ?? null;
193 212
194 - switch ( $property['type'] ) {
213 + $type = $property['type'] ?? 'string';
214 +
215 + // A union type ('string' or a list of them) keeps whichever shape
216 + // it arrived in: casting to string would flatten a list, and
217 + // casting to array would replace a name with [] (#692).
218 + if ( is_array( $type ) ) {
219 + $out[ $key ] = is_array( $value )
220 + ? array_values( array_map( 'strval', $value ) )
221 + : (string) ( $value ?? '' );
222 + continue;
223 + }
224 +
225 + switch ( $type ) {
195 226 case 'boolean':
196 227 $out[ $key ] = (bool) $value;
197 228 break;
198 229 case 'integer':
@@ -231,9 +262,21 @@
231 262 }
232 263
233 264 $value = $incoming[ $key ];
234 265
235 - switch ( $property['type'] ) {
266 + $type = $property['type'] ?? 'string';
267 +
268 + // A union type ('string' or a list of them) keeps whichever shape
269 + // it arrived in: casting to string would flatten a list, and
270 + // casting to array would replace a name with [] (#692).
271 + if ( is_array( $type ) ) {
272 + $out[ $key ] = is_array( $value )
273 + ? array_values( array_map( 'strval', $value ) )
274 + : (string) ( $value ?? '' );
275 + continue;
276 + }
277 +
278 + switch ( $type ) {
236 279 case 'boolean':
237 280 $out[ $key ] = (bool) $value;
238 281 break;
239 282 case 'integer':
@@ -308,13 +351,66 @@
308 351 ];
309 352 }
310 353
311 354 /**
355 + * The per-agent AI crawler allow/block map.
356 + *
357 + * Enumerating the known slugs rather than accepting a free-form object is
358 + * what makes this usable by an agent: the alternative is a caller guessing
359 + * `chatgpt` for a crawler registered as `chatgpt-user`, having the key
360 + * dropped as unknown, and being told the save succeeded — because it did,
361 + * with nothing in it.
362 + *
363 + * @return array<string, mixed>
364 + */
365 + private static function ai_crawler_rules(): array {
366 + $properties = [];
367 +
368 + foreach ( \ThinkRank\SEO\AI_Crawlers::for_display() as $agent ) {
369 + $properties[ $agent['slug'] ] = [
370 + 'type' => 'string',
371 + 'enum' => [ 'allow', 'block' ],
372 + /* translators: 1: crawler user-agent token, e.g. GPTBot. 2: what that crawler is for. */
373 + 'description' => sprintf( __( '%1$s — %2$s', 'thinkrank' ), $agent['token'], $agent['purpose'] ),
374 + ];
375 + }
376 +
377 + return [
378 + 'type' => 'object',
379 + 'description' => __( 'Per-agent AI crawler rules. "block" writes a User-agent/Disallow pair for that crawler into robots.txt; "allow" (the default for any crawler not listed) writes nothing. Blocking Google-Extended does not affect normal Google Search indexing.', 'thinkrank' ),
380 + 'additionalProperties' => false,
381 + 'properties' => $properties,
382 + ];
383 + }
384 +
385 + /**
312 386 * A boolean property.
313 387 *
314 388 * @param string $description Translated text describing what the toggle does.
315 389 * @return array<string, mixed>
316 390 */
391 + /**
392 + * The site's alternate name, as one value or several.
393 + *
394 + * schema.org and Google both allow `alternateName` to carry a list, and the
395 + * store already round-trips either shape, so the schema says so rather than
396 + * forcing a caller to pick one name and drop the rest (#692).
397 + *
398 + * @since 2.7.0
399 + * @return array<string, mixed>
400 + */
401 + private static function alternate_name(): array {
402 + return [
403 + // A union rather than anyOf: read() and coerce() switch on `type`,
404 + // and an entry without one is dropped as unrecognised. WordPress's
405 + // schema validator accepts a type list, so this is both valid JSON
406 + // Schema and legible to this file's own consumers.
407 + 'type' => [ 'string', 'array' ],
408 + 'items' => [ 'type' => 'string' ],
409 + 'description' => __( 'Alternate or former name of the site, published as schema alternateName on the homepage WebSite node. Send a string for one name, or an array for several. Blank entries and duplicates are dropped, and a single surviving name is published as a string.', 'thinkrank' ),
410 + ];
411 + }
412 +
317 413 private static function boolean( string $description ): array {
318 414 return [
319 415 'type' => 'boolean',
320 416 'description' => $description,