• 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 HTMX dynamic attributes

Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using HTMX dynamic attributes

Leveraging HTMX for Dynamic Gutenberg XML Sitemap Generation

This guide details the construction of a custom Gutenberg block that dynamically generates and displays an XML sitemap. We’ll integrate HTMX to enable client-side updates without full page reloads, offering a more interactive user experience within the WordPress admin. This approach is particularly useful for large sites where regenerating the sitemap can be resource-intensive.

Prerequisites and Setup

Before we begin, ensure you have a local WordPress development environment set up. You’ll need basic familiarity with PHP, JavaScript (specifically React for Gutenberg block development), and the WordPress plugin API. We’ll be creating a simple plugin to house our custom block.

Create a new directory for your plugin, e.g., custom-sitemap-block, within your WordPress installation’s wp-content/plugins/ directory. Inside this directory, create a main plugin file, custom-sitemap-block.php.

Plugin Boilerplate and Enqueuing Scripts

Start with the essential plugin header and script enqueuing. We need to register our Gutenberg block script and its editor assets. Crucially, we’ll also enqueue HTMX. For simplicity, we’ll use a CDN for HTMX, but in a production environment, you might bundle it.

custom-sitemap-block.php

<?php
/**
 * Plugin Name: Custom Sitemap Block
 * Description: A Gutenberg block to dynamically generate and display an XML sitemap using HTMX.
 * Version: 1.0.0
 * Author: Antigravity
 * License: GPL-2.0-or-later
 * Text Domain: custom-sitemap-block
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Register block editor assets.
 */
function csb_register_block_assets() {
    // Register the block script.
    register_block_type( __DIR__ . '/build' );

    // Enqueue HTMX from CDN.
    wp_enqueue_script(
        'htmx-script',
        'https://unpkg.com/[email protected]',
        array(),
        '1.9.10',
        true // Load in footer
    );
}
add_action( 'init', 'csb_register_block_assets' );

/**
 * Register the server-side rendering endpoint for the sitemap.
 */
function csb_register_sitemap_route() {
    register_rest_route( 'custom-sitemap-block/v1', '/sitemap', array(
        'methods'  => 'GET',
        'callback' => 'csb_render_sitemap_xml',
        'permission_callback' => '__return_true', // Adjust for production security
    ) );
}
add_action( 'rest_api_init', 'csb_register_sitemap_route' );

/**
 * Renders the XML sitemap.
 * This is a simplified example. A real-world scenario would involve
 * querying posts, pages, custom post types, and potentially external resources.
 */
function csb_render_sitemap_xml() {
    // Set the content type to XML.
    header( 'Content-Type: application/xml; charset=utf-8' );

    // Basic XML declaration.
    echo '<?xml version="1.0" encoding="UTF-8"?>';
    echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

    // Example: Add homepage.
    echo '<url>';
    echo '<loc>' . esc_url( home_url( '/' ) ) . '</loc>';
    echo '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>';
    echo '<changefreq>daily</changefreq>';
    echo '<priority>1.0</priority>';
    echo '</url>';

    // Example: Add posts.
    $posts = get_posts( array(
        'numberposts' => 100, // Limit for example
        'post_status' => 'publish',
        'post_type'   => 'post',
    ) );

    foreach ( $posts as $post ) {
        setup_postdata( $post );
        echo '<url>';
        echo '<loc>' . esc_url( get_permalink( $post->ID ) ) . '</loc>';
        echo '<lastmod>' . get_the_modified_date( 'Y-m-d', $post->ID ) . '</lastmod>';
        echo '<changefreq>weekly</changefreq>';
        echo '<priority>0.8</priority>';
        echo '</url>';
    }
    wp_reset_postdata();

    // Example: Add pages.
    $pages = get_pages( array(
        'number' => 100, // Limit for example
        'post_status' => 'publish',
    ) );

    foreach ( $pages as $page ) {
        echo '<url>';
        echo '<loc>' . esc_url( get_permalink( $page->ID ) ) . '</loc>';
        echo '<lastmod>' . get_the_modified_date( 'Y-m-d', $page->ID ) . '</lastmod>';
        echo '<changefreq>monthly</changefreq>';
        echo '<priority>0.6</priority>';
        echo '</url>';
    }

    echo '</urlset>';

    // Important: Exit after sending the response.
    exit;
}

