• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using REST API custom routes

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_route registers the endpoint /wp-json/my-plugin/v1/sitemap.
  • The callback is set to my_sitemap_generate_xml, which will contain our sitemap generation logic.
  • permission_callback is set to __return_true for 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_xml function constructs the XML string. It includes the homepage, posts, pages, a custom post type (product as an example), and categories. You’ll need to adapt the get_posts and get_categories calls to include your specific post types and taxonomies.
  • esc_url() and date() are used for proper URL encoding and date formatting according to sitemap protocol standards.
  • WP_REST_Response is used to return the XML content with the correct application/xml content type.
  • register_activation_hook and register_deactivation_hook are 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) and linkLabel.
  • The edit function renders the block in the Gutenberg editor. It includes InspectorControls to allow users to customize the sitemap URL and the link text. It also displays a preview of the link.
  • The save function 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 the href attribute 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_callback in register_rest_route should 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_xml function 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.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala