diff --git a/inc/namespace.php b/inc/namespace.php index 0ea670d..444758d 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -253,6 +253,24 @@ function get_post_ping_urls( $post ) { return $ping_urls; } +/** + * Get the last URL pinged for the post. + * + * This returns the most recently pinged URL from the post meta + * for sending a deindexing request to IndexNow. + * + * @param \WP_Post|int $post The post ID or object. + * @return string|null The last URL pinged, or null if none found. + */ +function get_last_post_ping_url( $post ) { + $urls = get_post_ping_urls( $post ); + if ( empty( $urls ) || ! is_array( $urls ) ) { + return null; + } + + return array_pop( $urls ); +} + /** * Add a URL or URLs to the post's ping URLs. * @@ -269,15 +287,18 @@ function add_post_ping_urls( $post, $urls ) { return; } - $ping_urls = get_post_ping_urls( $post ); + $ping_urls = array_values( get_post_ping_urls( $post ) ); $old_ping_urls = $ping_urls; $urls = (array) $urls; - $ping_urls = array_merge( $ping_urls, $urls ); - $ping_urls = array_unique( $ping_urls ); - sort( $ping_urls ); + + // Remove new $urls from $ping_urls to prevent duplicates. + $ping_urls = array_diff( $ping_urls, $urls ); + + // Append new URLs. + $ping_urls = array_merge( array_values( $ping_urls ), array_values( $urls ) ); // If the URLs haven't changed, do nothing. - if ( empty( array_diff( $ping_urls, $old_ping_urls ) ) ) { + if ( $old_ping_urls === $ping_urls ) { return; } @@ -296,7 +317,11 @@ function ping_indexnow( $post ) { return; } - $url_list = get_post_ping_urls( $post ); + $previous_url = get_last_post_ping_url( $post ); + $url_list = array(); + if ( $previous_url ) { + $url_list[] = $previous_url; + } $current_url = get_permalink( $post ); /* diff --git a/readme.md b/readme.md index 3f4e1df..da26318 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,9 @@ Crawl requests will be sent to search engines when: * new content is published * existing content is updated * existing content is unpublished (this encourages de-indexing) +* the post slug changes (both the old and new are sent index the redirect URL) + +De-indexed and redirect URLs are only sent to IndexNow once. The biggest features of this plugin are the features that it's missing: diff --git a/readme.txt b/readme.txt index 7db8cec..ffea90e 100644 --- a/readme.txt +++ b/readme.txt @@ -21,6 +21,9 @@ Crawl requests will be sent to search engines when: * new content is published * existing content is updated * existing content is unpublished (this encourages de-indexing) +* the post slug changes (both the old and new are sent index the redirect URL) + +De-indexed and redirect URLs are only sent to IndexNow once. The biggest features of this plugin are the features that it's missing: diff --git a/tests/class-test-indexnow-pings.php b/tests/class-test-indexnow-pings.php index 7263277..5decfaa 100644 --- a/tests/class-test-indexnow-pings.php +++ b/tests/class-test-indexnow-pings.php @@ -7,6 +7,7 @@ namespace PWCC\SimpleSearchSubmission\Tests; +use PWCC\SimpleSearchSubmission; use WP_UnitTestCase; use WP_UnitTest_Factory; @@ -242,4 +243,172 @@ public function test_no_ping_on_custom_private_post_status() { unset( $wp_post_statuses['custom_private'] ); $this->assertNotPing( get_permalink( $post_id ), 'Custom private post status should not ping.' ); } + + /** + * Ensure de-indexed URLs are pinged once only. + */ + public function test_no_duplicate_pings_on_deindexed_url() { + $post_id = self::$post_ids['publish']; + + // Update the post slug. + wp_update_post( + array( + 'ID' => $post_id, + 'post_name' => 'updated-test-post-publish', + ) + ); + + // Ensure both old and new URLs are pinged. + $this->assertPing( home_url( '/2025/test-post-publish/' ), 'Ping should include the old post URL after slug change.' ); + $this->assertPing( home_url( '/2025/updated-test-post-publish/' ), 'Ping should include the new post URL after slug change.' ); + // Ensure last ping URL is the updated URL. + $last_ping_url = SimpleSearchSubmission\get_last_post_ping_url( $post_id ); + $this->assertSame( home_url( '/2025/updated-test-post-publish/' ), $last_ping_url, 'Last ping URL should be the updated URL.' ); + + // Clear the ping list. + $this->pings = array(); + + // Update the post content. + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Another update to Test Post', + ) + ); + + // Ensure only the new URL is pinged again. + $this->assertNotPing( home_url( '/2025/test-post-publish/' ), 'Old post URL should not be pinged again after content update.' ); + $this->assertPing( home_url( '/2025/updated-test-post-publish/' ), 'New post URL should be pinged again after content update.' ); + // Ensure last ping URL is the updated URL. + $last_ping_url = SimpleSearchSubmission\get_last_post_ping_url( $post_id ); + $this->assertSame( home_url( '/2025/updated-test-post-publish/' ), $last_ping_url, 'Last ping URL should be the updated URL after the second ping.' ); + } + + /** + * Ensure de-indexed URLs are pinged only once for private post type transitions. + */ + public function test_no_duplicate_pings_on_private_post_type_transition() { + $post_id = self::$post_ids['publish']; + + // Set the post to a private status. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'private', + ) + ); + + // Ensure the old URL is pinged. + $this->assertPing( home_url( '/2025/test-post-publish/' ), 'Old post URL should be pinged after status change.' ); + + // Clear the ping list. + $this->pings = array(); + + // Set the post to a draft. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'draft', + ) + ); + // Ensure the old URL is not pinged again. + $this->assertNotPing( home_url( '/2025/test-post-publish/' ), 'Old post URL should not be pinged again after second status change.' ); + } + + /** + * Ensure previously deindexed URLs that now redirect are re-pinged. + */ + public function test_republished_post_with_slug_change_pings_old_urls() { + $post_id = self::$post_ids['publish']; + + // Set the post to draft. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'draft', + ) + ); + + // Ensure the old post URL is pinged. + $this->assertPing( home_url( '/2025/test-post-publish/' ), 'Old post URL should be pinged after unpublishing.' ); + + // Clear the ping list. + $this->pings = array(); + + // Update the post slug and re-publish all in one update. + wp_update_post( + array( + 'ID' => $post_id, + 'post_name' => 'final-test-post-publish', + 'post_status' => 'publish', + ) + ); + + // Ensure both URLs are pinged as the old URL is now a redirect. + $this->assertPing( home_url( '/2025/test-post-publish/' ), 'Old post URL should be pinged after slug and status update to index the redirect.' ); + $this->assertPing( home_url( '/2025/final-test-post-publish/' ), 'New post URL should be pinged after slug and status update.' ); + } + + /** + * Ensure updating the pinged URLs lists appends the new URLs correctly. + */ + public function test_update_post_ping_urls_appends_correctly() { + $post_id = self::$post_ids['publish']; + + // Update the post slug for the first time. + wp_update_post( + array( + 'ID' => $post_id, + 'post_name' => 'first-update-test-post-publish', + ) + ); + + $last_ping_url = SimpleSearchSubmission\get_last_post_ping_url( $post_id ); + $this->assertSame( home_url( '/2025/first-update-test-post-publish/' ), $last_ping_url, 'Last ping URL should be the first updated URL.' ); + + // Update the post slug for the second time. + wp_update_post( + array( + 'ID' => $post_id, + 'post_name' => 'second-update-test-post-publish', + ) + ); + + $last_ping_url = SimpleSearchSubmission\get_last_post_ping_url( $post_id ); + $this->assertSame( home_url( '/2025/second-update-test-post-publish/' ), $last_ping_url, 'Last ping URL should be the second updated URL.' ); + + // Restore the first updated URL. + wp_update_post( + array( + 'ID' => $post_id, + 'post_name' => 'first-update-test-post-publish', + ) + ); + + $last_ping_url = SimpleSearchSubmission\get_last_post_ping_url( $post_id ); + $this->assertSame( home_url( '/2025/first-update-test-post-publish/' ), $last_ping_url, 'Last ping URL should be the restored first updated URL.' ); + } + + /** + * Ensure URL list never contains duplicates. + */ + public function test_update_post_ping_urls_no_duplicates() { + $post_id = self::$post_ids['publish']; + + // Update the ping list (no need to actually ping). + SimpleSearchSubmission\add_post_ping_urls( $post_id, array( home_url( '/2025/test-post-publish/' ) ) ); + SimpleSearchSubmission\add_post_ping_urls( $post_id, array( home_url( '/2025/test-post-publish-two/' ) ) ); + SimpleSearchSubmission\add_post_ping_urls( $post_id, array( home_url( '/2025/test-post-publish-two/' ) ) ); + SimpleSearchSubmission\add_post_ping_urls( $post_id, array( home_url( '/2025/test-post-publish/' ) ) ); + + // Ensure there are no duplicates. + $ping_urls = SimpleSearchSubmission\get_post_ping_urls( $post_id ); + $this->assertCount( 2, $ping_urls, 'Ping URL list should not contain both URLs.' ); + + $expected = array( + home_url( '/2025/test-post-publish-two/' ), + home_url( '/2025/test-post-publish/' ), + ); + $this->assertSame( $expected, $ping_urls, 'Ping URL list should not contain duplicates.' ); + } }