The csb_register_block_type function points to a build directory. This implies we’ll need a build process for our JavaScript. We also register a REST API endpoint (custom-sitemap-block/v1/sitemap) that will serve the XML. The csb_render_sitemap_xml function is a placeholder for actual sitemap generation logic. For production, you’d want to query posts, pages, custom post types, and potentially include logic for taxonomies or custom URLs.

Gutenberg Block Development (React)

We’ll use the WordPress `@wordpress/scripts` package for building our JavaScript. This handles Babel and Webpack configuration. First, initialize your project for npm:

Project Structure

Create the following structure within your plugin directory:

custom-sitemap-block/
├── build/
├── src/
│   ├── index.js
│   └── editor.scss
├── package.json
├── custom-sitemap-block.php
└── readme.txt

package.json

{
    "name": "custom-sitemap-block",
    "version": "1.0.0",
    "description": "A Gutenberg block to dynamically generate and display an XML sitemap using HTMX.",
    "main": "build/index.js",
    "scripts": {
        "build": "wp-scripts build",
        "start": "wp-scripts start"
    },
    "keywords": [
        "wordpress",
        "gutenberg",
        "block",
        "sitemap",
        "htmx"
    ],
    "author": "Antigravity",
    "license": "GPL-2.0-or-later",
    "devDependencies": {
        "@wordpress/scripts": "^26.8.0"
    }
}

Run npm install in your plugin directory to install the development dependencies. Then, run npm run build to compile the JavaScript and CSS. npm run start will watch for changes and recompile automatically.

src/index.js – Block Registration

This file registers the Gutenberg block. We’ll define its attributes, edit function, and save function. For a dynamic block, the save function will return null, indicating that the block’s content is rendered server-side.

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './editor.scss'; // For editor styles

// Import the Edit component
import Edit from './edit';
// Import the Save component (will be null for dynamic blocks)
import save from './save';

registerBlockType( 'custom-sitemap-block/sitemap-generator', {
    title: __( 'XML Sitemap Generator', 'custom-sitemap-block' ),
    icon: 'admin-site-alt3', // Choose an appropriate icon
    category: 'widgets', // Or 'design', 'text', etc.
    attributes: {
        // No attributes needed for this dynamic block if all logic is server-side
        // and HTMX handles the rendering.
    },
    edit: Edit,
    save: () => null, // Dynamic block: content is rendered server-side.
} );

src/edit.js – Block Editor Interface

The edit.js file defines how the block appears in the Gutenberg editor. Since the sitemap itself is generated dynamically and displayed via HTMX, the editor view can be quite simple. It will primarily serve as a placeholder and a trigger for the HTMX request.

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';

export default function Edit() {
    const blockProps = useBlockProps();

    // The core of our dynamic block in the editor.
    // We use HTMX attributes to fetch the sitemap XML from our REST API endpoint.
    // 'hx-get' specifies the URL to fetch.
    // 'hx-trigger' defines when the request should be made (e.g., 'load' for on page load).
    // 'hx-swap' dictates how the response should be placed into the DOM (e.g., 'innerHTML').
    // The target div will be populated with the sitemap XML.
    return (
        <div { ...blockProps }>
            <div
                class="sitemap-output"
                hx-get={ "/wp-json/custom-sitemap-block/v1/sitemap" }
                hx-trigger="load"
                hx-swap="innerHTML"
                hx-headers='{"X-WP-Nonce": "' + wpApiSettings.nonce + '"}' // Include nonce for security
            >
                <p>{ __( 'Loading sitemap...', 'custom-sitemap-block' ) }</p>
            </div>
        </div>
    );
}

In this edit.js, we use useBlockProps for standard block wrapper attributes. The key is the div with HTMX attributes:

  • hx-get: Points to our REST API endpoint. Note the use of /wp-json/ which is standard for WordPress REST API.
  • hx-trigger="load": This tells HTMX to make the request as soon as the block is loaded in the editor.
  • hx-swap="innerHTML": The response from the API (our XML) will replace the content of this div.
  • hx-headers='{"X-WP-Nonce": "' + wpApiSettings.nonce + '"}': This is crucial for security. It injects the WordPress nonce into the request headers, allowing the REST API to verify the request’s authenticity. wpApiSettings.nonce is globally available in the WordPress admin context.

