PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.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 1.25.0 All 54 releases
← All changes | includes/abilities/settings/class-settings-key-map.php +103 -27 2.9.0 → 2.13.0 View file →
@@ -90,8 +90,15 @@
90 90 // without stripping their tokens.
91 91 'title_template' => self::string( __( 'Named title layout, e.g. "default", "reverse", "category".', 'thinkrank' ) ),
92 92 'title_separator' => self::string( __( 'Separator between title parts, e.g. "pipe", "dash".', 'thinkrank' ) ),
93 93 'homepage_title' => self::template( __( 'the homepage', 'thinkrank' ), '' ),
94 + 'homepage_description' => self::string(
95 + sprintf(
96 + /* translators: %s: the list of available variable tags. */
97 + __( 'Meta description for the homepage when it lists the latest posts. Available tags: %s. A static front page uses its own page description instead.', 'thinkrank' ),
98 + '%site_title%, %site_name%, %site_description%, %tagline%, %sep%'
99 + )
100 + ),
94 101 'category_title' => self::template( __( 'category archives', 'thinkrank' ), '%category_title%, %category%' ),
95 102 'tag_title' => self::template( __( 'tag archives', 'thinkrank' ), '%tag_title%' ),
96 103 'author_title' => self::template( __( 'author archives', 'thinkrank' ), '%author_name%' ),
97 104 'search_title' => self::template( __( 'search results pages', 'thinkrank' ), '%search_term%' ),
@@ -120,9 +127,22 @@
120 127
121 128 // Business / Local SEO. Feeds LocalBusiness schema.
122 129 'local_seo_enabled' => self::boolean( __( 'Whether Local SEO output and LocalBusiness schema are enabled.', 'thinkrank' ) ),
123 130 'business_name' => self::string( __( 'Registered business name.', 'thinkrank' ) ),
124 - 'business_type' => self::string( __( 'schema.org business type, e.g. "Restaurant", "Store".', 'thinkrank' ) ),
131 + // Enumerated rather than free-form: the value goes straight into
132 + // LocalBusiness schema, so an invented type is invalid structured
133 + // data. ~150 schema.org subtypes are accepted (#623).
134 + //
135 + // '' is in the list because it is a real stored state ("not set"):
136 + // the store holds it on every site that never opened Local SEO, and
137 + // without it the READ ability failed its own output schema on those
138 + // sites and there was no way to clear the value over MCP. It goes
139 + // last so the root stays first, which read() falls back to.
140 + 'business_type' => [
141 + 'type' => 'string',
142 + 'enum' => array_merge( \ThinkRank\Config\Local_Business_Types_Config::get_types(), [ '' ] ),
143 + 'description' => __( 'schema.org LocalBusiness type, e.g. "Restaurant", "HealthAndBeautyBusiness". "LocalBusiness" is the general-purpose default and is always valid. An empty string means not set, and is published as LocalBusiness.', 'thinkrank' ),
144 + ],
125 145 'business_email' => self::string( __( 'Public contact email address.', 'thinkrank' ) ),
126 146 'business_phone' => self::string( __( 'Public contact telephone number.', 'thinkrank' ) ),
127 147 'business_address' => self::string( __( 'Street address.', 'thinkrank' ) ),
128 148 'business_city' => self::string( __( 'City or locality.', 'thinkrank' ) ),
@@ -204,8 +224,17 @@
204 224 *
205 225 * The store keeps everything as strings ("1"/""), so each value is coerced
206 226 * back to the type the schema advertises before it reaches an agent.
207 227 *
228 + * The same properties are the read ability's OUTPUT schema, which the
229 + * Abilities API validates, and one value that fails it fails the whole
230 + * call. So what comes out has to fit the schema for any stored value, not
231 + * only for what today's save path writes: a business type imported before
232 + * the list was enumerated, or a crawler rule whose crawler a filter has
233 + * since removed, used to turn every get-site-identity-settings call into
234 + * ability_invalid_output. Hence the recursion into objects, and the enum
235 + * fallback in read_value().
236 + *
208 237 * @param array<string, array<string, mixed>> $properties Schema properties for the category.
209 238 * @param array<string, mixed> $stored Settings as the manager returns them.
210 239 * @return array<string, mixed> Exposed settings, one entry per declared key.
211 240 */
@@ -212,39 +241,86 @@
212 241 public static function read( array $properties, array $stored ): array {
213 242 $out = [];
214 243
215 244 foreach ( $properties as $key => $property ) {
216 - $value = $stored[ $key ] ?? null;
245 + $out[ $key ] = self::read_value( $property, $stored[ $key ] ?? null );
246 + }
217 247
218 - $type = $property['type'] ?? 'string';
248 + return $out;
249 + }
219 250
220 - // A union type ('string' or a list of them) keeps whichever shape
221 - // it arrived in: casting to string would flatten a list, and
222 - // casting to array would replace a name with [] (#692).
223 - if ( is_array( $type ) ) {
224 - $out[ $key ] = is_array( $value )
225 - ? array_values( array_map( 'strval', $value ) )
226 - : (string) ( $value ?? '' );
227 - continue;
228 - }
251 + /**
252 + * Coerce one stored value to the shape its property declares.
253 + *
254 + * An enumerated string that holds something outside its enum reads as the
255 + * enum's FIRST entry. Every enum in this map is ordered so that entry is
256 + * the value its manager falls back to for anything unrecognised
257 + * (`automatic`, `auto`, `allow`, `LocalBusiness`), so the agent is told
258 + * what the site actually does rather than a value it would be refused if
259 + * it wrote it back.
260 + *
261 + * @since 2.10.0
262 + *
263 + * @param array<string, mixed> $property Schema property.
264 + * @param mixed $value Stored value, or null when absent.
265 + * @return mixed
266 + */
267 + private static function read_value( array $property, $value ) {
268 + $type = $property['type'] ?? 'string';
229 269
230 - switch ( $type ) {
231 - case 'boolean':
232 - $out[ $key ] = (bool) $value;
233 - break;
234 - case 'integer':
235 - $out[ $key ] = (int) $value;
236 - break;
237 - case 'array':
238 - case 'object':
239 - $out[ $key ] = is_array( $value ) ? $value : [];
240 - break;
241 - default:
242 - $out[ $key ] = (string) ( $value ?? '' );
243 - }
270 + // A union type ('string' or a list of them) keeps whichever shape
271 + // it arrived in: casting to string would flatten a list, and
272 + // casting to array would replace a name with [] (#692).
273 + if ( is_array( $type ) ) {
274 + return is_array( $value )
275 + ? array_values( array_map( 'strval', array_filter( $value, 'is_scalar' ) ) )
276 + : ( is_scalar( $value ) ? (string) $value : '' );
244 277 }
245 278
246 - return $out;
279 + switch ( $type ) {
280 + case 'boolean':
281 + return (bool) $value;
282 + case 'integer':
283 + return (int) $value;
284 + case 'array':
285 + if ( ! is_array( $value ) ) {
286 + return [];
287 + }
288 + if ( isset( $property['items'] ) && is_array( $property['items'] ) ) {
289 + $items = $property['items'];
290 + return array_values( array_map( static fn( $item ) => self::read_value( $items, $item ), $value ) );
291 + }
292 + return $value;
293 + case 'object':
294 + if ( ! is_array( $value ) ) {
295 + return [];
296 + }
297 + if ( empty( $property['properties'] ) || ! is_array( $property['properties'] ) ) {
298 + return $value;
299 + }
300 +
301 + $declared = $property['properties'];
302 + $closed = isset( $property['additionalProperties'] ) && false === $property['additionalProperties'];
303 + $object = [];
304 +
305 + foreach ( $value as $name => $item ) {
306 + if ( isset( $declared[ $name ] ) ) {
307 + $object[ $name ] = self::read_value( $declared[ $name ], $item );
308 + } elseif ( ! $closed ) {
309 + $object[ $name ] = $item;
310 + }
311 + }
312 +
313 + return $object;
314 + default:
315 + $string = is_scalar( $value ) ? (string) $value : '';
316 +
317 + if ( ! empty( $property['enum'] ) && ! in_array( $string, $property['enum'], true ) ) {
318 + return (string) reset( $property['enum'] );
319 + }
320 +
321 + return $string;
322 + }
247 323 }
248 324
249 325 /**
250 326 * Coerce an incoming patch to the declared types, dropping unknown keys.