: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @package WPSEO\XML_Sitemaps
* @todo This class could use a general description with some explanation on sitemaps. OR.
* Sitemap index identifier.
public const SITEMAP_INDEX_TYPE = '1';
* Content of the sitemap to output.
* Flag to indicate if this is an invalid or empty sitemap.
public $bad_sitemap = false;
* Whether or not the XML sitemap was served from a transient or not.
private $transient = false;
* HTTP protocol to use in headers.
protected $http_protocol = 'HTTP/1.1';
private $current_page = 1;
* @var WPSEO_Sitemaps_Router
* @var WPSEO_Sitemaps_Renderer
* @var WPSEO_Sitemaps_Cache
* @var WPSEO_Sitemap_Provider[]
public function __construct() {
add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] );
add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 );
add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 );
add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] );
$this->router = new WPSEO_Sitemaps_Router();
$this->renderer = new WPSEO_Sitemaps_Renderer();
$this->cache = new WPSEO_Sitemaps_Cache();
if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
$this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
* Initialize sitemap providers classes.
public function init_sitemaps_providers() {
new WPSEO_Post_Type_Sitemap_Provider(),
new WPSEO_Taxonomy_Sitemap_Provider(),
new WPSEO_Author_Sitemap_Provider(),
$external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
foreach ( $external_providers as $provider ) {
if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) {
$this->providers[] = $provider;
* Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets.
public function reduce_query_load() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
$extension = substr( $request_uri, -4 );
if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
remove_all_actions( 'widgets_init' );
* Register your own sitemap. Call this during 'init'.
* @param string $name The name of the sitemap.
* @param callback $building_function Function to build your sitemap.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
public function register_sitemap( $name, $building_function, $rewrite = '' ) {
add_action( 'wpseo_do_sitemap_' . $name, $building_function );
Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' );
* Register your own XSL file. Call this during 'init'.
* @param string $name The name of the XSL file.
* @param callback $building_function Function to build your XSL file.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
public function register_xsl( $name, $building_function, $rewrite = '' ) {
add_action( 'wpseo_xsl_' . $name, $building_function );
Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' );
* Set the sitemap current page to allow creating partial sitemaps with WP-CLI
* @param int $current_page The part that should be generated.
public function set_n( $current_page ) {
if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) {
$this->current_page = intval( $current_page );
* Set the sitemap content to display after you have generated it.
* @param string $sitemap The generated sitemap to output.
public function set_sitemap( $sitemap ) {
$this->sitemap = $sitemap;
* Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests.
* @param bool $is_bad Is this a bad request. True or false.
public function set_bad_sitemap( $is_bad ) {
$this->bad_sitemap = (bool) $is_bad;
* Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML.
public function sitemap_close() {
remove_all_actions( 'wp_footer' );
* Hijack requests for potential sitemaps and XSL files.
* @param WP_Query $query Main query instance.
public function redirect( $query ) {
if ( ! $query->is_main_query() ) {
$yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
if ( ! empty( $yoast_sitemap_xsl ) ) {
* This is a method to provide the XSL via the home_url.
* Needed when the site_url and home_url are not the same.
* Loading the XSL needs to come from the same domain, protocol and port as the XML.
* Whenever home_url and site_url are the same, the file can be loaded directly.
$this->xsl_output( $yoast_sitemap_xsl );
$type = get_query_var( 'sitemap' );
if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) {
wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' );
$this->set_n( get_query_var( 'sitemap_n' ) );
if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
$this->build_sitemap( $type );
if ( $this->bad_sitemap ) {
* Try to get the sitemap from cache.
* @param string $type Sitemap type.
* @param int $page_number The page number to retrieve.
* @return bool If the sitemap has been retrieved from cache.
private function get_sitemap_from_cache( $type, $page_number ) {
$this->transient = false;
if ( $this->cache->is_enabled() !== true ) {
* Fires before the attempt to retrieve XML sitemap from the transient cache.
* @param WPSEO_Sitemaps $sitemaps Sitemaps object.
do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this );
$sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number );
// No cache was found, refresh it because cache is enabled.
if ( empty( $sitemap_cache_data ) ) {
return $this->refresh_sitemap_cache( $type, $page_number );
// Cache object was found, parse information.
$this->sitemap = $sitemap_cache_data->get_sitemap();
$this->bad_sitemap = ! $sitemap_cache_data->is_usable();
* Build and save sitemap to cache.
* @param string $type Sitemap type.
* @param int $page_number The page number to save to.
private function refresh_sitemap_cache( $type, $page_number ) {
$this->set_n( $page_number );
$this->build_sitemap( $type );
return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap );
* Attempts to build the requested sitemap.
* Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy.
* @param string $type The requested sitemap's identifier.
public function build_sitemap( $type ) {
* Filter the type of sitemap to build.
* @param string $type Sitemap type, determined by the request.
$type = apply_filters( 'wpseo_build_sitemap_post_type', $type );
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
if ( ! $provider->handles_type( $type ) ) {
$links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
} catch ( OutOfBoundsException $exception ) {
$this->bad_sitemap = true;
$this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page );
if ( has_action( 'wpseo_do_sitemap_' . $type ) ) {
* Fires custom handler, if hooked to generate sitemap for the type.
do_action( 'wpseo_do_sitemap_' . $type );
$this->bad_sitemap = true;
* Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
public function build_root_map() {
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
$links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
* Filter the sitemap links array before the index sitemap is built.
* @param array $links Array of sitemap links
$links = apply_filters( 'wpseo_sitemap_index_links', $links );
$this->bad_sitemap = true;
$this->sitemap = $this->renderer->get_index( $links );
* Spits out the XSL for the XML sitemap.
* @param string $type Type to output.
public function xsl_output( $type ) {
if ( $type !== 'main' ) {
* Fires for the output of XSL for XML sitemaps, other than type "main".
do_action( 'wpseo_xsl_' . $type );
header( $this->http_protocol . ' 200 OK', true, 200 );
// Prevent the search engines from indexing the XML Sitemap.
header( 'X-Robots-Tag: noindex, follow', true );
header( 'Content-Type: text/xml' );
// Make the browser cache this file properly.
$expires = YEAR_IN_SECONDS;
header( 'Pragma: public' );
header( 'Cache-Control: max-age=' . $expires );
header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' );
// Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043.
readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
* Spit out the generated sitemap.
public function output() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document.
echo $this->renderer->get_output( $this->sitemap );
* Makes a request to the sitemap index to cache it before the arrival of the search engines.
public function hit_sitemap_index() {
if ( ! $this->cache->is_enabled() ) {
wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
* Get the GMT modification date for the last modified post in the post type.
* @param string|array $post_types Post type or array of types.
* @param bool $return_all Flag to return array of values.