Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using Tailwind CSS isolated elements
Setting Up the WordPress Plugin and Block Environment
To begin, we need a foundational WordPress plugin structure and the necessary tools for modern block development. This involves creating a plugin directory, a main plugin file, and initializing the WordPress `@wordpress/scripts` package for compilation and asset management. We’ll use `npm` to manage our JavaScript dependencies.
First, create a new directory for your plugin within the wp-content/plugins/ directory of your WordPress installation. Let’s name it custom-sitemap-generator.
Inside this directory, create the main plugin file, custom-sitemap-generator.php.
<?php
/**
* Plugin Name: Custom Sitemap Generator Block
* Description: A Gutenberg block to generate and display a custom XML sitemap.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: custom-sitemap-generator
*/
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_sitemap_generator_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_sitemap_generator_block_init' );
Next, we need to set up the build process. Initialize an npm project in your plugin directory by running:
npm init -y
Now, install the necessary WordPress scripts package:
npm install @wordpress/scripts --save-dev
Add a build script to your package.json file:
{
"name": "custom-sitemap-generator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
Finally, create a block.json file in the root of your plugin directory. This file describes your block to WordPress.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "custom-sitemap-generator/sitemap-block",
"version": "0.1.0",
"title": "Custom Sitemap Generator",
"category": "widgets",
"icon": "admin-site-alt3",
"description": "Generates and displays a custom XML sitemap.",
"keywords": ["sitemap", "seo", "xml"],
"attributes": {
"maxPosts": {
"type": "number",
"default": 100
},
"postTypes": {
"type": "array",
"default": ["post", "page"]
}
},
"textdomain": "custom-sitemap-generator",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
With these files in place, you can now run npm run build to compile your block assets. This will create a build directory containing the compiled JavaScript and CSS files.
Developing the Block’s Editor Interface
The block’s editor interface is where users will configure its settings. We’ll use React components provided by the `@wordpress/block-editor` and `@wordpress/components` packages. For styling, we’ll leverage Tailwind CSS, but we need to ensure its styles are scoped to our block’s editor and front-end output to avoid conflicts.
Create a src directory in your plugin’s root and add an index.js file inside it. This will be the entry point for your block’s JavaScript.
import { registerBlockType } from '@wordpress/blocks';
import {
InspectorControls,
useBlockProps
} from '@wordpress/block-editor';
import { PanelBody, RangeControl, CheckboxControl } from '@wordpress/components';
import './editor.scss'; // Import editor-specific styles
import './style.scss'; // Import shared styles
import metadata from '../block.json';
const Edit = ( { attributes, setAttributes } ) => {
const blockProps = useBlockProps();
const { maxPosts, postTypes } = attributes;
const handleMaxPostsChange = ( newValue ) => {
setAttributes( { maxPosts: newValue } );
};
const handlePostTypeChange = ( postType, isChecked ) => {
let newPostTypes = [...postTypes];
if ( isChecked ) {
newPostTypes.push( postType );
} else {
newPostTypes = newPostTypes.filter( pt => pt !== postType );
}
setAttributes( { postTypes: newPostTypes } );
};
// Fetch available post types (simplified for example)
// In a real-world scenario, you'd likely fetch this dynamically via WP REST API
const availablePostTypes = [
{ value: 'post', label: 'Posts' },
{ value: 'page', label: 'Pages' },
{ value: 'product', label: 'Products' }, // Example custom post type
];
return (
<>
<InspectorControls>
<PanelBody title={ metadata.title } initialOpen={ true }>
<RangeControl
label="Maximum number of posts to include"
value={ maxPosts }
onChange={ handleMaxPostsChange }
min={ 10 }
max={ 500 }
step={ 10 }
/>
<p>Include Post Types:</p>
{ availablePostTypes.map( ( type ) => (
<CheckboxControl
label={ type.label }
checked={ postTypes.includes( type.value ) }
onChange={ ( isChecked ) => handlePostTypeChange( type.value, isChecked ) }
key={ type.value }
/>
) ) }
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<p>Sitemap Preview (Editor)</p>
<p>Max Posts: { maxPosts }</p>
<p>Post Types: { postTypes.join( ', ' ) }</p>
{/* In a real scenario, you'd render a preview of the sitemap links here */}
</div>
</>
);
};
const Save = ( { attributes } ) => {
const blockProps = useBlockProps.save();
const { maxPosts, postTypes } = attributes;
return (
<div { ...blockProps }>
<p>Sitemap will be generated here on the front-end.</p>
<p>Max Posts: { maxPosts }</p>
<p>Post Types: { postTypes.join( ', ' ) }</p>
{/* The actual sitemap generation logic will be handled server-side */}
</div>
);
};
registerBlockType( metadata.name, {
edit: Edit,
save: Save,
} );
Now, let’s create the SCSS files. Create src/editor.scss and src/style.scss.
/* src/editor.scss */
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
.wp-block-custom-sitemap-generator-sitemap-block {
border: 1px dashed #ccc;
padding: 15px;
background-color: #f9f9f9;
p {
margin-bottom: 0.5em;
}
}
/* Tailwind CSS isolated elements for the editor controls */
.components-panel__body {
@apply tw-p-4; /* Example: Apply padding from Tailwind */
.components-base-control__field {
@apply tw-mb-4; /* Example: Apply margin-bottom */
label {
@apply tw-font-semibold tw-text-gray-700; /* Example: Font styles */
}
input[type="checkbox"] {
@apply tw-mr-2 tw-form-checkbox tw-text-blue-600; /* Example: Checkbox styling */
}
}
.components-range-control__label {
@apply tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2;
}
.components-range-control__slider-wrapper {
@apply tw-relative tw-h-2 tw-bg-gray-200 tw-rounded-full;
}
}
/* src/style.scss */
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
.wp-block-custom-sitemap-generator-sitemap-block {
@apply tw-border tw-border-gray-300 tw-p-6 tw-rounded-lg tw-bg-white tw-shadow-md;
p {
@apply tw-text-gray-700 tw-mb-3;
}
}
To use Tailwind CSS, you need to configure it. Create a tailwind.config.js file in your plugin’s root:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.js',
'./build/**/*.asset.php', // Include build assets if needed
'./**/*.php', // Scan PHP files for classes if using PHP-based scanning
],
theme: {
extend: {},
},
plugins: [],
// Prefix all Tailwind classes to ensure isolation
prefix: 'tw-',
// Safelist to prevent purging of essential classes if using a production build
safelist: [
'tw-p-4',
'tw-mb-4',
'tw-font-semibold',
'tw-text-gray-700',
'tw-mr-2',
'tw-form-checkbox',
'tw-text-blue-600',
'tw-block',
'tw-text-sm',
'tw-font-medium',
'tw-mb-2',
'tw-relative',
'tw-h-2',
'tw-bg-gray-200',
'tw-rounded-full',
'tw-border',
'tw-border-gray-300',
'tw-p-6',
'tw-rounded-lg',
'tw-bg-white',
'tw-shadow-md',
'tw-text-gray-700',
'tw-mb-3',
],
};
You’ll also need to install Tailwind CSS and its dependencies:
npm install tailwindcss postcss autoprefixer --save-dev
Update your package.json scripts to include Tailwind compilation. You’ll need a postcss.config.js file as well.
// postcss.config.js
module.exports = {
plugins: {
'tailwindcss/nesting': {},
'tailwindcss': {},
'autoprefixer': {},
},
};
{
"name": "custom-sitemap-generator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build && tailwindcss -i ./src/style.scss -o ./build/style-index.css --minify && tailwindcss -i ./src/editor.scss -o ./build/index.css --minify",
"start": "wp-scripts start && tailwindcss -i ./src/style.scss -o ./build/style-index.css --watch && tailwindcss -i ./src/editor.scss -o ./build/index.css --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"postcss-loader": "^7.3.3",
"tailwindcss": "^3.3.6"
}
}
Run npm run build again. The editor.scss and style.scss files will be processed by Tailwind, and the prefixed classes (e.g., tw-p-4) will be applied to the elements within the editor and front-end output respectively. The prefix: 'tw-' in tailwind.config.js is crucial for isolating styles.
Server-Side Sitemap Generation and Rendering
The block’s front-end rendering and the actual sitemap XML generation need to happen server-side. We’ll use a custom endpoint to fetch posts and generate the XML, and then the block’s save function will simply output a placeholder or a link to the generated sitemap.
First, let’s modify the Save function in src/index.js to reflect that the sitemap is generated server-side. We’ll remove the attribute display and add a placeholder.
// ... (previous imports and Edit component)
const Save = ( { attributes } ) => {
const blockProps = useBlockProps.save();
// The actual sitemap generation logic will be handled server-side via a custom endpoint.
// This save function only needs to output a static placeholder or a link.
return (
<div { ...blockProps }>
<p>Sitemap is being generated server-side.</p>
{/* Optionally, you could add a link to the sitemap XML file if it's publicly accessible */}
{/* <a href="/sitemap.xml">View Sitemap</a> */}
</div>
);
};
registerBlockType( metadata.name, {
edit: Edit,
save: Save,
} );
Now, let’s add the server-side logic. We’ll create a custom REST API endpoint to generate the sitemap XML. Add the following to your main plugin file (custom-sitemap-generator.php):
WP_REST_Server::READABLE,
'callback' => 'custom_sitemap_generator_render_sitemap',
'permission_callback' => '__return_true', // Allow public access
) );
}
add_action( 'rest_api_init', 'custom_sitemap_generator_register_route' );
/**
* Renders the sitemap XML.
*
* @param WP_REST_Request $request Full data.
* @return WP_REST_Response|\WP_Error JSON response on error, or WP_Error object.
*/
function custom_sitemap_generator_render_sitemap( WP_REST_Request $request ) {
$max_posts = intval( $request->get_param( 'max_posts' ) ?? 100 );
$post_types_param = $request->get_param( 'post_types' );
$post_types = [];
if ( is_array( $post_types_param ) ) {
$post_types = array_map( 'sanitize_key', $post_types_param );
} else {
// Default to post and page if not provided or invalid
$post_types = ['post', 'page'];
}
// Ensure we only query registered post types
$registered_post_types = get_post_types( array( 'public' => true ), 'names', 'and' );
$post_types = array_intersect( $post_types, array_keys( $registered_post_types ) );
if ( empty( $post_types ) ) {
return new WP_Error( 'invalid_post_types', __( 'No valid post types specified.', 'custom-sitemap-generator' ), array( 'status' => 400 ) );
}
$args = array(
'post_type' => $post_types,
'posts_per_page' => $max_posts,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
);
$query = new WP_Query( $args );
$posts = $query->posts;
// Build XML
$xml = new \SimpleXMLElement(' ');
// Add homepage URL
$url_element = $xml->addChild('url');
$url_element->addChild('loc', esc_url( home_url( '/' ) ));
$url_element->addChild('lastmod', date('Y-m-d\TH:i:sP')); // Current date
$url_element->addChild('changefreq', 'daily');
$url_element->addChild('priority', '1.0');
// Add posts
foreach ( $posts as $post ) {
$url_element = $xml->addChild('url');
$url_element->addChild('loc', esc_url( get_permalink( $post->ID ) ));
$url_element->addChild('lastmod', get_post_modified_time( 'Y-m-d\TH:i:sP', true, $post ) );
$url_element->addChild('changefreq', 'weekly'); // Default changefreq
$url_element->addChild('priority', '0.8'); // Default priority
}
$response = new WP_REST_Response( $xml->asXML() );
$response->set_content_type( 'application/xml' );
return $response;
}
/**
* Enqueues block assets.
*/
function custom_sitemap_generator_enqueue_block_assets() {
// Enqueue Tailwind CSS for the front-end if needed, or rely on the block's style.css
// For this example, we assume style.css handles the front-end styling.
}
add_action( 'wp_enqueue_scripts', 'custom_sitemap_generator_enqueue_block_assets' );
/**
* Filters the block's attributes to pass them to the front-end rendering.
* This is a workaround to pass attributes to the server-side rendering.
* A more robust solution might involve a dedicated shortcode or block render_callback.
*/
function custom_sitemap_generator_render_block( $block_content, $block ) {
if ( 'custom-sitemap-generator/sitemap-block' === $block['blockName'] ) {
$attributes = $block['attrs'];
$max_posts = $attributes['maxPosts'] ?? 100;
$post_types = $attributes['postTypes'] ?? ['post', 'page'];
// Construct the URL for the REST API endpoint
$sitemap_url = rest_url( 'custom-sitemap-generator/v1/sitemap' );
$sitemap_url = add_query_arg( array(
'max_posts' => $max_posts,
'post_types' => $post_types,
), $sitemap_url );
// Replace the placeholder content with a link to the sitemap
$block_content = sprintf(
'
Your custom sitemap is available here.
',
esc_attr( implode( ' ', get_block_wrapper_attributes() ) ),
esc_url( $sitemap_url )
);
}
return $block_content;
}
add_filter( 'render_block', 'custom_sitemap_generator_render_block', 10, 2 );
?>
In the PHP code above:
- We register a REST API route at
/custom-sitemap-generator/v1/sitemap. - The
custom_sitemap_generator_render_sitemapfunction fetches posts based on parameters (max_posts,post_types) passed in the request. - It constructs an XML string using
SimpleXMLElement, including the homepage and fetched posts. - The response is set with the
application/xmlcontent type. - The
custom_sitemap_generator_render_blockfilter hook intercepts the rendering of our specific block. It constructs the URL to our REST API endpoint, passing the block’s attributes as query parameters. It then replaces the placeholder content with a link to this generated sitemap.
After running npm run build and activating the plugin, you should be able to add the “Custom Sitemap Generator” block to a page or post. In the editor, you can configure the maximum number of posts and select post types. On the front-end, the block will display a link to the generated XML sitemap, which can be accessed via the constructed REST API URL.
Advanced Considerations and Enhancements
For a production-ready solution, consider the following enhancements:
- Caching: The sitemap generation can be resource-intensive. Implement caching (e.g., using WordPress Transients API or a dedicated caching plugin) to store the generated XML for a set period, reducing database load and improving response times.
- Error Handling: Enhance error handling in the REST API callback. Provide more specific error messages and appropriate HTTP status codes.
- Security: While
permission_callback: '__return_true'is used for simplicity, for sensitive data, you might want to restrict access based on user roles or capabilities. - Sitemap Index: For very large sites, consider generating a sitemap index file that links to multiple individual sitemap files (e.g., sitemap-posts.xml, sitemap-pages.xml).
- Customization Options: Expose more options via block attributes, such as
changefreq,priority, and inclusion/exclusion rules for specific posts or categories. - Dynamic Post Type Fetching: Instead of hardcoding available post types, fetch them dynamically using
get_post_types()within theEditcomponent, potentially via a separate REST API endpoint for better maintainability. - Front-end Styling: Ensure the
style.scssfile correctly applies Tailwind CSS classes to the front-end output, using the prefixed classes (e.g.,tw-border,tw-p-6) to maintain isolation. - SEO Best Practices: Ensure the generated sitemap adheres strictly to the sitemaps protocol. Include
<xhtml:link>tags for multilingual sites, and consider adding images or video sitemap extensions if applicable.
By following these steps, you can build a robust and customizable XML sitemap generator block for Gutenberg, leveraging modern development practices and ensuring style isolation with Tailwind CSS.