| @@ -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 |
| @@ -381,23 +390,84 @@ | ||
| 381 | 390 | if (!$this->should_process_post($post)) { |
| 382 | 391 | return; |
| 383 | 392 | } |
| 384 | 393 | |
| 385 | - // We only care if the new status is publish (created or updated) | |
| 386 | - // OR if we are unpublishing (publish -> something else), we might want to update (though IndexNow is mostly for crawling new/updated content) | |
| 387 | - // For now, let's focus on published content. | |
| 388 | - if ($new_status === 'publish') { | |
| 389 | - $url = get_permalink($post->ID); | |
| 394 | + // Only content that is (still) published is submitted. | |
| 395 | + if ($new_status !== 'publish') { | |
| 396 | + return; | |
| 397 | + } | |
| 390 | 398 | |
| 391 | - // Check for duplicate submission using short-lived cache (15 seconds) | |
| 392 | - if ($this->is_recently_submitted_cache($url)) { | |
| 393 | - return; | |
| 394 | - } | |
| 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 | + } | |
| 395 | 414 | |
| 396 | - // Defer the outbound IndexNow call to WP-Cron so publishing doesn't | |
| 397 | - // block on a third-party HTTP request. | |
| 398 | - $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; | |
| 399 | 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]); | |
| 400 | 470 | } |
| 401 | 471 | |
| 402 | 472 | /** |
| 403 | 473 | * Check if URL was recently submitted using transient cache |
| @@ -469,9 +539,9 @@ | ||
| 469 | 539 | return false; |
| 470 | 540 | } |
| 471 | 541 | |
| 472 | 542 | // 3. Check autosave/revision |
| 473 | - 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)) { | |
| 474 | 544 | return false; |
| 475 | 545 | } |
| 476 | 546 | |
| 477 | 547 | return true; |