← All changes
|
includes/abilities/settings/class-settings-key-map.php
+98
-3
2.4.0
→
2.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' ) ), |
| @@ -135,9 +135,21 @@ | ||
| 135 | 135 | 'business_hours' => self::business_hours(), |
| 136 | 136 | |
| 137 | 137 | // Indexing. |
| 138 | 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 | + ], | |
| 139 | 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(), | |
| 140 | 152 | ]; |
| 141 | 153 | } |
| 142 | 154 | |
| 143 | 155 | /** |
| @@ -159,8 +171,14 @@ | ||
| 159 | 171 | 'custom_url_pattern' => self::string( __( 'Filename pattern for generated sitemaps, e.g. "sitemap-{type}.xml".', 'thinkrank' ) ), |
| 160 | 172 | 'enable_styling' => self::boolean( __( 'Whether an XSL stylesheet is attached so the sitemap is readable in a browser.', 'thinkrank' ) ), |
| 161 | 173 | 'ping_search_engines' => self::boolean( __( 'Whether search engines are notified after the sitemap is regenerated.', 'thinkrank' ) ), |
| 162 | 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 | + | |
| 163 | 181 | // What goes in. |
| 164 | 182 | 'include_posts' => self::boolean( __( 'Whether posts are included.', 'thinkrank' ) ), |
| 165 | 183 | 'include_pages' => self::boolean( __( 'Whether pages are included.', 'thinkrank' ) ), |
| 166 | 184 | 'include_categories' => self::boolean( __( 'Whether category archives are included.', 'thinkrank' ) ), |
| @@ -191,9 +209,21 @@ | ||
| 191 | 209 | |
| 192 | 210 | foreach ( $properties as $key => $property ) { |
| 193 | 211 | $value = $stored[ $key ] ?? null; |
| 194 | 212 | |
| 195 | - 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 ) { | |
| 196 | 226 | case 'boolean': |
| 197 | 227 | $out[ $key ] = (bool) $value; |
| 198 | 228 | break; |
| 199 | 229 | case 'integer': |
| @@ -232,9 +262,21 @@ | ||
| 232 | 262 | } |
| 233 | 263 | |
| 234 | 264 | $value = $incoming[ $key ]; |
| 235 | 265 | |
| 236 | - 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 ) { | |
| 237 | 279 | case 'boolean': |
| 238 | 280 | $out[ $key ] = (bool) $value; |
| 239 | 281 | break; |
| 240 | 282 | case 'integer': |
| @@ -309,13 +351,66 @@ | ||
| 309 | 351 | ]; |
| 310 | 352 | } |
| 311 | 353 | |
| 312 | 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 | + /** | |
| 313 | 386 | * A boolean property. |
| 314 | 387 | * |
| 315 | 388 | * @param string $description Translated text describing what the toggle does. |
| 316 | 389 | * @return array<string, mixed> |
| 317 | 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 | + | |
| 318 | 413 | private static function boolean( string $description ): array { |
| 319 | 414 | return [ |
| 320 | 415 | 'type' => 'boolean', |
| 321 | 416 | 'description' => $description, |