• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 7df79e3390f05f08da5c8e2bb4777c9e3650e3b6

14 Jul 2026 08:15AM UTC coverage: 52.186%. First build
7df79e3390f05f08da5c8e2bb4777c9e3650e3b6

Pull #23442

github

web-flow
Merge 93fb89c74 into b9fe3f49c
Pull Request #23442: Init commit for ability about posts with seo issues

171 of 189 new or added lines in 4 files covered. (90.48%)

22503 of 43121 relevant lines covered (52.19%)

4.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.7
/src/abilities/user-interface/abilities-integration.php
1
<?php
2

3
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4
namespace Yoast\WP\SEO\Abilities\User_Interface;
5

6
use Yoast\WP\SEO\Abilities\Application\Post_SEO_Data_Collector;
7
use Yoast\WP\SEO\Abilities\Application\Post_SEO_Data_Updater;
8
use Yoast\WP\SEO\Abilities\Application\Posts_With_SEO_Issues_Retriever;
9
use Yoast\WP\SEO\Abilities\Application\Score_Retriever;
10
use Yoast\WP\SEO\Conditionals\Abilities_API_Conditional;
11
use Yoast\WP\SEO\Conditionals\Should_Index_Indexables_Conditional;
12
use Yoast\WP\SEO\Config\Schema_Types;
13
use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository;
14
use Yoast\WP\SEO\Editors\Framework\Inclusive_Language_Analysis;
15
use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis;
16
use Yoast\WP\SEO\Editors\Framework\Readability_Analysis;
17
use Yoast\WP\SEO\Helpers\Capability_Helper;
18
use Yoast\WP\SEO\Integrations\Integration_Interface;
19

20
/**
21
 * Integration that registers Yoast SEO abilities with the WordPress Abilities API.
22
 */