src/save.js – Block Save Function

As mentioned, for dynamic blocks, the save function in index.js returns null. We still need a corresponding save.js file, even if it’s minimal.

export default function save() {
    return null;
}

src/editor.scss – Editor Styles

Add some basic styling for the editor view.

.wp-block-custom-sitemap-block-sitemap-generator {
    border: 1px dashed #ccc;
    padding: 15px;
    background-color: #f9f9f9;
    min-height: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;

    .sitemap-output {
        width: 100%;
        pre { // Style for preformatted text if we decide to display XML raw
            white-space: pre-wrap;
            word-wrap: break-word;
            text-align: left;
            background: #eee;
            padding: 10px;
            border-radius: 4px;
        }
    }
}

Implementing the Server-Side Rendering (REST API)

The custom-sitemap-block.php file already contains the basic structure for the REST API endpoint and the rendering function csb_render_sitemap_xml. Let’s refine this function for a more robust, albeit still simplified, sitemap generation.

Refining csb_render_sitemap_xml

/**
 * Renders the XML sitemap.
 * This function queries posts, pages, and custom post types.
 */
function csb_render_sitemap_xml() {
    // Ensure this is only accessed via GET.
    if ( 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
        status_header( 405 ); // Method Not Allowed
        echo '<error>Method Not Allowed</error>';
        exit;
    }

    // Verify nonce for security.
    if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'wp_rest' ) ) {
        // Note: For HTMX GET requests, nonce is typically passed in headers.
        // If using query params, adjust verification.
        // For simplicity here, we'll assume nonce is handled by wpApiSettings.nonce in JS.
        // A more robust check would involve checking $_SERVER['HTTP_X_WP_NONCE']
        // if the JS correctly sends it.
        // For this example, we rely on the JS sending the nonce via headers.
        // If the REST API endpoint itself is accessed directly without nonce,
        // it might pass. The JS ensures it's sent.
    }

    // Set the content type to XML.
    header( 'Content-Type: application/xml; charset=utf-8' );

    echo '<?xml version="1.0" encoding="UTF-8"?>';
    echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

    // 1. Homepage
    echo '<url>';
    echo '<loc>' . esc_url( home_url( '/' ) ) . '</loc>';
    echo '<lastmod>' . date( 'Y-m-d\TH:i:sP', current_time( 'timestamp' ) ) . '</lastmod>'; // ISO 8601 format
    echo '<changefreq>daily</changefreq>';
    echo '<priority>1.0</priority>';
    echo '</url>';

    // 2. Posts
    $post_args = array(
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => -1, // Get all posts
        'orderby'        => 'modified',
        'order'          => 'DESC',
    );
    $post_query = new WP_Query( $post_args );
    if ( $post_query->have_posts() ) {
        while ( $post_query->have_posts() ) {
            $post_query->the_post();
            $lastmod = get_post_modified_time( 'Y-m-d\TH:i:sP', true, get_post() ); // Use true for GMT
            echo '<url>';
            echo '<loc>' . esc_url( get_permalink() ) . '</loc>';
            echo '<lastmod>' . $lastmod . '</lastmod>';
            echo '<changefreq>weekly</changefreq>';
            echo '<priority>0.8</priority>';
            echo '</url>';
        }
        wp_reset_postdata();
    }

    // 3. Pages
    $page_args = array(
        'post_type'      => 'page',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'orderby'        => 'modified',
        'order'          => 'DESC',
    );
    $page_query = new WP_Query( $page_args );
    if ( $page_query->have_posts() ) {
        while ( $page_query->have_posts() ) {
            $page_query->the_post();
            $lastmod = get_post_modified_time( 'Y-m-d\TH:i:sP', true, get_post() );
            echo '<url>';
            echo '<loc>' . esc_url( get_permalink() ) . '</loc>';
            echo '<lastmod>' . $lastmod . '</lastmod>';
            echo '<changefreq>monthly</changefreq>';
            echo '<priority>0.6</priority>';
            echo '</url>';
        }
        wp_reset_postdata();
    }

    // 4. Custom Post Types (Example: 'product')
    $cpt_args = array(
        'post_type'      => 'product', // Replace 'product' with your CPT slug
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'orderby'        => 'modified',
        'order'          => 'DESC',
    );
    $cpt_query = new WP_Query( $cpt_args );
    if ( $cpt_query->have_posts() ) {
        while ( $cpt_query->have_posts() ) {
            $cpt_query->the_post();
            $lastmod = get_post_modified_time( 'Y-m-d\TH:i:sP', true, get_post() );
            echo '<url>';
            echo '<loc>' . esc_url( get_permalink() ) . '</loc>';
            echo '<lastmod>' . $lastmod . '</lastmod>';
            echo '<changefreq>weekly</changefreq>'; // Adjust as needed
            echo '<priority>0.7</priority>'; // Adjust as needed
            echo '</url>';
        }
        wp_reset_postdata();
    }

    // Add taxonomies, custom URLs, etc. here as needed.

    echo '</urlset>';

    exit;
}

Key improvements:

  • Nonce Verification: While the JS sends the nonce via headers, a server-side check is good practice. The current implementation relies on the JS sending it correctly. For direct access to the REST endpoint, you’d need a different nonce mechanism or stricter permissions.
  • ISO 8601 Date Format: Sitemap protocol recommends YYYY-MM-DDTHH:MM:SS+HH:MM. We use date( 'Y-m-d\TH:i:sP', current_time( 'timestamp' ) ) and get_post_modified_time( 'Y-m-d\TH:i:sP', true, get_post() ) for this. The true argument in get_post_modified_time ensures GMT time.
  • WP_Query: Using WP_Query is more robust than get_posts for complex queries and allows for better control over post types, status, and ordering.
  • Custom Post Types: Added an example for querying a ‘product’ CPT. You’ll need to adapt this for your specific CPTs.
  • Error Handling: Basic check for GET method.

Testing the Block

1. Activate the “Custom Sitemap Block” plugin in your WordPress admin.

2. Navigate to a page or post where you want to add the sitemap (e.g., a dedicated “Sitemap” page).

3. Open the Gutenberg editor.

4. Search for “XML Sitemap Generator” and add the block.

5. In the editor view, you should see “Loading sitemap…” initially, followed by the generated XML sitemap content fetched dynamically via HTMX.

6. Save the post/page and view it on the front end. If you’ve placed the block on a public page, the sitemap content will be rendered there. Note that the sitemap protocol itself is typically served directly from the server root (e.g., sitemap.xml), not embedded within a page. This block is more for an administrative or preview purpose within the editor, or for embedding a sitemap *representation* on a page.

Further Enhancements and Considerations

Security: The permission_callback in register_rest_route should be more restrictive in production. Currently, __return_true allows anyone to access the endpoint. Consider checking user capabilities (e.g., current_user_can('manage_options')).

Performance: For very large sites, generating the sitemap on every load (even via HTMX) might still be slow. Consider:

  • Caching: Implement transient API caching for the generated XML.
  • Scheduled Generation: Use WordPress cron jobs to pre-generate the sitemap and store it as a static file or in a transient, serving that instead of generating on the fly.
  • Excluding Content: Add options to the block’s inspector controls (in edit.js) to allow users to exclude specific post types, categories, or individual posts/pages.

User Interface: The current editor view is basic. You could enhance edit.js to display the sitemap in a more human-readable format (e.g., a table of URLs) rather than raw XML, perhaps using a server-side rendered preview or a client-side parser if the XML is small enough.

HTMX Configuration: Explore advanced HTMX features like polling (hx-trigger="every 5s") if you need the sitemap to refresh periodically, or use HTMX extensions for more complex interactions.

Sitemap Protocol Compliance: For a fully compliant sitemap, you’d need to handle sitemaps for different content types (e.g., image sitemaps, video sitemaps) and potentially index files if the sitemap exceeds the 50,000 URL limit.

By integrating HTMX with a custom Gutenberg block and WordPress REST API, we’ve created a dynamic and interactive solution for managing and previewing XML sitemaps directly within the WordPress editor, offering a glimpse into more modern, client-driven approaches within the WordPress ecosystem.

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