Creating a WordPress Sitemap Provider

The WordPress Sitemap system, like many other parts, makes it quite easy to extend the functionality and add custom links to an existing Sitemap. Below we will look at how this can be done.

Which custom content is automatically added to the Sitemap?

When you create custom post types or custom taxonomies, this entities automatically get into the WordPress Sitemap. The only condition to it happens — new custom post type or taxonomy should be "public" — it must have parameters public = true and publicly_queryable = true. These parameters are specified when registering the post type or taxonomy.

Creating your own provider for the Site Map

But if your plugin or theme has its own entities that need to be added to the sitemap, then you will need to create a new provider. This is a custom PHP class that extends an abstract WP_Sitemaps_Provider class.

The created Class, you need to register (connect) using the wp_register_sitemap_provider() function:

add_filter( 'init', function(){

	$provider = new Awesome_Plugin_Sitemaps_Provider();

	wp_register_sitemap_provider( 'awesome-plugin', $provider );
} );

This provider will be responsible for displaying all new Sitemaps, paginating them, and displaying map of single elements (site URLs).

Creating Providers Examples

#1 Your own provider with two types and a custom table

Let's say we have a table in the database that stores media data about audio and video entities. Each such entity has its own page on the site. We need to add all these pages to the Sitemap.

Let's create our own provider, call it "megamedia". To do this, create a class-Megamedia_Sitemaps_Provider.php file in the theme with the following code:

class Media_Sitemaps_Provider extends WP_Sitemaps_Provider {

	// make visibility not protected
	public $name;

	/**
	 * Constructor. Sets name, object_type properties.
	 *
	 * $name         Provider name. Uses in URL (should be unique).
	 * $object_type  The object name that the provider works with.
	 *               Passes into the hooks (should be unique).
	 */
	public function __construct() {

		$this->name        = 'megamedia';
		$this->object_type = 'megamedia';
	}

	/**
	 * Returns the list of supported object subtypes exposed by the provider.
	 *
	 * @return array List of object subtypes objects keyed by their name.
	 */
	public function get_object_subtypes() {
		return array(
			'audio' => new stdClass(),
			'video' => new stdClass()
		);
	}

	/**
	 * Gets a URL list for a sitemap.
	 *
	 * @param int    $page_num       Page of results.
	 * @param string $subtype Optional. Object subtype name. Default empty.
	 *
	 * @return array Array of URLs for a sitemap.
	 */
	public function get_url_list( $page_num, $subtype = '' ) {

		$result = $this->db_query( [
			'subtype' => $subtype,
			'paged'   => $page_num,
		] );

		$url_list = array();

		foreach ( $result as $megamedia ) {
			$sitemap_entry = [
				'loc' => home_url( "/megamedia/$subtype/$megamedia->id" ),
				// 'priority'   => 0.5,
				// 'changefreq' => 'monthly',
			];

			$url_list[] = $sitemap_entry;
		}

		return $url_list;
	}

	/**
	 * Gets the max number of pages available for the object type.
	 *
	 * @param string $subtype Optional. Object subtype. Default empty.
	 * @return int Total number of pages.
	 */
	public function get_max_num_pages( $subtype = '' ) {

		$total = $this->db_query( [
			'subtype' => $subtype,
			'count'   => true,
		] );

		return (int) ceil( $total / wp_sitemaps_get_max_urls( $this->object_type ) );
	}

	/**
	 * Returns the SQL query result.
	 *
	 * @return array Array of query arguments.
	 */
	protected function db_query( $args ) {
		global $wpdb;

		$arg = (object) array_merge( [
			'paged'   => 1,
			'subtype' => 'audio',
			'count'   => false,
		], $args );

		$SELECT = $arg->count ? 'count(*)' : '*';

		$WHERE = [];
		$WHERE[] = 'post_id = 0';
		$WHERE[] = $wpdb->prepare( "media_type = %s", $arg->subtype );
		$WHERE = implode( ' AND ', $WHERE );

		$per_page = wp_sitemaps_get_max_urls( $this->object_type );
		$offset = ( $arg->paged - 1 ) * $per_page;
		$LIMIT = sprintf( "LIMIT %d, %d", $offset,$per_page );

		$sql = "SELECT $SELECT FROM $wpdb->wp_core_data WHERE $WHERE $LIMIT";

		$result = $arg->count ? $wpdb->get_var( $sql ) : $wpdb->get_results( $sql );

		return $result;
	}

}

