Make WordPress Core


Ignore:
Timestamp:
09/09/2026 10:24:50 PM (27 hours ago)
Author:
westonruter
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/sitemaps/class-wp-sitemaps.php

    r59229 r63570  
    158158         *
    159159         * @since 5.5.0
    160          *
    161          * @global WP_Query $wp_query WordPress Query object.
    162160         */
    163161        public function render_sitemaps() {
    164                 global $wp_query;
     162                /*
     163                 * Bail early if this isn't a sitemap or stylesheet route.
     164                 *
     165                 * This runs on every front-end request, so it comes before any
     166                 * sanitizing. The raw query vars are tested here, matching
     167                 * WP::handle_404(), which exempts sitemap requests from its own 404 on
     168                 * the same basis. Testing the sanitized values instead would let a
     169                 * request that handle_404() exempted fall through both, leaving it a 200.
     170                 */
     171                if ( ! get_query_var( 'sitemap' ) && ! get_query_var( 'sitemap-stylesheet' ) ) {
     172                        return;
     173                }
    165174
    166175                $sitemap         = sanitize_text_field( get_query_var( 'sitemap' ) );
     
    169178                $paged           = absint( get_query_var( 'paged' ) );
    170179
    171                 // Bail early if this isn't a sitemap or stylesheet route.
     180                // Force a 404 and bail early if the route did not survive sanitizing.
    172181                if ( ! ( $sitemap || $stylesheet_type ) ) {
     182                        $this->send_404();
    173183                        return;
    174184                }
    175185
    176186                if ( ! $this->sitemaps_enabled() ) {
    177                         $wp_query->set_404();
    178                         status_header( 404 );
     187                        $this->send_404();
    179188                        return;
    180189                }
     
    182191                // Render stylesheet if this is stylesheet route.
    183192                if ( $stylesheet_type ) {
     193                        // Force a 404 and bail early if the stylesheet type is not recognized.
     194                        if ( ! in_array( $stylesheet_type, array( 'sitemap', 'index' ), true ) ) {
     195                                $this->send_404();
     196                                return;
     197                        }
     198
    184199                        $stylesheet = new WP_Sitemaps_Stylesheet();
    185200
     
    198213                $provider = $this->registry->get_provider( $sitemap );
    199214
     215                // Force a 404 and bail early if the requested provider is not registered.
    200216                if ( ! $provider ) {
     217                        $this->send_404();
    201218                        return;
    202219                }
     
    210227                // Force a 404 and bail early if no URLs are present.
    211228                if ( empty( $url_list ) ) {
    212                         $wp_query->set_404();
    213                         status_header( 404 );
     229                        $this->send_404();
    214230                        return;
    215231                }
     
    217233                $this->renderer->render_sitemap( $url_list );
    218234                exit;
     235        }
     236
     237        /**
     238         * Sends a 404 for a sitemap route that cannot be served.
     239         *
     240         * WP::handle_404() exempts sitemap requests, so every sitemap 404 is issued
     241         * here instead. That includes the no-cache headers handle_404() sends with
     242         * its own 404, so an intermediary does not retain a 404 for a route that
     243         * becomes valid once the site has more content.
     244         *
     245         * @since 7.1.1
     246         *
     247         * @global WP_Query $wp_query WordPress Query object.
     248         */
     249        private function send_404(): void {
     250                global $wp_query;
     251
     252                $wp_query->set_404();
     253                status_header( 404 );
     254                nocache_headers();
    219255        }
    220256
Note: See TracChangeset for help on using the changeset viewer.