Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using REST API custom routes
Leveraging WordPress REST API for Dynamic XML Sitemaps
While WordPress offers built-in sitemap functionality, the need for highly customized, dynamic sitemaps that reflect specific content types, taxonomies, or even external data sources often arises. This guide details the construction of a custom Gutenberg block that dynamically generates an XML sitemap by leveraging WordPress’s REST API and custom endpoint registration. This approach ensures the sitemap is always up-to-date without relying on cron jobs or static file generation.
Registering a Custom REST API Route for Sitemap Data
The foundation of our dynamic sitemap lies in a custom REST API endpoint. We’ll register a new route that, when accessed, will query WordPress for the necessary data and format it as an XML sitemap. This involves using the register_rest_route function within a plugin or theme’s functions.php file.
We’ll define a route, for example, /my-plugin/v1/sitemap, and associate it with a callback function that handles the sitemap generation logic. It’s crucial to specify the HTTP methods allowed for this route (GET in this case) and any necessary permissions checks.
Plugin Activation Hook for Registration
To ensure the REST API route is registered only when our plugin is active, we’ll hook into the plugin activation process. This prevents potential errors or performance issues if the route is registered on every WordPress load.
/**
* Plugin Name: Custom Sitemap Generator
* Description: Dynamically generates an XML sitemap via REST API.
* Version: 1.0.0
* Author: Antigravity
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register the REST API route on plugin activation.
*/
function my_sitemap_register_rest_route() {
register_rest_route( 'my-plugin/v1', '/sitemap', array(
'methods' => 'GET',
'callback' => 'my_sitemap_generate_xml',
'permission_callback' => '__return_true', // For simplicity, allow public access. Consider more robust checks.
) );
}
add_action( 'rest_api_init', 'my_sitemap_register_rest_route' );
/**
* Generates the XML sitemap.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response Response object.
*/
function my_sitemap_generate_xml( WP_REST_Request $request ) {
// Sitemap generation logic will go here.
$xml_content = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
// Example: Add homepage
$xml_content .= '<url><loc>' . esc_url( home_url( '/' ) ) . '</loc><lastmod>' . date( 'Y-m-d\TH:i:sP' ) . '</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>';
// Add posts
$posts = get_posts( array(
'numberposts' => -1,
'post_type' => 'post',
'post_status' => 'publish',
) );
foreach ( $posts as $post ) {
$xml_content .= '<url><loc>' . esc_url( get_permalink( $post->ID ) ) . '</loc><lastmod>' . get_post_modified_time( 'Y-m-d\TH:i:sP', true, $post ) . '</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>';
}
// Add pages
$pages = get_pages( array(
'number' => -1,
'post_status' => 'publish',
) );
foreach ( $pages as $page ) {
$xml_content .= '<url><loc>' . esc_url( get_permalink( $page->ID ) ) . '</loc><lastmod>' . get_post_modified_time( 'Y-m-d\TH:i:sP', true, $page ) . '</lastmod><changefreq>monthly</changefreq><priority>0.6</priority></url>';
}
// Add custom post types (example: 'product')
$products = get_posts( array(
'numberposts' => -1,
'post_type' => 'product', // Replace 'product' with your custom post type slug
'post_status' => 'publish',
) );
foreach ( $products as $product ) {
$xml_content .= '<url><loc>' . esc_url( get_permalink( $product->ID ) ) . '</loc><lastmod>' . get_post_modified_time( 'Y-m-d\TH:i:sP', true, $product ) . '</lastmod><changefreq>daily</changefreq><priority>0.9</priority></url>';
}
// Add taxonomies (example: categories)
$categories = get_categories( array(
'hide_empty' => true,
) );
foreach ( $categories as $category ) {
$xml_content .= '<url><loc>' . esc_url( get_category_link( $category->term_id ) ) . '</loc><changefreq>weekly</changefreq><priority>0.7</priority></url>';
}
$xml_content .= '</urlset>';
$response = new WP_REST_Response( $xml_content );
$response->set_content_type( 'application/xml' );
return $response;
}
/**
* Flush rewrite rules on plugin activation to ensure the REST API route is recognized.
*/
function my_sitemap_activate_plugin() {
my_sitemap_register_rest_route(); // Ensure route is registered before flushing
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_sitemap_activate_plugin' );
/**
* Flush rewrite rules on plugin deactivation.
*/
function my_sitemap_deactivate_plugin() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_sitemap_deactivate_plugin' );
In this code:
- We define a plugin header for basic plugin management.
my_sitemap_register_rest_routeregisters the endpoint/wp-json/my-plugin/v1/sitemap.- The
callbackis set tomy_sitemap_generate_xml, which will contain our sitemap generation logic. permission_callbackis set to__return_truefor simplicity. In a production environment, you would implement more granular permission checks, perhaps checking for specific user capabilities or API keys.- The
my_sitemap_generate_xmlfunction constructs the XML string. It includes the homepage, posts, pages, a custom post type (productas an example), and categories. You’ll need to adapt theget_postsandget_categoriescalls to include your specific post types and taxonomies. esc_url()anddate()are used for proper URL encoding and date formatting according to sitemap protocol standards.WP_REST_Responseis used to return the XML content with the correctapplication/xmlcontent type.register_activation_hookandregister_deactivation_hookare used to flush rewrite rules. This is crucial for the REST API endpoint to be recognized immediately after activation and to be cleaned up on deactivation.
Creating the Gutenberg Block for Sitemap Display
Now that we have a dynamic sitemap endpoint, we can create a Gutenberg block that allows users to easily embed a link to this sitemap within their posts or pages. This involves creating a JavaScript file for the block’s editor and frontend rendering.
Block Registration and Editor Interface
We’ll use the WordPress Script and Style enqueueing system to load our block’s JavaScript and CSS. The block itself will be registered using wp.blocks.registerBlockType.
For the editor interface, we’ll keep it simple: a static text indicating the sitemap will be displayed, and perhaps a link to the sitemap’s URL for easy access during editing. The actual sitemap rendering will happen on the frontend.
/**
* Registers the custom sitemap link block.
*/
( function( blocks, element, components, editor ) {
var el = element.createElement;
var registerBlockType = blocks.registerBlockType;
var RichText = editor.RichText;
var InspectorControls = editor.InspectorControls;
var PanelBody = components.PanelBody;
var TextControl = components.TextControl;
var ServerSideRender = editor.ServerSideRender; // For potential future server-side rendering of the block itself
registerBlockType( 'my-plugin/sitemap-link', {
title: 'Sitemap Link',
icon: 'admin-site-alt3',
category: 'widgets',
attributes: {
sitemapUrl: {
type: 'string',
default: '/wp-json/my-plugin/v1/sitemap', // Default to our custom endpoint
},
linkLabel: {
type: 'string',
default: 'View Sitemap',
}
},
edit: function( props ) {
var attributes = props.attributes;
var sitemapUrl = attributes.sitemapUrl;
var linkLabel = attributes.linkLabel;
var setAttributes = props.setAttributes;
var onSitemapUrlChange = function( newUrl ) {
setAttributes( { sitemapUrl: newUrl } );
};
var onLinkLabelChange = function( newLabel ) {
setAttributes( { linkLabel: newLabel } );
};
// In the editor, we'll just show a link to the sitemap and allow configuration.
// The actual sitemap XML is too complex to render directly in the editor.
return [
el( InspectorControls, null,
el( PanelBody, { title: 'Sitemap Settings' },
el( TextControl, {
label: 'Sitemap API Endpoint URL',
value: sitemapUrl,
onChange: onSitemapUrlChange,
} ),
el( TextControl, {
label: 'Link Text',
value: linkLabel,
onChange: onLinkLabelChange,
} )
)
),
el( 'div', { className: props.className },
el( 'p', null, 'This block will display a link to your sitemap.' ),
el( 'a', { href: sitemapUrl, target: '_blank', rel: 'noopener noreferrer' }, linkLabel ),
el( 'p', null, 'Configure the URL and link text in the block settings sidebar.' )
)
];
},
save: function( props ) {
var sitemapUrl = props.attributes.sitemapUrl;
var linkLabel = props.attributes.linkLabel;
// On save, we output a simple anchor tag.
// The actual sitemap content is fetched dynamically via the REST API when the page loads.
return el( 'div', { className: 'sitemap-link-block' },
el( 'a', { href: sitemapUrl, target: '_blank', rel: 'noopener noreferrer' }, linkLabel )
);
}
} );
}(
window.wp.blocks,
window.wp.element,
window.wp.components,
window.wp.editor
) );
Enqueueing Block Assets
We need to enqueue the JavaScript file for our Gutenberg block. This is done using wp_enqueue_script, typically within an action hook like enqueue_block_editor_assets for the editor and wp_enqueue_scripts for the frontend.
/**
* Enqueue Gutenberg block assets for the editor.
*/
function my_sitemap_enqueue_block_editor_assets() {
wp_enqueue_script(
'my-sitemap-block-editor',
plugin_dir_url( __FILE__ ) . 'build/index.js', // Path to your compiled JS file
array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-editor' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
}
add_action( 'enqueue_block_editor_assets', 'my_sitemap_enqueue_block_editor_assets' );
/**
* Enqueue frontend assets if needed (e.g., for styling).
* For this specific block, the frontend rendering is handled by the 'save' function,
* which outputs static HTML. The dynamic sitemap content is fetched via the REST API.
*/
function my_sitemap_enqueue_frontend_assets() {
// If you need CSS for the frontend link, enqueue it here.
// wp_enqueue_style( 'my-sitemap-style', plugin_dir_url( __FILE__ ) . 'style.css', array(), filemtime( plugin_dir_path( __FILE__ ) . 'style.css' ) );
}
add_action( 'wp_enqueue_scripts', 'my_sitemap_enqueue_frontend_assets' );
In the JavaScript:
- We register a block type named
my-plugin/sitemap-link. - It has two attributes:
sitemapUrl(defaulting to our REST API endpoint) andlinkLabel. - The
editfunction renders the block in the Gutenberg editor. It includesInspectorControlsto allow users to customize the sitemap URL and the link text. It also displays a preview of the link. - The
savefunction defines the static HTML that will be saved to the post content. This is a simple anchor tag. The actual sitemap content is not embedded here; it’s fetched dynamically by the browser when the page loads via thehrefattribute pointing to our REST API endpoint.
Building and Compiling JavaScript Assets
Modern JavaScript development for WordPress often involves build tools like Webpack. You’ll need to set up a package.json and a Webpack configuration to compile your block’s JavaScript into a format that WordPress can use.
Example package.json
{
"name": "custom-sitemap-generator",
"version": "1.0.0",
"description": "Custom Sitemap Generator Plugin",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [
"wordpress",
"gutenberg",
"block"
],
"author": "Antigravity",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
After creating this package.json file in your plugin’s root directory, run the following commands in your terminal:
npm install npm run build
This will create a build directory containing the compiled JavaScript file (e.g., index.js) that you referenced in your PHP enqueueing function.
Testing and Verification
Once the plugin is activated and the block is compiled, you can test the functionality:
- Navigate to a post or page in the WordPress editor and add the “Sitemap Link” block.
- In the block settings sidebar, verify that the default “Sitemap API Endpoint URL” points to
/wp-json/my-plugin/v1/sitemap. - Add some content to the page and save it.
- View the page on the frontend. You should see the “View Sitemap” link (or whatever text you configured).
- Clicking this link should take you to a page displaying the XML sitemap generated by your custom REST API endpoint.
- To verify the REST API endpoint directly, you can access it via your browser:
yourwebsite.com/wp-json/my-plugin/v1/sitemap. This should render the raw XML sitemap.
Advanced Considerations and Enhancements
This setup provides a robust foundation. Here are some advanced considerations:
- Security: The
permission_callbackinregister_rest_routeshould be hardened. For sensitive data, consider API keys, nonces, or user role checks. - Performance: For very large sites, generating the sitemap on every request might become a performance bottleneck. Caching strategies (e.g., using transient API or a dedicated caching plugin) for the sitemap data can mitigate this. Alternatively, consider a hybrid approach where the REST API endpoint returns a cacheable response.
- Sitemap Index: For very large sitemaps, you might need to implement a sitemap index file that links to multiple individual sitemap files. This would involve creating another REST API endpoint to serve the index.
- Customization: Extend the
my_sitemap_generate_xmlfunction to include more complex logic, such as:- Filtering by post date ranges.
- Including custom fields or metadata.
- Excluding specific posts or pages.
- Handling different content types with varying priorities and change frequencies.
- Internationalization: Ensure all user-facing strings (like the link label) are translatable using WordPress’s internationalization functions.
- Error Handling: Implement robust error handling within the REST API callback to gracefully manage situations where data cannot be retrieved or formatted correctly.
By combining custom REST API routes with Gutenberg blocks, you can create highly dynamic and user-friendly solutions for managing content presentation and discoverability within WordPress.