23
class Abilities_Integration implements Integration_Interface {
24

25
        /**
26
         * The score retriever.
27
         *
28
         * @var Score_Retriever
29
         */
30
        private $score_retriever;
31

32
        /**
33
         * The capability helper.
34
         *
35
         * @var Capability_Helper
36
         */
37
        private $capability_helper;
38

39
        /**
40
         * The enabled analysis features repository.
41
         *
42
         * @var Enabled_Analysis_Features_Repository
43
         */
44
        private $enabled_analysis_features_repository;
45

46
        /**
47
         * The post SEO data collector.
48
         *
49
         * @var Post_SEO_Data_Collector
50
         */
51
        private $post_seo_data_collector;
52

53
        /**
54
         * The post SEO data updater.
55
         *
56
         * @var Post_SEO_Data_Updater
57
         */
58
        private $post_seo_data_updater;
59

60
        /**
61
         * The posts with SEO issues retriever.
62
         *
63
         * @var Posts_With_SEO_Issues_Retriever
64
         */
65
        private $posts_with_seo_issues_retriever;
66

67
        /**
68
         * Returns the conditionals based on which this loadable should be active.
69
         *
70
         * @return array<string> The conditionals.
71
         */
72
        public static function get_conditionals() {
2✔
73
                return [
2✔
74
                        Abilities_API_Conditional::class,
2✔
75
                        Should_Index_Indexables_Conditional::class,
2✔
76
                ];
2✔
77
        }
78

79
        /**
80
         * Constructor.
81
         *
82
         * @param Score_Retriever                      $score_retriever                      The score retriever.
83
         * @param Capability_Helper                    $capability_helper                    The capability helper.
84
         * @param Enabled_Analysis_Features_Repository $enabled_analysis_features_repository The enabled analysis features repository.
85
         * @param Post_SEO_Data_Collector              $post_seo_data_collector              The post SEO data collector.
86
         * @param Post_SEO_Data_Updater                $post_seo_data_updater                The post SEO data updater.
87
         * @param Posts_With_SEO_Issues_Retriever      $posts_with_seo_issues_retriever      The posts with SEO issues retriever.
88
         */
89
        public function __construct(
×
90
                Score_Retriever $score_retriever,
91
                Capability_Helper $capability_helper,
92
                Enabled_Analysis_Features_Repository $enabled_analysis_features_repository,
93
                Post_SEO_Data_Collector $post_seo_data_collector,
94
                Post_SEO_Data_Updater $post_seo_data_updater,
95
                Posts_With_SEO_Issues_Retriever $posts_with_seo_issues_retriever
96
        ) {
97
                $this->score_retriever                      = $score_retriever;
×
98
                $this->capability_helper                    = $capability_helper;
×
99
                $this->enabled_analysis_features_repository = $enabled_analysis_features_repository;
×
100
                $this->post_seo_data_collector              = $post_seo_data_collector;
×
101
                $this->post_seo_data_updater                = $post_seo_data_updater;
×
NEW
102
                $this->posts_with_seo_issues_retriever      = $posts_with_seo_issues_retriever;
×
103
        }
104

105
        /**
106
         * Registers hooks with WordPress.
107
         *
108
         * @return void
109
         */
110
        public function register_hooks() {
2✔
111
                \add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
2✔
112
        }
113

114
        /**
115
         * Registers the Yoast SEO abilities.
116
         *
117
         * @return void
118
         */
119
        public function register_abilities() {
16✔
120
                $enabled_features = $this->enabled_analysis_features_repository->get_features_by_keys(
16✔
121
                        [
16✔
122
                                Keyphrase_Analysis::NAME,
16✔
123
                                Readability_Analysis::NAME,
16✔
124
                                Inclusive_Language_Analysis::NAME,
16✔
125
                        ],
16✔
126
                )->to_array();
16✔
127

128
                if ( $enabled_features[ Keyphrase_Analysis::NAME ] === true ) {
16✔
129
                        $this->register_seo_scores_ability();
6✔
130
                }
131

132
                if ( $enabled_features[ Readability_Analysis::NAME ] === true ) {
16✔
133
                        $this->register_readability_scores_ability();
4✔
134
                }
135

136
                if ( $enabled_features[ Inclusive_Language_Analysis::NAME ] === true ) {
16✔
137
                        $this->register_inclusive_language_scores_ability();
4✔
138
                }
139

140
                // Metadata read/write is independent of which analysis features are enabled.
141
                $this->register_get_post_seo_data_ability();
16✔
142
                $this->register_update_post_seo_data_ability();
16✔
143

144
                $this->register_get_posts_with_seo_issues_ability( $enabled_features );
16✔
145
        }
146

147
        /**
148
         * Checks whether the current user can manage Yoast SEO.
149
         *
150
         * Gates every ability — reading scores, reading post SEO data, and updating it —
151
         * behind the same Yoast SEO management capability.
152
         *
153
         * @return bool Whether the current user can manage Yoast SEO.
154
         */
155
        public function can_manage_seo(): bool {
4✔
156
                return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
4✔
157
        }
158

159
        /**
160
         * Checks whether the current user can read scores.
161
         *
162
         * @deprecated 28.2
163
         * @codeCoverageIgnore Because of deprecation.
164
         *
165
         * @return bool Whether the current user can read scores.
166
         */
167
        public function can_read_scores(): bool {
168
                \_deprecated_function( __METHOD__, 'Yoast SEO 28.2', 'Use can_manage_seo() instead.' );
169

170
                return $this->can_manage_seo();
171
        }
172

173
        /**
174
         * Registers the SEO scores ability.
175
         *
176
         * @return void
177
         */
178
        private function register_seo_scores_ability(): void {
×
179
                $output_schema                                  = $this->get_score_output_schema();
×
180
                $output_schema['properties']['focus_keyphrase'] = [
×
181
                        'type'        => [ 'string', 'null' ],
×
182
                        'description' => \__( 'The focus keyphrase for the post, or null if not set.', 'wordpress-seo' ),
×
183
                ];
×
184

185
                \wp_register_ability(
×
186
                        Ability_Categories_Integration::CATEGORY_SLUG . '/get-seo-scores',
×
187
                        $this->get_shared_ability_args(
×
188
                                [
×
189
                                        'label'            => \__( 'Get SEO Scores', 'wordpress-seo' ),
×
190
                                        'description'      => \__( 'Get the SEO scores for the most recently modified posts.', 'wordpress-seo' ),
×
191
                                        'output_schema'    => $this->wrap_in_array_schema( $output_schema ),
×
192
                                        'execute_callback' => [ $this->score_retriever, 'get_seo_scores' ],
×
193
                                ],
×
194
                        ),
×
195
                );
×
196
        }
197

198
        /**
199
         * Registers the readability scores ability.
200
         *
201
         * @return void
202
         */
203
        private function register_readability_scores_ability(): void {
×
204
                \wp_register_ability(
×
205
                        Ability_Categories_Integration::CATEGORY_SLUG . '/get-readability-scores',
×
206
                        $this->get_shared_ability_args(
×
207
                                [
×
208
                                        'label'            => \__( 'Get Readability Scores', 'wordpress-seo' ),
×
209
                                        'description'      => \__( 'Get the readability scores for the most recently modified posts.', 'wordpress-seo' ),
×
210
                                        'output_schema'    => $this->wrap_in_array_schema( $this->get_score_output_schema() ),
×
211
                                        'execute_callback' => [ $this->score_retriever, 'get_readability_scores' ],
×
212
                                ],
×
213
                        ),
×
214
                );
×
215
        }
216

217
        /**
218
         * Registers the inclusive language scores ability.
219
         *
220
         * @return void
221
         */
222
        private function register_inclusive_language_scores_ability(): void {
×
223
                \wp_register_ability(
×
224
                        Ability_Categories_Integration::CATEGORY_SLUG . '/get-inclusive-language-scores',
×
225
                        $this->get_shared_ability_args(
×
226
                                [
×
227
                                        'label'            => \__( 'Get Inclusive Language Scores', 'wordpress-seo' ),
×
228
                                        'description'      => \__( 'Get the inclusive language scores for the most recently modified posts.', 'wordpress-seo' ),
×
229
                                        'output_schema'    => $this->wrap_in_array_schema( $this->get_score_output_schema() ),
×
230
                                        'execute_callback' => [ $this->score_retriever, 'get_inclusive_language_scores' ],
×
231
                                ],
×
232
                        ),
×
233
                );
×
234
        }
235

236
        /**
237
         * Registers the get post SEO data ability.
238
         *
239
         * @return void
240
         */
241
        private function register_get_post_seo_data_ability(): void {
2✔
242
                \wp_register_ability(
2✔
243
                        Ability_Categories_Integration::CATEGORY_SLUG . '/get-post-seo-data',
2✔
244
                        $this->get_shared_ability_args(
2✔
245
                                [
2✔
246
                                        'label'               => \__( 'Get Post SEO Data', 'wordpress-seo' ),
2✔
247
                                        'description'         => \__( 'Get the SEO data for a post. Identify the post by post_id, by permalink (URL), or by title keywords; the title may be a comma-separated list and returns the SEO data for every post matching any of the values, paginated most recently modified first (use the page parameter to reach older matches). At least one identifier is required.', 'wordpress-seo' ),
2✔
248
                                        'input_schema'        => $this->get_post_identifier_input_schema(),
2✔
249
                                        'output_schema'       => $this->wrap_in_array_schema( $this->get_post_seo_data_output_schema() ),
2✔
250
                                        'execute_callback'    => [ $this->post_seo_data_collector, 'get_post_seo_data' ],
2✔
251
                                ],
2✔
252
                        ),
2✔
253
                );
2✔
254
        }
255

256
        /**
257
         * Registers the update post SEO data ability.
258
         *
259
         * @return void
260
         */
261
        private function register_update_post_seo_data_ability(): void {
6✔
262
                \wp_register_ability(
6✔
263
                        Ability_Categories_Integration::CATEGORY_SLUG . '/update-post-seo-data',
6✔
264
                        $this->get_shared_ability_args(
6✔
265
                                [
6✔
266
                                        'label'               => \__( 'Update Post SEO Data', 'wordpress-seo' ),
6✔
267
                                        'description'         => \__( 'Update the SEO data for a single post. Identify the post by post_id or by permalink (URL). Only the fields you provide are changed; a provided empty value clears that field.', 'wordpress-seo' ),
6✔
268
                                        'input_schema'        => $this->get_update_post_seo_data_input_schema(),
6✔
269
                                        'output_schema'       => $this->get_post_seo_data_output_schema(),
6✔
270
                                        'execute_callback'    => [ $this->post_seo_data_updater, 'update_post_seo_data' ],
6✔
271
                                        'meta'                => [
6✔
272
                                                'show_in_rest' => true,
6✔
273
                                                'annotations'  => [
6✔
274
                                                        'readonly'    => false,
6✔
275
                                                        'destructive' => false,
6✔
276
                                                        'idempotent'  => true,
6✔
277
                                                ],
6✔
278
                                                'mcp'          => [
6✔
279
                                                        'public' => true,
6✔
280
                                                ],
6✔
281
                                        ],
6✔
282
                                ],
6✔
283
                        ),
6✔
284
                );
6✔
285
        }
286

287
        /**
288
         * Registers the get posts with SEO issues ability.
289
         *
290
         * Registered regardless of the enabled analysis features, since the meta
291
         * description issue type depends on none of them; the score-based issue types
292
         * are only offered when their analysis runs on this site.
293
         *
294
         * @param array<string, bool> $enabled_features The enabled analysis features, keyed by feature name.
295
         *
296
         * @return void
297
         */
298
        private function register_get_posts_with_seo_issues_ability( array $enabled_features ): void {
4✔
299
                $issue_types = [];
4✔
300

301
                if ( $enabled_features[ Keyphrase_Analysis::NAME ] === true ) {
4✔
302
                        $issue_types[] = Posts_With_SEO_Issues_Retriever::ISSUE_TYPE_LOW_SEO_SCORE;
2✔
303
                }
304

305
                if ( $enabled_features[ Readability_Analysis::NAME ] === true ) {
4✔
306
                        $issue_types[] = Posts_With_SEO_Issues_Retriever::ISSUE_TYPE_LOW_READABILITY_SCORE;
2✔
307
                }
308

309
                $issue_types[] = Posts_With_SEO_Issues_Retriever::ISSUE_TYPE_DEFAULT_META_DESCRIPTION;
4✔
310

311
                \wp_register_ability(
4✔
312
                        Ability_Categories_Integration::CATEGORY_SLUG . '/get-posts-with-seo-issues',
4✔
313
                        $this->get_shared_ability_args(
4✔
314
                                [
4✔
315
                                        'label'            => \__( 'Get Posts With SEO Issues', 'wordpress-seo' ),
4✔
316
                                        'description'      => \__( 'Get published posts that have a known SEO issue of the given type: a low (not good) SEO score, a low (not good) readability score, or no custom meta description. Results are ordered most recently modified first and paginated; an empty result means there are no posts with the issue or no further pages.', 'wordpress-seo' ),
4✔
317
                                        'input_schema'     => $this->get_posts_with_seo_issues_input_schema( $issue_types ),
4✔
318
                                        'output_schema'    => $this->wrap_in_array_schema( $this->get_post_with_issue_output_schema() ),
4✔
319
                                        'execute_callback' => [ $this->posts_with_seo_issues_retriever, 'get_posts_with_seo_issues' ],
4✔
320
                                ],
4✔
321
                        ),
4✔
322
                );
4✔
323
        }
324

325
        // phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- Too complicated of a param declaration for this case.
326

327
        /**
328
         * Returns the shared ability arguments merged with ability-specific arguments.
329
         *
330
         * @param array<string, mixed> $ability_specific_args The ability-specific arguments.
331
         *
332
         * @return array<string, mixed> The merged ability arguments.
333
         */
334
        private function get_shared_ability_args( array $ability_specific_args ): array {
×
335
        // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
336
                return \array_merge(
×
337
                        [
×
338
                                'category'            => Ability_Categories_Integration::CATEGORY_SLUG,
×
339
                                'input_schema'        => [
×
340
                                        'type'                 => 'object',
×
341
                                        'additionalProperties' => false,
×
342
                                        'properties'           => [
×
343
                                                'number_of_posts' => [
×
344
                                                        'type'        => 'integer',
×
345
                                                        'description' => \__( 'The number of recently modified posts to retrieve scores for. Defaults to 10.', 'wordpress-seo' ),
×
346
                                                        'minimum'     => 1,
×
347
                                                        'maximum'     => 100,
×
348
                                                        'default'     => 10,
×
349
                                                ],
×
350
                                        ],
×
351
                                ],
×
352
                                'permission_callback' => [ $this, 'can_manage_seo' ],
×
353
                                'meta'                => [
×
354
                                        'show_in_rest' => true,
×
355
                                        'annotations'  => [
×
356
                                                'readonly'    => true,
×
357
                                                'destructive' => false,
×
358
                                                'idempotent'  => true,
×
359
                                        ],
×
360
                                        'mcp'          => [
×
361
                                                'public' => true,
×
362
                                        ],
×
363
                                ],
×
364
                        ],
×
365
                        $ability_specific_args,
×
366
                );
×
367
        }
368

369
        /**
370
         * Wraps an item schema in an array schema.
371
         *
372
         * @param array<string, array<string, string>> $item_schema The item schema.
373
         *
374
         * @return array<string, array<string, string>> The array schema.
375
         */
376
        private function wrap_in_array_schema( array $item_schema ): array {
×
377
                return [
×
378
                        'type'  => 'array',
×
379
                        'items' => $item_schema,
×
380
                ];
×
381
        }
382

383
        /**
384
         * Returns the score output schema, including the title property.
385
         *
386
         * @return array<string, array<string, string>> The score output schema.
387
         */
388
        private function get_score_output_schema(): array {
×
389
                return [
×
390
                        'type'       => 'object',
×
391
                        'properties' => [
×
392
                                'title' => [
×
393
                                        'type'        => 'string',
×
394
                                        'description' => \__( 'The post title.', 'wordpress-seo' ),
×
395
                                ],
×
396
                                'score' => [
×
397
                                        'type'        => 'string',
×
398
                                        'enum'        => [ 'na', 'bad', 'ok', 'good' ],
×
399
                                        'description' => \__( 'The score slug.', 'wordpress-seo' ),
×
400
                                ],
×
401
                                'label' => [
×
402
                                        'type'        => 'string',
×
403
                                        'description' => \__( 'A human-readable label for the score.', 'wordpress-seo' ),
×
404
                                ],
×
405
                        ],
×
406
                ];
×
407
        }
408

409
        // phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- The JSON schema arrays are heterogeneous by nature.
410

411
        /**
412
         * Returns the input schema for identifying a post (read path).
413
         *
414
         * @return array<string, mixed> The input schema.
415
         */
416
        private function get_post_identifier_input_schema(): array {
×
417
                return [
×
418
                        'type'                 => 'object',
×
419
                        'additionalProperties' => false,
×
420
                        'properties'           => [
×
421
                                'post_id'   => [
×
422
                                        'type'        => 'integer',
×
423
                                        'description' => \__( 'The ID of the post to retrieve.', 'wordpress-seo' ),
×
424
                                        'minimum'     => 1,
×
425
                                ],
×
426
                                'permalink' => [
×
427
                                        'type'        => 'string',
×
428
                                        'description' => \__( 'The permalink (URL) of the post to retrieve.', 'wordpress-seo' ),
×
429
                                ],
×
430
                                'title'     => [
×
431
                                        'type'        => 'string',
×
432
                                        'description' => \__( 'Keywords to search for in post titles. Provide a comma-separated list to search for several titles at once; each value is matched as a whole phrase against the post title, and a post matching any value is returned. At most 10 phrases are used per request; any beyond the first 10 are ignored. Results are paginated to 10 entities per page; see the page parameter.', 'wordpress-seo' ),
×
433
                                ],
×
434
                                'page'      => [
×
435
                                        'type'        => 'integer',
×
436
                                        'description' => \__( 'The page of title-search results to return, 1-based and defaulting to 1. Matches are ordered most recently modified first, so request a later page to reach older matches. An empty result means there are no further pages. Only applies to a title search.', 'wordpress-seo' ),
×
437
                                        'minimum'     => 1,
×
438
                                        'default'     => 1,
×
439
                                ],
×
440
                        ],
×
441
                ];
×
442
        }
443

444
        /**
445
         * Returns the input schema for looking up posts with an SEO issue.
446
         *
447
         * @param array<int, string> $issue_types The issue types on offer on this site.
448
         *
449
         * @return array<string, mixed> The input schema.
450
         */
451
        private function get_posts_with_seo_issues_input_schema( array $issue_types ): array {
4✔
452
                return [
4✔
453
                        'type'                 => 'object',
4✔
454
                        'additionalProperties' => false,
4✔
455
                        'required'             => [ 'issue_type' ],
4✔
456
                        'properties'           => [
4✔
457
                                'issue_type'      => [
4✔
458
                                        'type'        => 'string',
4✔
459
                                        'enum'        => $issue_types,
4✔
460
                                        'description' => \__( 'The SEO issue to look for. Only issue types whose analysis is enabled on this site are offered.', 'wordpress-seo' ),
4✔
461
                                ],
4✔
462
                                'post_type'       => [
4✔
463
                                        'type'        => 'string',
4✔
464
                                        'enum'        => Posts_With_SEO_Issues_Retriever::SUPPORTED_POST_TYPES,
4✔
465
                                        'default'     => 'post',
4✔
466
                                        'description' => \__( 'The post type to look in. Defaults to post.', 'wordpress-seo' ),
4✔
467
                                ],
4✔
468
                                'number_of_posts' => [
4✔
469
                                        'type'        => 'integer',
4✔
470
                                        'description' => \__( 'The number of posts to return per page. Defaults to 10.', 'wordpress-seo' ),
4✔
471
                                        'minimum'     => 1,
4✔
472
                                        'maximum'     => 100,
4✔
473
                                        'default'     => 10,
4✔
474
                                ],
4✔
475
                                'page'            => [
4✔
476
                                        'type'        => 'integer',
4✔
477
                                        'description' => \__( 'The page of results to return, 1-based and defaulting to 1. Posts are ordered most recently modified first, so request a later page to reach older posts. An empty result means there are no further pages.', 'wordpress-seo' ),
4✔
478
                                        'minimum'     => 1,
4✔
479
                                        'default'     => 1,
4✔
480
                                ],
4✔
481
                        ],
4✔
482
                ];
4✔
483
        }
484

485
        /**
486
         * Returns the output schema describing a post with an SEO issue.
487
         *
488
         * @return array<string, mixed> The output schema.
489
         */
490
        private function get_post_with_issue_output_schema(): array {
2✔
491
                return [
2✔
492
                        'type'       => 'object',
2✔
493
                        'properties' => [
2✔
494
                                'post_id' => [
2✔
495
                                        'type'        => 'integer',
2✔
496
                                        'description' => \__( 'The ID of the post.', 'wordpress-seo' ),
2✔
497
                                ],
2✔
498
                                'title'   => [
2✔
499
                                        'type'        => 'string',
2✔
500
                                        'description' => \__( 'The post title.', 'wordpress-seo' ),
2✔
501
                                ],
2✔
502
                        ],
2✔
503
                ];
2✔
504
        }
505

506
        /**
507
         * Returns the input schema for updating a post's SEO data (write path).
508
         *
509
         * @return array<string, mixed> The input schema.
510
         */
511
        private function get_update_post_seo_data_input_schema(): array {
2✔
512
                $nullable_string = [ 'type' => [ 'string', 'null' ] ];
2✔
513

514
                return [
2✔
515
                        'type'                 => 'object',
2✔
516
                        'additionalProperties' => false,
2✔
517
                        'properties'           => [
2✔
518
                                'post_id'                => [
2✔
519
                                        'type'        => 'integer',
2✔
520
                                        'description' => \__( 'The ID of the post to update.', 'wordpress-seo' ),
2✔
521
                                        'minimum'     => 1,
2✔
522
                                ],
2✔
523
                                'permalink'              => [
2✔
524
                                        'type'        => 'string',
2✔
525
                                        'description' => \__( 'The permalink (URL) of the post to update.', 'wordpress-seo' ),
2✔
526
                                ],
2✔
527
                                'seo_title'              => $nullable_string,
2✔
528
                                'meta_description'       => $nullable_string,
2✔
529
                                'focus_keyphrase'        => \array_merge( $nullable_string, [ 'maxLength' => 191 ] ),
2✔
530
                                'canonical'              => $nullable_string,
2✔
531
                                'is_cornerstone'         => [ 'type' => 'boolean' ],
2✔
532
                                'noindex'                => [
2✔
533
                                        'type'        => [ 'boolean', 'null' ],
2✔
534
                                        'description' => \__( 'Whether search engines should be told not to index this post. true sets noindex (the post is excluded from search results); false forces the post to be indexed; null clears the setting and falls back to the post-type default.', 'wordpress-seo' ),
2✔
535
                                ],
2✔
536
                                'nofollow'               => [ 'type' => 'boolean' ],
2✔
537
                                'noimageindex'           => [ 'type' => 'boolean' ],
2✔
538
                                'noarchive'              => [ 'type' => 'boolean' ],
2✔
539
                                'nosnippet'              => [ 'type' => 'boolean' ],
2✔
540
                                'open_graph_title'       => $nullable_string,
2✔
541
                                'open_graph_description' => $nullable_string,
2✔
542
                                'twitter_title'          => $nullable_string,
2✔
543
                                'twitter_description'    => $nullable_string,
2✔
544
                                'schema_page_type'       => $this->nullable_enum_schema(
2✔
545
                                        \array_keys( Schema_Types::PAGE_TYPES ),
2✔
546
                                        \__( 'The Schema.org page type for the post. Must be one of the supported page types. Use null to clear it and fall back to the default.', 'wordpress-seo' ),
2✔
547
                                ),
2✔
548
                                'schema_article_type'    => $this->nullable_enum_schema(
2✔
549
                                        $this->get_schema_article_types(),
2✔
550
                                        \__( 'The Schema.org article type for the post. Must be one of the supported article types. Use null to clear it and fall back to the default.', 'wordpress-seo' ),
2✔
551
                                ),
2✔
552
                        ],
2✔
553
                ];
2✔
554
        }
555

556
        /**
557
         * Returns the allowed Schema.org article type values.
558
         *
559
         * Mirrors the validation in WPSEO_Option_Titles so the ability accepts exactly the
560
         * article types the editor does, including any registered through the filter.
561
         *
562
         * @return array<int, string> The allowed article type values.
563
         */
564
        private function get_schema_article_types(): array {
×
565
                /**
566
                 * Filter: 'wpseo_schema_article_types' - Allow developers to filter the available article types.
567
                 *
568
                 * Make sure when you filter this to also filter `wpseo_schema_article_types_labels`.
569
                 *
570
                 * @param array $schema_article_types The available schema article types.
571
                 */
572
                return \array_keys( \apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ) );
×
573
        }
