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.