Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using React components
Setting Up the Development Environment
Before diving into the React component development for our Gutenberg block, a robust development environment is crucial. This involves setting up a local WordPress instance and configuring the necessary build tools. We’ll leverage the official WordPress `@wordpress/scripts` package, which provides a streamlined way to handle JavaScript compilation, linting, and other build processes.
First, ensure you have Node.js and npm (or yarn) installed. Navigate to your WordPress plugin’s root directory. If you don’t have a package.json file, initialize one:
npm init -y
Next, install the `@wordpress/scripts` package as a development dependency:
npm install @wordpress/scripts --save-dev
Now, update your package.json to include build scripts. These scripts will be used to compile your React code into a format WordPress can understand.
{
"name": "custom-xml-sitemap-block",
"version": "1.0.0",
"description": "A custom Gutenberg block for generating XML sitemaps.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": ["wordpress", "gutenberg", "react", "sitemap"],
"author": "Your Name",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
The build script will compile your source JavaScript files into the build directory, and the start script will watch for changes and recompile automatically, which is invaluable during development.
Registering the Gutenberg Block
To make your custom block available in the Gutenberg editor, you need to register it using PHP. This involves creating a main plugin file and using the register_block_type function. We’ll also enqueue our compiled JavaScript assets.
Create a main plugin file, for example, custom-xml-sitemap-block.php, in your plugin’s root directory.
<?php
/**
* Plugin Name: Custom XML Sitemap Block
* Description: A custom Gutenberg block for generating XML sitemaps.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: custom-xml-sitemap-block
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function custom_xml_sitemap_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_xml_sitemap_block_init' );
?>
This PHP code registers the block by pointing to a directory containing a block.json file. This block.json file is essential for defining the block’s metadata, including its name, title, attributes, and the location of its JavaScript and CSS assets. Let’s create this file.
Create a block.json file in the root of your plugin directory:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "custom-xml-sitemap-block/generator",
"version": "1.0.0",
"title": "XML Sitemap Generator",
"category": "widgets",
"icon": "admin-site-alt3",
"description": "Generates and displays an XML sitemap.",
"keywords": ["sitemap", "seo", "xml"],
"attributes": {
"postTypes": {
"type": "array",
"default": ["post", "page"]
},
"includeImages": {
"type": "boolean",
"default": false
},
"priority": {
"type": "number",
"default": 0.5
},
"changeFrequency": {
"type": "string",
"default": "weekly"
}
},
"textdomain": "custom-xml-sitemap-block",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
In this block.json:
name: A unique identifier for the block (namespace/block-name).title: The human-readable name displayed in the editor.category: Where the block appears in the inserter.icon: The icon for the block.attributes: Defines the data that the block will store. We’ve added attributes for selecting post types, including images, and setting priority and change frequency.editorScript: Points to the compiled JavaScript file for the editor.editorStyle: Points to the compiled CSS for the editor.style: Points to the compiled CSS for the frontend.
Developing the React Components
Now, let’s build the React components that will power our Gutenberg block. All our React source files will live in a src directory.
Create a src directory at the root of your plugin. Inside src, create an index.js file. This will be the entry point for our block’s JavaScript.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import save from './save';
import metadata from '../block.json';
registerBlockType( metadata.name, {
edit: Edit,
save,
} );
The index.js file imports the necessary functions from @wordpress/blocks, our Edit and save components, and the block.json metadata. It then registers the block type using the name from block.json and associates our Edit and save components.
The Edit Component
The Edit component is responsible for rendering the block’s interface within the Gutenberg editor. It allows users to interact with the block’s settings and see a live preview.
Create a file named edit.js inside the src directory.
// src/edit.js
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
InspectorControls,
PanelColorSettings,
} from '@wordpress/block-editor';
import { PanelBody, SelectControl, ToggleControl, RangeControl } from '@wordpress/components';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const { postTypes, includeImages, priority, changeFrequency } = attributes;
// In a real-world scenario, you'd fetch available post types dynamically.
// For this example, we'll use a static list.
const availablePostTypes = [
{ label: 'Posts', value: 'post' },
{ label: 'Pages', value: 'page' },
{ label: 'Custom Post Type 1', value: 'custom_post_type_1' },
{ label: 'Custom Post Type 2', value: 'custom_post_type_2' },
];
const changeFrequencyOptions = [
{ label: __('Always', 'custom-xml-sitemap-block'), value: 'always' },
{ label: __('Hourly', 'custom-xml-sitemap-block'), value: 'hourly' },
{ label: __('Daily', 'custom-xml-sitemap-block'), value: 'daily' },
{ label: __('Weekly', 'custom-xml-sitemap-block'), value: 'weekly' },
{ label: __('Monthly', 'custom-xml-sitemap-block'), value: 'monthly' },
{ label: __('Yearly', 'custom-xml-sitemap-block'), value: 'yearly' },
{ label: __('Never', 'custom-xml-sitemap-block'), value: 'never' },
];
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Sitemap Settings', 'custom-xml-sitemap-block' ) }>
<SelectControl
label={ __( 'Post Types', 'custom-xml-sitemap-block' ) }
value={ postTypes }
multiple={ true } // Note: Gutenberg's SelectControl doesn't natively support multi-select in this way for attributes. This would require custom implementation or a different component. For simplicity, we'll assume a single selection or a more complex UI for multi-select.
options={ availablePostTypes }
onChange={ ( newPostTypes ) => setAttributes( { postTypes: newPostTypes } ) }
/>
<ToggleControl
label={ __( 'Include Images', 'custom-xml-sitemap-block' ) }
checked={ includeImages }
onChange={ ( newIncludeImages ) => setAttributes( { includeImages: newIncludeImages } ) }
/>
<RangeControl
label={ __( 'Priority', 'custom-xml-sitemap-block' ) }
value={ priority }
onChange={ ( newPriority ) => setAttributes( { priority: newPriority } ) }
min={ 0 }
max={ 1 }
step={ 0.1 }
/>
<SelectControl
label={ __( 'Change Frequency', 'custom-xml-sitemap-block' ) }
value={ changeFrequency }
options={ changeFrequencyOptions }
onChange={ ( newChangeFrequency ) => setAttributes( { changeFrequency: newChangeFrequency } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<h3>{ __( 'XML Sitemap Preview', 'custom-xml-sitemap-block' ) }</h3>
<p>{ __( 'Settings configured in the sidebar will affect the generated sitemap.', 'custom-xml-sitemap-block' ) }</p>
<p>{ __( 'Post Types:', 'custom-xml-sitemap-block' ) } { postTypes.join(', ') }</p>
<p>{ __( 'Include Images:', 'custom-xml-sitemap-block' ) } { includeImages ? __( 'Yes', 'custom-xml-sitemap-block' ) : __( 'No', 'custom-xml-sitemap-block' ) }</p>
<p>{ __( 'Priority:', 'custom-xml-sitemap-block' ) } { priority }</p>
<p>{ __( 'Change Frequency:', 'custom-xml-sitemap-block' ) } { changeFrequency }</p>
</div>
</>
);
}
In this edit.js:
- We import necessary components from
@wordpress/i18n,@wordpress/block-editor, and@wordpress/components. useBlockPropsis a hook that provides necessary props for the block’s wrapper element.InspectorControlsis a component that renders settings in the block’s sidebar.PanelBodyis a container for settings within the sidebar.SelectControl,ToggleControl, andRangeControlare interactive form elements for our attributes.- The
Editcomponent receivesattributes(the current state of the block’s data) andsetAttributes(a function to update those attributes). - We render a preview of the sitemap based on the current attributes.
Note on SelectControl for multiple post types: The standard SelectControl in WordPress components is designed for single selections. For a true multi-select experience for post types, you would typically need to use a more advanced component or a custom implementation, possibly involving a loop of ToggleControl or a dedicated multi-select library. For this example, we’ve kept it simpler, but be aware of this limitation for production use.
The Save Component
The save component defines how the block’s content is saved to the database and rendered on the frontend. It should return a React element that represents the static HTML output.
Create a file named save.js inside the src directory.
// src/save.js
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
const { postTypes, includeImages, priority, changeFrequency } = attributes;
return (
<div { ...blockProps }>
<p>XML Sitemap Configuration:</p>
<ul>
<li>Post Types: { postTypes.join(', ') }</li>
<li>Include Images: { includeImages ? 'Yes' : 'No' }</li>
<li>Priority: { priority }</li>
<li>Change Frequency: { changeFrequency }</li>
</ul>
<p>This block will generate an XML sitemap based on these settings.</p>
</div>
);
}
The save component is simpler. It receives the attributes and renders the static HTML that will be saved. It does not include any interactive controls, as those are only for the editor experience.
Styling the Block
Gutenberg blocks can have separate styles for the editor and the frontend. We’ll create basic styles for both.
Create an editor.scss file inside the src directory for editor-specific styles.
// src/editor.scss
.wp-block-custom-xml-sitemap-block-generator {
border: 1px dashed #ccc;
padding: 15px;
background-color: #f9f9f9;
h3 {
margin-top: 0;
color: #333;
}
}
Create a style.scss file inside the src directory for frontend styles.
// src/style.scss
.wp-block-custom-xml-sitemap-block-generator {
border: 1px solid #eee;
padding: 20px;
background-color: #fff;
margin-bottom: 20px;
p {
font-size: 0.9em;
color: #555;
}
ul {
list-style: disc inside;
margin-left: 20px;
}
}
The @wordpress/scripts package automatically compiles these SCSS files into CSS files in the build directory, as specified in our block.json.
Building the Block Assets
With our React components and styles in place, it’s time to build the block assets. Navigate to your plugin’s root directory in your terminal and run the build script:
npm run build
This command will:
- Compile
src/index.js(and any imported JS/JSX) intobuild/index.js. - Compile
src/editor.scssintobuild/index.css. - Compile
src/style.scssintobuild/style-index.css.
After the build process completes, you should see a build directory containing these compiled files. Your plugin is now ready to be activated.
Implementing the XML Sitemap Generation Logic
The current block only defines the editor interface and saves the configuration. To actually generate an XML sitemap, we need server-side logic. This typically involves a custom endpoint or a cron job that generates the sitemap file.
For a truly dynamic and on-demand sitemap, you would create a REST API endpoint. For a more traditional approach, you might generate a static XML file periodically.
Let’s outline a basic approach for generating the sitemap content using PHP. This logic would typically reside in a separate PHP file or within your main plugin file, triggered by a specific action.
Here’s a conceptual PHP function to generate sitemap XML content. This would need to be integrated into your plugin’s structure, perhaps via a REST API endpoint or a WP-CLI command.
<?php
/**
* Generates the XML sitemap content.
*
* @param array $settings Block attributes from the Gutenberg block.
* @return string XML sitemap content.
*/
function generate_xml_sitemap_content( $settings ) {
$post_types = $settings['postTypes'] ?? ['post', 'page'];
$include_images = $settings['includeImages'] ?? false;
$priority = $settings['priority'] ?? 0.5;
$change_frequency = $settings['changeFrequency'] ?? 'weekly';
$xml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
// Add image namespace if needed
if ( $include_images ) {
$xml .= ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
}
$xml .= '>';
// Add homepage
$xml .= sprintf(
'<url><loc>%s</loc><lastmod>%s</lastmod><changefreq>%s</changefreq><priority>%.1f</priority></url>',
esc_url( home_url() ),
date( 'c' ), // ISO 8601 format
esc_attr( $change_frequency ),
(float) $priority
);
// Query posts
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1, // Get all posts
'orderby' => 'modified',
'order' => 'DESC',
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
global $post;
$loc = get_permalink( $post->ID );
$lastmod = get_post_modified_time( 'c', true, $post ); // ISO 8601 format
$xml .= sprintf(
'<url><loc>%s</loc><lastmod>%s</lastmod><changefreq>%s</changefreq><priority>%.1f</priority>',
esc_url( $loc ),
esc_attr( $lastmod ),
esc_attr( $change_frequency ),
(float) $priority
);
// Add images if enabled
if ( $include_images && has_post_thumbnail( $post->ID ) ) {
$image_url = esc_url( get_the_post_thumbnail_url( $post->ID, 'full' ) );
$image_caption = esc_attr( get_post( $post->ID )->post_excerpt ); // Or get_the_title()
$image_title = esc_attr( get_the_title( $post->ID ) );
if ( $image_url ) {
$xml .= '<image:image><image:loc>' . $image_url . '</image:loc>';
if ( $image_caption ) {
$xml .= '<image:caption>' . $image_caption . '</image:caption>';
}
if ( $image_title ) {
$xml .= '<image:title>' . $image_title . '</image:title>';
}
$xml .= '</image:image>';
}
}
$xml .= '</url>';
}
wp_reset_postdata();
}
$xml .= '</urlset>';
return $xml;
}
// Example of how to hook this into a REST API endpoint:
function register_sitemap_rest_route() {
register_rest_route( 'custom-xml-sitemap/v1', '/generate', array(
'methods' => 'GET',
'callback' => 'handle_sitemap_request',
'permission_callback' => '__return_true', // Adjust permissions as needed
) );
}
add_action( 'rest_api_init', 'register_sitemap_rest_route' );
function handle_sitemap_request( WP_REST_Request $request ) {
// In a real scenario, you'd fetch the settings for the block instance(s)
// or a global setting. For simplicity, we'll use default settings here.
// You might need to query post meta or options to get block-specific settings.
$settings = array(
'postTypes' => ['post', 'page'], // Example: fetch from options or block meta
'includeImages' => true,
'priority' => 0.7,
'changeFrequency' => 'daily',
);
$xml_content = generate_xml_sitemap_content( $settings );
$response = new WP_REST_Response( $xml_content, 200 );
$response->set_content_type( 'application/xml' );
return $response;
}
?>
This PHP code provides a foundation. Key considerations for a production-ready sitemap generator include:
- Dynamic Settings Retrieval: The
handle_sitemap_requestfunction currently uses hardcoded settings. In a real plugin, you’d need to retrieve the settings associated with the block instance(s) or global sitemap settings stored in the WordPress options table. - Performance: For sites with a very large number of posts, querying all posts at once (
'posts_per_page' => -1) can be slow. Consider pagination or a cron-based generation approach. - Excluding Content: You’ll likely want to add logic to exclude specific posts, pages, or custom post types based on categories, tags, or custom meta fields.
- Image Handling: The image logic is basic. You might want to include multiple images per post or handle featured images more robustly.
- Error Handling: Implement proper error checking and logging.
- Security: Ensure proper sanitization and escaping of all output. Adjust
permission_callbackfor the REST API endpoint as needed. - Caching: Implement caching for the generated sitemap to reduce server load.
- Sitemap Index: For very large sites, you’ll need to generate a sitemap index file that links to multiple individual sitemap files.
Conclusion and Next Steps
You’ve now built a custom Gutenberg block using React components that allows users to configure XML sitemap settings. The block is registered, its editor interface is functional, and the static save content is defined. The next critical step is to integrate the server-side PHP logic to actually generate the XML sitemap based on these configurations, likely via a REST API endpoint or a scheduled task.
Further enhancements could include:
- Adding support for custom post types and taxonomies.
- Implementing advanced filtering options (e.g., by date, category).
- Providing a direct link to view the generated sitemap.
- Integrating with SEO plugins for more sophisticated sitemap generation.
- Developing a WP-CLI command for manual sitemap generation and management.