PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.2 2.14.1 2.14.0 2.13.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 All 57 releases
← All changes | includes/seo/class-instant-indexing-manager.php +100 -16 2.4.0 → 2.14.1 View file →
@@ -53,8 +53,16 @@
53 53 */
54 54 public const MAX_URLS_PER_SUBMISSION = 100;
55 55
56 56 /**
57 + * Posts that transitioned to publish in this request and await the end of
58 + * their save, keyed by ID, with what was true of them before it.
59 + *
60 + * @var array<int, array{was_published: bool, was_excluded: bool}>
61 + */
62 + private array $pending_transitions = [];
63 +
64 + /**
57 65 * Initialize the component
58 66 *
59 67 * @since 1.1.0
60 68 * @return void
@@ -60,8 +68,9 @@
60 68 * @return void
61 69 */
62 70 public function init(): void {
63 71 add_action('transition_post_status', [$this, 'handle_post_transition'], 10, 3);
72 + add_action('wp_after_insert_post', [$this, 'handle_after_insert_post'], 10, 4);
64 73 add_action('delete_post', [$this, 'handle_post_deletion'], 10, 2);
65 74
66 75 // Serve the IndexNow key file from PHP when no physical file exists.
67 76 // The key is normally written to the WordPress root, but on managed and
@@ -150,9 +159,11 @@
150 159 * @param string $api_key Verification key.
151 160 * @return string Absolute key file URL.
152 161 */
153 162 public static function key_location(string $api_key): string {
154 - return home_url('/' . $api_key . '.txt');
163 + // Matches the scheme the submitted URLs go out with, so IndexNow is
164 + // never told to verify ownership at an address on the other scheme.
165 + return Url_Scheme::apply(home_url('/' . $api_key . '.txt'));
155 166 }
156 167
157 168 /**
158 169 * Actively verify that the advertised keyLocation is reachable and returns
@@ -247,9 +258,9 @@
247 258 }
248 259
249 260 return sprintf(
250 261 /* translators: %d: HTTP status code returned by the key URL. */
251 - __('The key file could not be verified (HTTP %d). Open it in a browser — it should show the key and nothing else.', 'thinkrank'),
262 + __('The key file could not be verified (HTTP %d). Open it in a browser: it should show the key and nothing else.', 'thinkrank'),
252 263 $code
253 264 );
254 265 }
255 266
@@ -379,23 +390,84 @@
379 390 if (!$this->should_process_post($post)) {
380 391 return;
381 392 }
382 393
383 - // We only care if the new status is publish (created or updated)
384 - // OR if we are unpublishing (publish -> something else), we might want to update (though IndexNow is mostly for crawling new/updated content)
385 - // For now, let's focus on published content.
386 - if ($new_status === 'publish') {
387 - $url = get_permalink($post->ID);
394 + // Only content that is (still) published is submitted.
395 + if ($new_status !== 'publish') {
396 + return;
397 + }
388 398
389 - // Check for duplicate submission using short-lived cache (15 seconds)
390 - if ($this->is_recently_submitted_cache($url)) {
391 - return;
392 - }
399 + // Whether to submit is decided in handle_after_insert_post(), not
400 + // here. This hook fires inside wp_insert_post() before the post's meta
401 + // is written: the block editor saves the robots override through
402 + // REST after the insert, the classic metabox on save_post. Deciding
403 + // here read the previous robots value, so a post published with
404 + // noindex was submitted (#911).
405 + //
406 + // What this hook can still see is the previous state, which the
407 + // decision needs: a post that was an indexable destination and is no
408 + // longer one is submitted once more, so engines recrawl it and drop it.
409 + $this->pending_transitions[(int) $post->ID] = [
410 + 'was_published' => $old_status === 'publish',
411 + 'was_excluded' => Indexability::is_post_noindexed($post) || Indexability::is_post_redirected($post),
412 + ];
413 + }
393 414
394 - // Defer the outbound IndexNow call to WP-Cron so publishing doesn't
395 - // block on a third-party HTTP request.
396 - $this->schedule_url_submission([$url]);
415 + /**
416 + * Submit a published post once its save is complete.
417 + *
418 + * Runs on `wp_after_insert_post`, which fires after meta and terms are
419 + * saved on every path (classic editor, REST, Quick Edit, WP-CLI, and the
420 + * scheduled-post publish in wp_publish_post()).
421 + *
422 + * A post that is not an indexable destination (noindexed by its own
423 + * override or its type, password-protected, or redirected) is skipped,
424 + * unless it was one before this save: that transition is submitted so
425 + * engines recrawl the page and drop it.
426 + *
427 + * @since 2.15.0
428 + *
429 + * @param int $post_id Post ID.
430 + * @param \WP_Post $post Post object after the save.
431 + * @param bool $update Whether this was an update.
432 + * @param \WP_Post|null $post_before Post object before the save, null for a new post.
433 + * @return void
434 + */
435 + public function handle_after_insert_post(int $post_id, \WP_Post $post, bool $update, ?\WP_Post $post_before): void {
436 + if (!isset($this->pending_transitions[$post_id])) {
437 + return;
397 438 }
439 +
440 + $before = $this->pending_transitions[$post_id];
441 + unset($this->pending_transitions[$post_id]);
442 +
443 + if ($post->post_status !== 'publish') {
444 + return;
445 + }
446 +
447 + $was_indexable = $before['was_published']
448 + && !$before['was_excluded']
449 + && !($post_before instanceof \WP_Post && Indexability::is_password_protected($post_before));
450 +
451 + // Decided before the dedupe check below, which claims its 15-second
452 + // slot as a side effect: a skipped post must not occupy it.
453 + if (!Indexability::is_indexable_post($post) && !$was_indexable) {
454 + return;
455 + }
456 +
457 + $url = get_permalink($post->ID);
458 + if (!$url) {
459 + return;
460 + }
461 +
462 + // Check for duplicate submission using short-lived cache (15 seconds)
463 + if ($this->is_recently_submitted_cache($url)) {
464 + return;
465 + }
466 +
467 + // Defer the outbound IndexNow call to WP-Cron so publishing doesn't
468 + // block on a third-party HTTP request.
469 + $this->schedule_url_submission([$url]);
398 470 }
399 471
400 472 /**
401 473 * Check if URL was recently submitted using transient cache
@@ -467,9 +539,9 @@
467 539 return false;
468 540 }
469 541
470 542 // 3. Check autosave/revision
471 - if (wp_is_post_autosave($post) || wp_is_post_revision($post)) {
543 + if (wp_is_post_autosave((int) $post->ID) || wp_is_post_revision((int) $post->ID)) {
472 544 return false;
473 545 }
474 546
475 547 return true;
@@ -523,8 +595,20 @@
523 595 if (empty($urls)) {
524 596 return ['success' => false, 'message' => 'No URLs matched this site host', 'submitted_count' => 0];
525 597 }
526 598
599 + // Every submission path lands here, so this is where the site's scheme
600 + // preference is applied (#638). Submitting http URLs for a site served
601 + // over https asks search engines to index an address that redirects,
602 + // and it is the canonical mismatch all over again in the one place a
603 + // site owner cannot see it happening.
604 + $urls = array_values(array_unique(array_map(
605 + static function ($u): string {
606 + return Url_Scheme::apply((string) $u);
607 + },
608 + $urls
609 + )));
610 +
527 611 // Enforce the shared per-submission cap so every path (manual, bulk, MCP)
528 612 // behaves consistently.
529 613 if (count($urls) > self::MAX_URLS_PER_SUBMISSION) {
530 614 $urls = array_slice($urls, 0, self::MAX_URLS_PER_SUBMISSION);
@@ -573,9 +657,9 @@
573 657 // actually means, and name the URL to check.
574 658 if ($response_code === 403) {
575 659 $response_message = sprintf(
576 660 /* translators: %s: public URL of the IndexNow key file. */
577 - __('Key file could not be verified. Search engines must be able to read your key at %s — open it in a browser: it should show the key and nothing else.', 'thinkrank'),
661 + __('Key file could not be verified. Search engines must be able to read your key at %s. Open it in a browser: it should show the key and nothing else.', 'thinkrank'),
578 662 $key_location
579 663 );
580 664 }
581 665 }