PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.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 trunk 1.0.0 1.0.1 1.0.2 All 50 releases
thinkrank / includes / abilities / content / class-list-snippet-issues.php

class-list-snippet-issues.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.9.0, at includes/abilities/content/class-list-snippet-issues.php

224 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * List snippet issues ability.
4 *
5 * @package ThinkRank\Abilities\Content
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Content;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\API\Snippets_Endpoint;
14 use ThinkRank\SEO\Global_SEO_Post_Types;
15 use ThinkRank\SEO\Snippet_Issues;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Lists posts whose search snippet has a problem — the Bulk Snippets screen,
23 * for an agent (#727).
24 *
25 * Reads through the same collection as the screen, so an agent and a person
26 * looking at "empty description" see the same posts and the same counts.
27 */
28 class List_Snippet_Issues extends Ability_Base {
29 /**
30 * Constructor.
31 */
32 public function __construct() {
33 $this->id = 'thinkrank/list-snippet-issues';
34 $this->label = __( 'List ThinkRank Snippet Issues', 'thinkrank' );
35 $this->description = __( 'Find posts whose search snippet has a problem: no custom SEO title or meta description, a title or description outside the recommended length, no focus keyword, noindex, or a title another post also uses. Returns each post\'s stored values, the title and description the page actually renders, its issues, and a count per issue across the whole post type. Use list-content-types for post type slugs, then fix a post with update-post-seo.', 'thinkrank' );
36 }
37
38 /**
39 * {@inheritDoc}
40 *
41 * @return array<string, bool|float|string>
42 */
43 public function get_annotations() {
44 return [
45 'readonly' => true,
46 'destructive' => false,
47 'idempotent' => true,
48 'priority' => 1.0,
49 'openWorldHint' => false,
50 ];
51 }
52
53 /**
54 * {@inheritDoc}
55 *
56 * @return array<string, mixed>
57 */
58 public function get_input_schema() {
59 return [
60 'type' => 'object',
61 'additionalProperties' => false,
62 'properties' => [
63 'post_type' => [
64 'type' => 'string',
65 'description' => __( 'Post type slug, e.g. post, page or product. See list-content-types.', 'thinkrank' ),
66 ],
67 'issue' => [
68 'type' => 'string',
69 'enum' => array_merge( [ '' ], Snippet_Issues::all() ),
70 'default' => '',
71 'description' => __( 'Only return posts with this issue. Empty returns every post, each with its issues. empty_title and empty_description mean the post has no value of its own and renders its post type template; length issues are judged on the rendered value.', 'thinkrank' ),
72 ],
73 'status' => [
74 'type' => 'string',
75 'enum' => array_keys( Snippets_Endpoint::STATUS_SETS ),
76 'default' => 'publish',
77 'description' => __( 'publish = published only; draft = drafts, pending and scheduled; any = both plus private.', 'thinkrank' ),
78 ],
79 'search' => [
80 'type' => 'string',
81 'description' => __( 'Optional search within post titles and content.', 'thinkrank' ),
82 ],
83 'page' => [
84 'type' => 'integer',
85 'default' => 1,
86 'minimum' => 1,
87 ],
88 'per_page' => [
89 'type' => 'integer',
90 'default' => 20,
91 'minimum' => 1,
92 'maximum' => 100,
93 ],
94 ],
95 'required' => [ 'post_type' ],
96 ];
97 }
98
99 /**
100 * {@inheritDoc}
101 *
102 * @return array<string, mixed>
103 */
104 public function get_output_schema() {
105 return [
106 'type' => 'object',
107 'properties' => [
108 'post_type' => [ 'type' => 'string' ],
109 'issue' => [ 'type' => 'string' ],
110 'total' => [ 'type' => 'integer' ],
111 'total_pages' => [ 'type' => 'integer' ],
112 'page' => [ 'type' => 'integer' ],
113 'pending' => [
114 'type' => 'integer',
115 'description' => __( 'Posts not analysed yet. When above zero, counts are partial; call again to continue.', 'thinkrank' ),
116 ],
117 'counts' => [
118 'type' => 'object',
119 'description' => __( 'Posts per issue across the whole post type and status, regardless of the issue filter.', 'thinkrank' ),
120 ],
121 'items' => [
122 'type' => 'array',
123 'items' => [
124 'type' => 'object',
125 'properties' => [
126 'post_id' => [ 'type' => 'integer' ],
127 'post_title' => [ 'type' => 'string' ],
128 'status' => [ 'type' => 'string' ],
129 'title' => [ 'type' => 'string' ],
130 'description' => [ 'type' => 'string' ],
131 'focus_keyword' => [ 'type' => 'string' ],
132 'effective_title' => [ 'type' => 'string' ],
133 'effective_description' => [ 'type' => 'string' ],
134 'noindex' => [ 'type' => 'boolean' ],
135 'issues' => [
136 'type' => 'array',
137 'items' => [ 'type' => 'string' ],
138 ],
139 ],
140 ],
141 ],
142 ],
143 ];
144 }
145
146 /**
147 * Execute ability.
148 *
149 * @param array<string, mixed> $input Ability input payload.
150 * @return array<string, mixed>|\WP_Error
151 */
152 public function execute( $input ) {
153 $post_type = isset( $input['post_type'] ) ? sanitize_key( (string) $input['post_type'] ) : '';
154 $issue = isset( $input['issue'] ) ? sanitize_key( (string) $input['issue'] ) : '';
155 $status = isset( $input['status'] ) ? sanitize_key( (string) $input['status'] ) : 'publish';
156 $search = isset( $input['search'] ) ? sanitize_text_field( (string) $input['search'] ) : '';
157 $page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
158 $per_page = isset( $input['per_page'] ) ? max( 1, min( 100, (int) $input['per_page'] ) ) : 20;
159
160 if ( ! Global_SEO_Post_Types::is_allowed( $post_type ) ) {
161 return new \WP_Error(
162 'thinkrank_invalid_post_type',
163 __( 'That post type is not one ThinkRank manages. Use list-content-types for valid slugs.', 'thinkrank' ),
164 [ 'status' => 400 ]
165 );
166 }
167
168 if ( '' !== $issue && ! in_array( $issue, Snippet_Issues::all(), true ) ) {
169 return new \WP_Error(
170 'thinkrank_invalid_issue',
171 __( 'Unknown issue.', 'thinkrank' ),
172 [ 'status' => 400 ]
173 );
174 }
175
176 $result = Snippets_Endpoint::page(
177 [
178 'post_type' => $post_type,
179 'statuses' => Snippets_Endpoint::STATUS_SETS[ $status ] ?? Snippets_Endpoint::STATUS_SETS['publish'],
180 'issue' => $issue,
181 'search' => $search,
182 'page' => $page,
183 'per_page' => $per_page,
184 ]
185 );
186
187 $items = [];
188 foreach ( $result['snippets'] as $snippet ) {
189 $row = Snippets_Endpoint::present( $snippet );
190
191 // Only what an agent needs to decide and act; the screen's URLs and
192 // editing flags stay on the REST response.
193 $items[] = [
194 'post_id' => $row['post_id'],
195 'post_title' => $row['post_title'],
196 'status' => $row['status'],
197 'title' => $row['title'],
198 'description' => $row['description'],
199 'focus_keyword' => $row['focus_keyword'],
200 'effective_title' => $row['effective_title'],
201 'effective_description' => $row['effective_description'],
202 'noindex' => $row['noindex'],
203 'issues' => $row['issues'],
204 ];
205 }
206
207 $total = $result['total'];
208 $counts = $result['counts'];
209
210 return [
211 'post_type' => $post_type,
212 'issue' => $issue,
213 'total' => $total,
214 'total_pages' => (int) max( 1, ceil( $total / $per_page ) ),
215 'page' => $page,
216 'counts' => $counts,
217 // Posts not analysed yet; counts cover the rest. Call again to
218 // continue — each call analyses another batch.
219 'pending' => $result['pending'],
220 'items' => $items,
221 ];
222 }
223 }
224