PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.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 trunk All 53 releases
← All changes | includes/api/class-seo-score-endpoint.php +56 -3 2.10.0 → 2.12.0 View file →
@@ -300,14 +300,25 @@
300 300 // applied over the correct server-side analysis this request had just
301 301 // run, then persisted by save_score(). That is what put "No content"
302 302 // on the dashboard for a page with a valid score (#778). With nothing
303 303 // measured, the analysis above is the better answer.
304 + //
305 + // Non-empty `live_content` is not on its own proof that the editor
306 + // measured anything: builder markup (Divi 5) is markup the editor
307 + // has plenty of and can read no words from, so it sent that same
308 + // "No content" pair alongside 450 characters of block comments.
309 + // Treat the sentinel itself as "nothing measured" whenever this
310 + // request's own analysis did find words, so a stale bundle cannot
311 + // overwrite a valid analysis either.
304 312 $editor_measured = '' !== trim((string) $live_content);
313 + $server_found_content = ($content_data['word_count'] ?? 0) > 0;
305 314
306 - if ($editor_measured && null !== $readability_score) {
315 + if ($editor_measured && null !== $readability_score
316 + && !($server_found_content && self::is_no_content_label($readability_score))) {
307 317 $score_data['readability_score'] = $readability_score;
308 318 }
309 - if ($editor_measured && null !== $content_quality) {
319 + if ($editor_measured && null !== $content_quality
320 + && !($server_found_content && self::is_no_content_label($content_quality))) {
310 321 $score_data['content_quality'] = $content_quality;
311 322 }
312 323
313 324 // Save score if requested
@@ -334,8 +345,25 @@
334 345 }
335 346 }
336 347
337 348 /**
349 + * Whether an editor-supplied label means "I could not read the content".
350 + *
351 + * `calculateReadabilityScore()` and `calculateContentQuality()` both return
352 + * this exact untranslated string when handed an empty body, so it is the
353 + * editor saying it measured nothing rather than a measurement in its own
354 + * right. Every real label carries a level or a grade.
355 + *
356 + * @since 2.12.0
357 + *
358 + * @param mixed $label Label sent by the editor.
359 + * @return bool True when the label is the empty-content sentinel.
360 + */
361 + private static function is_no_content_label($label): bool {
362 + return is_string($label) && 'no content' === strtolower(trim($label));
363 + }
364 +
365 + /**
338 366 * Get existing SEO score for a post
339 367 *
340 368 * @param WP_REST_Request $request Request object
341 369 * @return WP_REST_Response|WP_Error Response object
@@ -347,8 +375,10 @@
347 375 // Get existing score data from database
348 376 $existing_data = $this->calculator->get_existing_score_data($post_id);
349 377
350 378 if ($existing_data) {
379 + $existing_data['keywords'] = $this->keyword_placements((int) $post_id);
380 +
351 381 return new WP_REST_Response([
352 382 'success' => true,
353 383 'data' => $existing_data,
354 384 'message' => __('Existing SEO score retrieved successfully', 'thinkrank')
@@ -366,8 +396,27 @@
366 396 }
367 397 }
368 398
369 399 /**
400 + * Where each focus keyword sits (#729), for a stored score. A stored score
401 + * predates the editor's current keywords and content, so this is read
402 + * from the post as saved now — a text pass, no request, no AI.
403 + *
404 + * @param int $post_id Post ID.
405 + * @return array<int, array<string, mixed>>
406 + */
407 + private function keyword_placements(int $post_id): array {
408 + return $this->calculator->keyword_placements(
409 + $this->calculator->analyze_post_content($post_id),
410 + [
411 + 'title' => \ThinkRank\SEO\Pattern_Resolver::effective_title($post_id),
412 + 'description' => \ThinkRank\SEO\Pattern_Resolver::effective_description($post_id),
413 + ],
414 + \ThinkRank\SEO\Focus_Keywords::get($post_id)
415 + );
416 + }
417 +
418 + /**
370 419 * Get score history for a post
371 420 *
372 421 * @param WP_REST_Request $request Request object
373 422 * @return WP_REST_Response|WP_Error Response object
@@ -402,10 +451,14 @@
402 451 public function get_latest_score(WP_REST_Request $request) {
403 452 try {
404 453 $post_id = $request->get_param('post_id');
405 454
455 + // The stored row only — no keyword gauges (#729). The dashboard
456 + // asks for five posts at once and reads overall_score; a gauge
457 + // pass would analyse and render each whole post for nothing. The
458 + // editor reads its gauges from /seo-score/get.
406 459 $latest_score = $this->calculator->get_latest_score($post_id);
407 -
460 +
408 461 return new WP_REST_Response([
409 462 'success' => true,
410 463 'data' => $latest_score,
411 464 ], 200);