PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.11.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.11.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 trunk 1.0.0 All 52 releases
thinkrank / includes / abilities / analysis / class-get-post-seo-checks.php

class-get-post-seo-checks.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.11.0, at includes/abilities/analysis/class-get-post-seo-checks.php

188 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Get post SEO checks ability.
4 *
5 * @package ThinkRank\Abilities\Analysis
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Analysis;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\Admin\Metabox_Manager;
14 use ThinkRank\AI\SEOScoreCalculator;
15 use ThinkRank\Core\Database;
16 use ThinkRank\SEO\Pattern_Resolver;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 /**
23 * Runs ThinkRank's SEO score analysis for one or more posts.
24 */
25 class Get_Post_Seo_Checks extends Ability_Base {
26 /**
27 * Constructor.
28 */
29 public function __construct() {
30 $this->id = 'thinkrank/get-post-seo-checks';
31 $this->label = __( 'Get ThinkRank Post SEO Checks', 'thinkrank' );
32 $this->description = __( 'Run ThinkRank SEO score analysis for one or more posts, pages, or custom post type items. Each score includes keywords: for every focus keyword, whether it appears in the SEO title, meta description, URL slug, first paragraph, a subheading (H2-H6), the body, and an image alt text, with the heading or alt text that matched - use it to see which keyword is missing where. Scores are not persisted; use bulk-analyze-and-save to score and store them.', 'thinkrank' );
33 }
34
35 /**
36 * {@inheritDoc}
37 *
38 * @return array<string, bool|float|string>
39 */
40 public function get_annotations() {
41 return [
42 'readonly' => true,
43 'destructive' => false,
44 'idempotent' => true,
45 'priority' => 1.0,
46 'openWorldHint' => false,
47 ];
48 }
49
50 /**
51 * {@inheritDoc}
52 *
53 * @return array<string, mixed>
54 */
55 public function get_input_schema() {
56 return [
57 'type' => 'object',
58 'additionalProperties' => false,
59 'properties' => [
60 'post_ids' => [
61 'type' => 'array',
62 'description' => __( 'One or more post IDs to inspect.', 'thinkrank' ),
63 'items' => [
64 'type' => 'integer',
65 ],
66 ],
67 ],
68 'required' => [ 'post_ids' ],
69 ];
70 }
71
72 /**
73 * {@inheritDoc}
74 *
75 * @return array<string, mixed>
76 */
77 public function get_output_schema() {
78 return [
79 'type' => 'object',
80 'properties' => [
81 'status' => [ 'type' => 'string' ],
82 'message' => [ 'type' => 'string' ],
83 'data' => [
84 'type' => 'array',
85 'items' => [
86 'type' => 'object',
87 'properties' => [
88 'post_id' => [ 'type' => 'integer' ],
89 'error' => [ 'type' => 'string' ],
90 'score' => [
91 'type' => 'object',
92 'properties' => [
93 'overall_score' => [ 'type' => 'integer' ],
94 'grade' => [ 'type' => 'string' ],
95 'keywords' => [
96 'type' => 'array',
97 'items' => [
98 'type' => 'object',
99 'properties' => [
100 'keyword' => [ 'type' => 'string' ],
101 'passed' => [
102 'type' => 'integer',
103 'description' => __( 'Placements the keyword appears in.', 'thinkrank' ),
104 ],
105 'total' => [ 'type' => 'integer' ],
106 'placements' => [
107 'type' => 'object',
108 'description' => sprintf(
109 /* translators: %s: comma-separated placement names. */
110 __( 'One entry per placement (%s), each {passed, where}; where is the matching heading or alt text.', 'thinkrank' ),
111 implode( ', ', SEOScoreCalculator::PLACEMENTS )
112 ),
113 ],
114 ],
115 ],
116 ],
117 ],
118 ],
119 ],
120 ],
121 ],
122 ],
123 ];
124 }
125
126 /**
127 * Execute ability.
128 *
129 * @param array<string, mixed> $input Ability input payload.
130 * @return array<string, mixed>|\WP_Error
131 */
132 public function execute( $input ) {
133 $post_ids = isset( $input['post_ids'] ) && is_array( $input['post_ids'] ) ? array_map( 'absint', $input['post_ids'] ) : [];
134 $post_ids = array_values(
135 array_filter(
136 $post_ids,
137 static function ( $post_id ) {
138 return $post_id > 0 && get_post( $post_id ) instanceof \WP_Post;
139 }
140 )
141 );
142
143 if ( empty( $post_ids ) ) {
144 return new \WP_Error(
145 'thinkrank_missing_post_ids',
146 __( 'At least one valid post ID is required.', 'thinkrank' ),
147 [ 'status' => 400 ]
148 );
149 }
150
151 $calculator = new SEOScoreCalculator( new Database() );
152 $metabox = new Metabox_Manager();
153 $data = [];
154
155 foreach ( $post_ids as $post_id ) {
156 try {
157 $content = $calculator->analyze_post_content( $post_id );
158
159 // Override the raw per-post title/description with their effective
160 // values (custom value, else the resolved Global pattern) so an
161 // inherited title or description is scored as present — matching
162 // the editor and frontend — instead of counting as missing.
163 $metadata = $metabox->get_post_metadata( $post_id );
164 $metadata['title'] = Pattern_Resolver::effective_title( $post_id );
165 $metadata['description'] = Pattern_Resolver::effective_description( $post_id );
166
167 $score = $calculator->calculate_score( $content, $metadata );
168
169 $data[] = [
170 'post_id' => $post_id,
171 'score' => $score,
172 ];
173 } catch ( \Throwable $e ) {
174 $data[] = [
175 'post_id' => $post_id,
176 'error' => $e->getMessage(),
177 ];
178 }
179 }
180
181 return [
182 'status' => 'success',
183 'message' => __( 'SEO checks generated.', 'thinkrank' ),
184 'data' => $data,
185 ];
186 }
187 }
188