PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.27.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.27.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 1.1.0 1.10.0 All 48 releases
thinkrank / includes / abilities / analysis / class-get-seo-score.php

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

154 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Get SEO score 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\AI\SEOScoreCalculator;
14 use ThinkRank\Core\Database;
15 use ThinkRank\SEO\Focus_Keywords;
16 use ThinkRank\SEO\Pattern_Resolver;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 /**
23 * Calculates the ThinkRank SEO score for a single post.
24 */
25 class Get_Seo_Score extends Ability_Base {
26 /**
27 * Constructor.
28 */
29 public function __construct() {
30 $this->id = 'thinkrank/get-seo-score';
31 $this->label = __( 'Get ThinkRank SEO Score', 'thinkrank' );
32 $this->description = __( 'Calculate the ThinkRank SEO score for a single post, analyzing its content against its configured title and meta description.', 'thinkrank' );
33 }
34
35 /**
36 * {@inheritDoc}
37 *
38 * @return array<string, bool|float|string>
39 */
40 public function get_annotations() {
41 return [
42 // Not strictly read-only: with save_score the result is persisted.
43 'readonly' => false,
44 'destructive' => false,
45 'idempotent' => true,
46 'priority' => 1.0,
47 'openWorldHint' => false,
48 ];
49 }
50
51 /**
52 * {@inheritDoc}
53 *
54 * @return array<string, mixed>
55 */
56 public function get_input_schema() {
57 return [
58 'type' => 'object',
59 'additionalProperties' => false,
60 'properties' => [
61 'post_id' => [
62 'type' => 'integer',
63 'description' => __( 'The ID of the post to score.', 'thinkrank' ),
64 ],
65 'save_score' => [
66 'type' => 'boolean',
67 'description' => __( 'Persist the calculated score so it appears in the post list SEO column and score history. Defaults to false, which only reports the score.', 'thinkrank' ),
68 ],
69 ],
70 'required' => [ 'post_id' ],
71 ];
72 }
73
74 /**
75 * {@inheritDoc}
76 *
77 * @return array<string, mixed>
78 */
79 public function get_output_schema() {
80 return [
81 'type' => 'object',
82 'properties' => [
83 'post_id' => [ 'type' => 'integer' ],
84 'score' => [
85 'type' => 'object',
86 'additionalProperties' => true,
87 ],
88 'saved' => [ 'type' => 'boolean' ],
89 'score_id' => [ 'type' => [ 'integer', 'null' ] ],
90 ],
91 ];
92 }
93
94 /**
95 * Execute ability.
96 *
97 * @param array<string, mixed> $input Ability input payload.
98 * @return array<string, mixed>|\WP_Error
99 */
100 public function execute( $input ) {
101 $post_id = isset( $input['post_id'] ) ? (int) $input['post_id'] : 0;
102
103 if ( $post_id <= 0 || ! get_post( $post_id ) ) {
104 return new \WP_Error(
105 'thinkrank_invalid_post_id',
106 __( 'A valid post ID is required.', 'thinkrank' ),
107 [ 'status' => 400 ]
108 );
109 }
110
111 $calc = new SEOScoreCalculator( new Database() );
112 $content = $calc->analyze_post_content( $post_id );
113
114 if ( empty( $content ) ) {
115 return new \WP_Error(
116 'thinkrank_post_content_unavailable',
117 __( 'Unable to analyze the content for this post.', 'thinkrank' ),
118 [ 'status' => 404 ]
119 );
120 }
121
122 // Carry the stored focus keywords so keyword-dependent factors are
123 // actually measured instead of scoring as "no focus keyword set".
124 // Score against the FINAL effective title/description (custom value, else
125 // the resolved Global pattern), so a post that inherits its title or
126 // description from a global pattern scores the same here as in the editor
127 // and on the frontend — not as if those fields were missing.
128 $metadata = [
129 'title' => Pattern_Resolver::effective_title( $post_id ),
130 'description' => Pattern_Resolver::effective_description( $post_id ),
131 'focus_keywords' => Focus_Keywords::get( $post_id ),
132 ];
133
134 $score = $calc->calculate_score( $content, $metadata );
135
136 // Without this the score was calculated and thrown away, so the post
137 // list kept reporting "Not Analyzed" until a separate saved run
138 // happened. Opt-in so a plain read stays a plain read.
139 $saved = false;
140 $score_id = null;
141 if ( ! empty( $input['save_score'] ) ) {
142 $score_id = $calc->save_score( $post_id, get_current_user_id(), $score );
143 $saved = false !== $score_id;
144 }
145
146 return [
147 'post_id' => $post_id,
148 'score' => $score,
149 'saved' => $saved,
150 'score_id' => $saved ? (int) $score_id : null,
151 ];
152 }
153 }
154