574

575
        /**
576
         * Returns a nullable-string input schema constrained to a fixed set of allowed values.
577
         *
578
         * Null and the empty string are always allowed on top of the enum so the field can be
579
         * cleared, matching the patch-clear semantics of the other write fields.
580
         *
581
         * @param array<int, string> $allowed_values The allowed string values.
582
         * @param string             $description    The field description.
583
         *
584
         * @return array<string, mixed> The input schema fragment.
585
         */
586
        private function nullable_enum_schema( array $allowed_values, string $description ): array {
×
587
                return [
×
588
                        'type'        => [ 'string', 'null' ],
×
589
                        'description' => $description,
×
590
                        'enum'        => \array_merge( $allowed_values, [ '', null ] ),
×
591
                ];
×
592
        }
593

594
        /**
595
         * Returns the output schema describing a post's SEO data.
596
         *
597
         * @return array<string, mixed> The output schema.
598
         */
599
        private function get_post_seo_data_output_schema(): array {
2✔
600
                $nullable_string = [
2✔
601
                        'type' => [ 'string', 'null' ],
2✔
602
                ];
2✔
603
                $score           = static function ( $analysis ) {
2✔
604
                        return [
2✔
605
                                'type'        => 'string',
2✔
606
                                'enum'        => [ 'na', 'bad', 'ok', 'good' ],
2✔
607
                                'description' => \sprintf(
2✔
608
                                        /* translators: %s expands to the name of the analysis, e.g. "SEO analysis". */
609
                                        \__( 'The result of the %s that ran on the post when it was last saved.', 'wordpress-seo' ),
2✔
610
                                        $analysis,
2✔
611
                                ),
2✔
612
                        ];
2✔
613
                };
2✔
614

615
                // The rendered companion of a field carries the value as actually output on the front end: the global default template applied where no custom value is set, with replacement variables expanded.
616
                $rendered = static function ( $field ) {
2✔
617
                        return [
2✔
618
                                'type'        => [ 'string', 'null' ],
2✔
619
                                'description' => \sprintf(
2✔
620
                                        /* translators: %s expands to the name of the SEO field, e.g. "SEO title". */
621
                                        \__( 'The %s as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output.', 'wordpress-seo' ),
2✔
622
                                        $field,
2✔
623
                                ),
2✔
624
                        ];
2✔
625
                };
2✔
626

627
                return [
2✔
628
                        'type'       => 'object',
2✔
629
                        'properties' => [
2✔
630
                                'post_id'                         => [ 'type' => 'integer' ],
2✔
631
                                'post_title'                      => $nullable_string,
2✔
632
                                'permalink'                       => $nullable_string,
2✔
633
                                'post_type'                       => [ 'type' => 'string' ],
2✔
634
                                'post_status'                     => $nullable_string,
2✔
635
                                'seo_title'                       => $nullable_string,
2✔
636
                                'seo_title_rendered'              => $rendered( \__( 'SEO title', 'wordpress-seo' ) ),
2✔
637
                                'meta_description'                => $nullable_string,
2✔
638
                                'meta_description_rendered'       => $rendered( \__( 'meta description', 'wordpress-seo' ) ),
2✔
639
                                'focus_keyphrase'                 => $nullable_string,
2✔
640
                                'canonical'                       => $nullable_string,
2✔
641
                                'canonical_rendered'              => $rendered( \__( 'canonical URL', 'wordpress-seo' ) ),
2✔
642
                                'is_cornerstone'                  => [ 'type' => 'boolean' ],
2✔
643
                                'noindex'                         => [
2✔
644
                                        'type'        => [ 'boolean', 'null' ],
2✔
645
                                        'description' => \__( 'Whether search engines are told not to index this post. true means noindex (the post is excluded from search results); false means the post is forced to be indexed; null means no setting is stored and the post-type default applies.', 'wordpress-seo' ),
2✔
646
                                ],
2✔
647
                                'nofollow'                        => [ 'type' => 'boolean' ],
2✔
648
                                'noimageindex'                    => [ 'type' => 'boolean' ],
2✔
649
                                'noarchive'                       => [ 'type' => 'boolean' ],
2✔
650
                                'nosnippet'                       => [ 'type' => 'boolean' ],
2✔
651
                                'open_graph_title'                => $nullable_string,
2✔
652
                                'open_graph_title_rendered'       => $rendered( \__( 'Open Graph title', 'wordpress-seo' ) ),
2✔
653
                                'open_graph_description'          => $nullable_string,
2✔
654
                                'open_graph_description_rendered' => $rendered( \__( 'Open Graph description', 'wordpress-seo' ) ),
2✔
655
                                'twitter_title'                   => $nullable_string,
2✔
656
                                'twitter_title_rendered'          => $rendered( \__( 'Twitter title', 'wordpress-seo' ) ),
2✔
657
                                'twitter_description'             => $nullable_string,
2✔
658
                                'twitter_description_rendered'    => $rendered( \__( 'Twitter description', 'wordpress-seo' ) ),
2✔
659
                                'schema_page_type'                => $nullable_string,
2✔
660
                                'schema_article_type'             => $nullable_string,
2✔
661
                                'seo_score'                       => $score( \__( 'SEO analysis', 'wordpress-seo' ) ),
2✔
662
                                'readability_score'               => $score( \__( 'readability analysis', 'wordpress-seo' ) ),
2✔
663
                                'inclusive_language_score'        => $score( \__( 'inclusive language analysis', 'wordpress-seo' ) ),
2✔
664
                        ],
2✔
665
                ];
2✔
666
        }
667

668
        // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
669
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc