Commit 0c4ee43fed for wordpress.org

commit 0c4ee43fed239a021d709c6dc50e1eeea77eb4e9
Author: westonruter <westonruter@git.wordpress.org>
Date:   Wed Sep 9 22:25:46 2026 +0000

    Sitemaps: Don't 404 valid sitemaps on sites with no posts.

    `WP::handle_404()` sets a 404 when the main query matches no posts and no exception applies. Sitemap requests were never among those exceptions; they were shielded only incidentally, by falling through to `is_home`, and in r62664 that fallthrough was removed. A site with no published posts therefore served a complete, valid sitemap under a 404 status, which search engines discard.

    Exempt sitemap and stylesheet routes there, alongside the existing admin, robots and favicon exceptions. Since `handle_404()` no longer decides the status for these requests, every sitemap 404 now has to be issued by `WP_Sitemaps::render_sitemaps()` instead: an unregistered provider, an unrecognized stylesheet type, and a route whose query vars do not survive `sanitize_text_field()` would each otherwise be served as a 200 on an arbitrary URL. These share a `send_404()` helper, which also sends the no-cache headers `handle_404()` was previously contributing, so an intermediary does not retain a 404 for a route that becomes valid once the site has more content.

    Sitemaps disabled via the `wp_sitemaps_enabled` filter, and providers with an empty URL list, keep the status they already had; whether the latter should render an empty sitemap instead is #61293.

    Developed in https://github.com/WordPress/wordpress-develop/pull/13247.
    Follow-up to r48072, r48523, r62664.

    Props iamchitti, westonruter, fernandot, wildworks, harishtewari, l1onofjudah, luksusspokoju, abrahamfariaz, andreasca, siliconforks, adamsilverstein, audrasjb, ocean90, mrkenobi.
    See #39157, #61293.
    Fixes #65945.

    Built from https://develop.svn.wordpress.org/trunk@63570


    git-svn-id: http://core.svn.wordpress.org/trunk@62746 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/class-wp.php b/wp-includes/class-wp.php
index f1664747d4..ad6ce74b17 100644
--- a/wp-includes/class-wp.php
+++ b/wp-includes/class-wp.php
@@ -746,8 +746,9 @@ class WP {

 		$set_404 = true;

-		// Never 404 for the admin, robots, or favicon.
-		if ( is_admin() || is_robots() || is_favicon() ) {
+		// Never 404 here for the admin, robots, favicon, or sitemaps.
+		// Sitemap routes send their own status in WP_Sitemaps::render_sitemaps().
+		if ( is_admin() || is_robots() || is_favicon() || is_sitemap() || get_query_var( 'sitemap-stylesheet' ) ) {
 			$set_404 = false;

 			// If posts were found, check for paged content.
diff --git a/wp-includes/sitemaps/class-wp-sitemaps.php b/wp-includes/sitemaps/class-wp-sitemaps.php
index d4bbe38c86..87b3765b3f 100644
--- a/wp-includes/sitemaps/class-wp-sitemaps.php
+++ b/wp-includes/sitemaps/class-wp-sitemaps.php
@@ -157,30 +157,45 @@ class WP_Sitemaps {
 	 * Renders sitemap templates based on rewrite rules.
 	 *
 	 * @since 5.5.0
-	 *
-	 * @global WP_Query $wp_query WordPress Query object.
 	 */
 	public function render_sitemaps() {
-		global $wp_query;
+		/*
+		 * Bail early if this isn't a sitemap or stylesheet route.
+		 *
+		 * This runs on every front-end request, so it comes before any
+		 * sanitizing. The raw query vars are tested here, matching
+		 * WP::handle_404(), which exempts sitemap requests from its own 404 on
+		 * the same basis. Testing the sanitized values instead would let a
+		 * request that handle_404() exempted fall through both, leaving it a 200.
+		 */
+		if ( ! get_query_var( 'sitemap' ) && ! get_query_var( 'sitemap-stylesheet' ) ) {
+			return;
+		}

 		$sitemap         = sanitize_text_field( get_query_var( 'sitemap' ) );
 		$object_subtype  = sanitize_text_field( get_query_var( 'sitemap-subtype' ) );
 		$stylesheet_type = sanitize_text_field( get_query_var( 'sitemap-stylesheet' ) );
 		$paged           = absint( get_query_var( 'paged' ) );

-		// Bail early if this isn't a sitemap or stylesheet route.
+		// Force a 404 and bail early if the route did not survive sanitizing.
 		if ( ! ( $sitemap || $stylesheet_type ) ) {
+			$this->send_404();
 			return;
 		}

 		if ( ! $this->sitemaps_enabled() ) {
-			$wp_query->set_404();
-			status_header( 404 );
+			$this->send_404();
 			return;
 		}

 		// Render stylesheet if this is stylesheet route.
 		if ( $stylesheet_type ) {
+			// Force a 404 and bail early if the stylesheet type is not recognized.
+			if ( ! in_array( $stylesheet_type, array( 'sitemap', 'index' ), true ) ) {
+				$this->send_404();
+				return;
+			}
+
 			$stylesheet = new WP_Sitemaps_Stylesheet();

 			$stylesheet->render_stylesheet( $stylesheet_type );
@@ -197,7 +212,9 @@ class WP_Sitemaps {

 		$provider = $this->registry->get_provider( $sitemap );

+		// Force a 404 and bail early if the requested provider is not registered.
 		if ( ! $provider ) {
+			$this->send_404();
 			return;
 		}

@@ -209,8 +226,7 @@ class WP_Sitemaps {

 		// Force a 404 and bail early if no URLs are present.
 		if ( empty( $url_list ) ) {
-			$wp_query->set_404();
-			status_header( 404 );
+			$this->send_404();
 			return;
 		}

@@ -218,6 +234,26 @@ class WP_Sitemaps {
 		exit;
 	}

+	/**
+	 * Sends a 404 for a sitemap route that cannot be served.
+	 *
+	 * WP::handle_404() exempts sitemap requests, so every sitemap 404 is issued
+	 * here instead. That includes the no-cache headers handle_404() sends with
+	 * its own 404, so an intermediary does not retain a 404 for a route that
+	 * becomes valid once the site has more content.
+	 *
+	 * @since 7.1.1
+	 *
+	 * @global WP_Query $wp_query WordPress Query object.
+	 */
+	private function send_404(): void {
+		global $wp_query;
+
+		$wp_query->set_404();
+		status_header( 404 );
+		nocache_headers();
+	}
+
 	/**
 	 * Redirects a URL to the wp-sitemap.xml
 	 *
diff --git a/wp-includes/version.php b/wp-includes/version.php
index cf460d2d87..39349af7c2 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-63569';
+$wp_version = '7.2-alpha-63570';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.