Now connect (register) the created provider in the functions.php file.

add_filter( 'init', 'wpkama_register_sitemap_providers' );

function wpkama_register_sitemap_providers(){

	require_once __DIR__ .'/class-Megamedia_Sitemaps_Provider.php';

	$provider = new Megamedia_Sitemaps_Provider();

	wp_register_sitemap_provider( $provider->name, $provider );
}

Now visit the Sitemap to see what we have:

Notes:

Important: the first wp_register_sitemap_provider() function parameter should be same as provider $name property! And this name can only consist of a-z characters!

WP_Sitemaps_Provider Class properties:

$this->name

The name of the provider. Used in the site map URL. Must be unique.

IMPORTANT! Only characters are allowed a-z. That is, you can't use dashes, spaces, or lowercase characters. Wrong: similar_posts, similar-posts, Similar. Right: similarposts.

$this->object_type
The name of the object that the provider works with (post, term, user). Used in hooks. Should be unique.

Media_Sitemaps_Provider and WP_Sitemaps_Provider Classes methods:

get_object_subtypes()
Should return list of subtypes. For example, for posts should return post types. Returns an array of objects with subtype names in the keys.
get_url_list( $page_num, $subtype = '' )

Should return a list of links for each map — a list of data for XML <url> tags. This list looks like an array of arrays, each nested array is a sitemap link element. For example:

$url_list = [
	[ 'loc'=> 'https://example.com/megamedia/audio/2610735' ],
	[ 'loc'=> 'https://example.com/megamedia/audio/9514241' ],
	...
];
get_max_num_pages( $subtype = '' )
Should return a number - the total number of elements for the specified subtype. For example, for WP posts, this total number of posts of the specified post type, for example, how many there are pages on site (page post type).
db_query( $args )
This is custom class method, which is creates DB query and gets data by specified parameters. Here the subtype and pagination are taken into account.

Used Functions:

wp_sitemaps_get_max_urls()
An external function that returns the maximum number of elements in each sitemap. By default 2000.

#2 Post-based provider

Let's say we have finalized the URL of the posts and now if we add /similar_posts/ to the end of the post URL, we will see a page with similar posts. We need to add all such links (pages) to the Sitemap.

Let's name the provider similarposts (we can't specify spaces and dashes in the name!) and create provider class like so:

class Similar_Posts_Sitemaps_Provider extends WP_Sitemaps_Provider {

	// make visibility not protected
	public $name;

	public function __construct() {

		$this->name        = 'similarposts';
		$this->object_type = 'similarposts';
	}

	public function get_url_list( $page_num, $subtype = '' ) {

		$args = $this->wp_query_args();
		$args['paged'] = $page_num;

		$query = new WP_Query( $args );

		$url_list = array();

		foreach ( $query->posts as $post ) {
			$sitemap_entry = [
				'loc' => user_trailingslashit( untrailingslashit( get_permalink( $post ) ) .'/similar_posts/' ),
			];

			$url_list[] = $sitemap_entry;
		}

		return $url_list;
	}

	public function get_max_num_pages( $subtype = '' ) {

		$args                  = $this->wp_query_args();
		$args['fields']        = 'ids';
		$args['no_found_rows'] = false;

		$query = new WP_Query( $args );

		return $query->max_num_pages;
	}

	protected function wp_query_args(){

		return array(
			'orderby'                => 'ID',
			'order'                  => 'ASC',
			'post_type'              => 'post',
			'posts_per_page'         => wp_sitemaps_get_max_urls( $this->object_type ),
			'post_status'            => array( 'publish' ),
			'no_found_rows'          => true,
			'update_post_term_cache' => false,
			'update_post_meta_cache' => false,
		);
	}

}

Now register our Provider:

add_filter( 'init', 'wpkama_register_sitemap_providers' );

function wpkama_register_sitemap_providers(){

	require_once __DIR__ .'/class-Similar_Posts_Sitemaps_Provider.php';
	$provider = new Similar_Posts_Sitemaps_Provider();

	wp_register_sitemap_provider( $provider->name, $provider );
}

Done! Go to the sitemap to see what we have:

Index Sitemap page with links to the sitemaps of the "similarposts" provider.
The single sitemap page containing URLs in which we can see our URL suffix.

#3 Examples from WP core

There are three site map providers in WP core code. You can use it's code as a basis for creating your own provider.