Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using Vue micro-frontends
Project Setup and Dependencies
This guide details the creation of a custom Gutenberg block that generates an XML sitemap. We’ll leverage Vue.js for the block’s editor interface, treating it as a micro-frontend within the WordPress ecosystem. This approach offers a modern, component-based development experience for the block’s UI.
First, ensure you have a local WordPress development environment set up. We’ll use Composer for PHP dependencies and npm/yarn for JavaScript. Create a new plugin directory, for instance, wp-content/plugins/custom-sitemap-generator.
Initialize your project with a composer.json and package.json:
cd wp-content/plugins/custom-sitemap-generator composer init npm init -y
Next, install necessary PHP packages for WordPress plugin development and Vue.js tooling. We’ll use wp-scripts for compiling our Vue assets.
npm install @wordpress/scripts vue vue-loader vue-template-compiler --save-dev composer require --dev dealerdirect/phpcodesniffer wp-coding-standards/wpcs
Configure composer.json to include WordPress Coding Standards for PHP linting.
{
"name": "your-vendor/custom-sitemap-generator",
"description": "Custom XML Sitemap Generator Gutenberg Block",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Your Name",
"email": "[email protected]"
}
],
"require": {
"php": ">=7.4",
"dealerdirect/phpcodesniffer": "^3.7",
"wp-coding-standards/wpcs": "^3.1"
},
"autoload": {
"psr-4": {
"CustomSitemapGenerator\\": "src/"
}
},
"scripts": {
"phpcs": "phpcs --standard=WordPress --extensions=php src/ plugin.php"
}
}
Configure package.json to use wp-scripts for asset compilation. Add the following scripts:
{
"name": "custom-sitemap-generator",
"version": "1.0.0",
"description": "Custom XML Sitemap Generator Gutenberg Block",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js"
},
"keywords": ["wordpress", "gutenberg", "block", "vue", "sitemap"],
"author": "Your Name",
"license": "GPL-2.0-or-later",
"dependencies": {
"vue": "^3.2.45"
},
"devDependencies": {
"@wordpress/scripts": "^25.4.0",
"vue-loader": "^17.0.1",
"vue-template-compiler": "^2.7.14"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Create a webpack.config.js in the root of your plugin directory to configure Vue Loader.
const { getWebpackConfig } = require( '@wordpress/scripts' );
const VueLoaderPlugin = require( 'vue-loader/lib/plugin' );
module.exports = getWebpackConfig( {
entry: './src/index.js',
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
// you will need sass loader if you use scss files
// {
// test: /\.scss$/,
// use: [
// 'vue-style-loader',
// 'css-loader',
// 'sass-loader'
// ]
// }
],
},
plugins: [
// make sure to include the vloader plugin
new VueLoaderPlugin(),
],
} );
Plugin Structure and Main File
Organize your plugin files as follows:
custom-sitemap-generator/plugin.php(Main plugin file)composer.jsonpackage.jsonwebpack.config.jssrc/index.js(Gutenberg block registration)block.json(Block metadata)editor.scssstyle.scsscomponents/SitemapSettings.vue(Vue component for settings)SitemapPreview.vue(Vue component for preview)
utils/api.js(Helper for API calls)
build/(Compiled assets will go here)
Create the main plugin file, plugin.php. This file will register the Gutenberg block and enqueue necessary assets.
<?php
/**
* Plugin Name: Custom Sitemap Generator
* Description: A Gutenberg block to generate and manage XML sitemaps.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: custom-sitemap-generator
*
* @package CustomSitemapGenerator
*/
namespace CustomSitemapGenerator;
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 register_sitemap_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', __NAMESPACE__ . '\\register_sitemap_block' );
/**
* Enqueue custom scripts for the sitemap generator.
*/
function enqueue_custom_sitemap_scripts() {
// Enqueue Vue.js if not already loaded by WordPress core or other plugins.
// This is a simplified check; a more robust solution might involve checking
// registered script handles or using a CDN.
if ( ! wp_script_is( 'vue-3', 'enqueued' ) ) {
wp_enqueue_script(
'vue-3',
'https://unpkg.com/vue@3/dist/vue.global.js', // Using CDN for simplicity
array(),
'3.2.45',
true
);
}
// Enqueue our compiled block assets.
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php' );
wp_enqueue_script(
'custom-sitemap-generator-block-editor',
plugins_url( 'build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_style(
'custom-sitemap-generator-block-editor',
plugins_url( 'build/index.css', __FILE__ ),
array( 'wp-edit-blocks' ),
$asset_file['version']
);
// Localize script with data if needed, e.g., REST API nonce.
wp_localize_script(
'custom-sitemap-generator-block-editor',
'customSitemapGeneratorData',
array(
'restUrl' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
// Add any other necessary data here
)
);
}
add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\enqueue_custom_sitemap_scripts' );
/**
* Register REST API endpoint for sitemap generation.
*/
function register_sitemap_api_route() {
register_rest_route( 'custom-sitemap-generator/v1', '/generate', array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => __NAMESPACE__ . '\\handle_sitemap_generation',
'permission_callback' => function () {
return current_user_can( 'manage_options' ); // Example permission
},
) );
}
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_sitemap_api_route' );
/**
* Handles the sitemap generation request.
*
* @param \WP_REST_Request $request Full data about the request.
* @return \WP_REST_Response|\WP_Error Response object on success, or \WP_Error object on failure.
*/
function handle_sitemap_generation( \WP_REST_Request $request ) {
// In a real-world scenario, this would involve complex logic to fetch posts,
// pages, custom post types, and generate the XML.
// For this example, we'll simulate a successful generation.
$posts_to_include = $request->get_param( 'posts' ) ?: array(); // Example parameter
$pages_to_include = $request->get_param( 'pages' ) ?: array();
// Simulate generating XML content
$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></url>';
// Example: Add posts (replace with actual query)
$args = array(
'post_type' => 'post',
'posts_per_page' => -1,
'post_status' => 'publish',
);
$query = new \WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$xml_content .= '<url><loc>' . esc_url( get_permalink() ) . '</loc></url>';
}
wp_reset_postdata();
}
$xml_content .= '</urlset>';
// In a real plugin, you might save this to a file or serve it directly.
// For this example, we'll just return it.
return new \WP_REST_Response( array( 'xml' => $xml_content ), 200 );
}
// Add a filter to allow the block to be rendered server-side if needed
// add_filter( 'render_block', __NAMESPACE__ . '\\render_custom_sitemap_block', 10, 2 );
// function render_custom_sitemap_block( $block_content, $block ) {
// if ( isset( $block['blockName'] ) && 'custom-sitemap-generator/sitemap-block' === $block['blockName'] ) {
// // Logic to render the sitemap block server-side if necessary
// // For this example, we're primarily focusing on the editor experience.
// }
// return $block_content;
// }
Gutenberg Block Registration and Metadata
The block.json file defines the metadata for your Gutenberg block. This includes its name, title, category, icon, and the scripts/styles to be enqueued.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "custom-sitemap-generator/sitemap-block",
"version": "0.1.0",
"title": "XML Sitemap Generator",
"category": "widgets",
"icon": "admin-site",
"description": "Generate and manage your XML sitemap.",
"keywords": ["sitemap", "seo", "xml"],
"attributes": {
"includePosts": {
"type": "boolean",
"default": true
},
"includePages": {
"type": "boolean",
"default": true
},
"customUrls": {
"type": "array",
"default": []
}
},
"supports": {
"html": false
},
"textdomain": "custom-sitemap-generator",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
The src/index.js file is the entry point for your block’s JavaScript. It registers the block and defines its editor interface.
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './style.scss';
import './editor.scss';
import Edit from './components/Edit.vue'; // We will create this component
// Register the block
registerBlockType( 'custom-sitemap-generator/sitemap-block', {
edit: Edit,
save: () => null, // We will handle saving via REST API and not store complex data in post_content
} );
Vue.js Micro-frontend for the Editor Interface
We’ll create a Vue component, src/components/Edit.vue, which will serve as the editor interface for our block. This component will manage the block’s settings and interact with the WordPress REST API.
<template>
<div class="custom-sitemap-generator-block">
<div class="block-controls">
<h3>{{ __('Sitemap Settings', 'custom-sitemap-generator') }}</h3>
<ToggleControl
label="Include Posts"
checked={ includePosts }
onChange={ ( value ) => updateAttribute( 'includePosts', value ) }
/>
<ToggleControl
label="Include Pages"
checked={ includePages }
onChange={ ( value ) => updateAttribute( 'includePages', value ) }
/>
<!-- Add more controls for custom URLs, post types, etc. -->
<Button isPrimary onClick={ generateSitemap }>
{ __('Generate Sitemap', 'custom-sitemap-generator') }
</Button>
</div>
<div class="sitemap-preview" v-if="sitemapXml">
<h3>{{ __('Generated Sitemap (XML)', 'custom-sitemap-generator') }}</h3>
<pre>{{ sitemapXml }}</pre>
<!-- Add options to download or copy the XML -->
</div>
<div v-if="isLoading">{{ __('Generating...', 'custom-sitemap-generator') }}</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</template>
<script>
import { ToggleControl, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { useState, useEffect } from 'vue';
import api from '../utils/api'; // Our API helper
export default {
name: 'SitemapGeneratorEdit',
components: {
ToggleControl,
Button,
},
setup( props ) {
const { attributes, setAttributes } = props;
const includePosts = useState( attributes.includePosts );
const includePages = useState( attributes.includePages );
const sitemapXml = useState( '' );
const isLoading = useState( false );
const error = useState( '' );
// Update local state when attributes change
useEffect( () => {
includePosts.value = attributes.includePosts;
includePages.value = attributes.includePages;
}, [ attributes.includePosts, attributes.includePages ] );
const updateAttribute = ( key, value ) => {
setAttributes( { [key]: value } );
// Optionally trigger generation or update preview here
};
const generateSitemap = async () => {
isLoading.value = true;
error.value = '';
sitemapXml.value = '';
try {
const response = await api.post( 'custom-sitemap-generator/v1/generate', {
posts: includePosts.value,
pages: includePages.value,
// Add other parameters from attributes
} );
if ( response.data && response.data.xml ) {
sitemapXml.value = response.data.xml;
} else {
throw new Error( __('Failed to generate sitemap. Unexpected response.', 'custom-sitemap-generator') );
}
} catch ( e ) {
console.error( 'Sitemap generation error:', e );
error.value = e.message || __('An unknown error occurred.', 'custom-sitemap-generator');
} finally {
isLoading.value = false;
}
};
return {
includePosts,
includePages,
sitemapXml,
isLoading,
error,
updateAttribute,
generateSitemap,
__ // Make __ available in template
};
},
};
</script>
<style scoped>
.custom-sitemap-generator-block {
border: 1px solid #ddd;
padding: 15px;
background-color: #f9f9f9;
}
.block-controls {
margin-bottom: 20px;
}
.sitemap-preview {
margin-top: 20px;
background-color: #eee;
padding: 10px;
border: 1px solid #ccc;
overflow-x: auto;
}
.sitemap-preview pre {
white-space: pre-wrap;
word-wrap: break-word;
font-size: 0.9em;
}
.error-message {
color: red;
font-weight: bold;
}
</style>
We need to adapt the src/index.js to use this Vue component. Since Gutenberg’s `edit` prop expects a React component or a function returning JSX, we’ll use a wrapper or a library that bridges Vue and React/Gutenberg’s rendering context. A common approach is to use a simple wrapper that mounts the Vue app.
First, let’s create a Vue app instance and mount it. We’ll need to adjust src/index.js and potentially create a new entry point for the Vue app itself.
Revised src/index.js:
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { createRoot } from 'react-dom/client'; // For React 18+
// import { render } from 'react-dom'; // For React 17 and below
/**
* Internal dependencies
*/
import './style.scss';
import './editor.scss';
import VueBlockWrapper from './VueBlockWrapper'; // We will create this wrapper
// Register the block
registerBlockType( 'custom-sitemap-generator/sitemap-block', {
edit: ( { attributes, setAttributes } ) => {
// This component will render our Vue app
return <VueBlockWrapper attributes={ attributes } setAttributes={ setAttributes } />;
},
save: () => null, // We handle saving via REST API
} );
Now, create the src/VueBlockWrapper.js file. This acts as a bridge, rendering a React component that in turn mounts and manages our Vue application.
import React, { useEffect, useRef } from 'react';
import { create } from 'vue';
import SitemapGeneratorEdit from './components/Edit.vue';
const VueBlockWrapper = ( { attributes, setAttributes } ) => {
const vueAppContainer = useRef( null );
const vueAppInstance = useRef( null );
useEffect( () => {
// Ensure the container exists
if ( ! vueAppContainer.current ) {
return;
}
// If an instance already exists, update its props and re-render
if ( vueAppInstance.current ) {
vueAppInstance.current.component.props.attributes = attributes;
vueAppInstance.current.component.props.setAttributes = setAttributes;
// Vue 3 doesn't have a direct re-render method like Vue 2's $forceUpdate.
// We rely on reactivity. If props change, the component should update.
// If complex updates are needed, consider unmounting and remounting.
return;
}
// Create a new Vue app instance
const App = {
extends: SitemapGeneratorEdit, // Extend our Vue component
props: ['attributes', 'setAttributes'], // Define props to receive from React
// You might need to adjust how props are passed or accessed in Vue 3
// For Vue 3, you might directly use the props in the template or methods
};
// Mount the Vue app
vueAppInstance.current = create( App, {
props: {
attributes: attributes,
setAttributes: setAttributes,
},
} );
vueAppInstance.current.mount( vueAppContainer.current );
// Cleanup on unmount
return () => {
if ( vueAppInstance.current ) {
vueAppInstance.current.unmount();
vueAppInstance.current = null;
}
};
}, [ attributes, setAttributes ] ); // Re-run effect if attributes or setAttributes change
// Render a div that Vue will mount to
return React.createElement( 'div', { ref: vueAppContainer } );
};
export default VueBlockWrapper;
Note on Vue 3 and React Integration: The integration of Vue 3 within a React-based environment like Gutenberg can be complex. The above VueBlockWrapper.js uses a basic approach. For more robust solutions, consider libraries like @vue/reactivity or dedicated wrappers that handle prop synchronization and lifecycle management more effectively. The key is that the `edit` function in `index.js` must return a React element, and that element needs to manage the lifecycle of the Vue application.
API Interaction and Utilities
Create a utility file for handling API requests, src/utils/api.js. This will abstract the interaction with the WordPress REST API.
import axios from 'axios';
const api = axios.create( {
baseURL: customSitemapGeneratorData.restUrl, // Provided by wp_localize_script
headers: {
'X-WP-Nonce': customSitemapGeneratorData.nonce, // Provided by wp_localize_script
},
} );
export default {
get( endpoint, params ) {
return api.get( endpoint, { params } );
},
post( endpoint, data, params ) {
return api.post( endpoint, data, { params } );
},
put( endpoint, data, params ) {
return api.put( endpoint, data, { params } );
},
delete( endpoint, params ) {
return api.delete( endpoint, { params } );
},
};
Styling the Block
Add basic styles for the block editor and the frontend. Create src/editor.scss and src/style.scss.
/** editor.scss */
.custom-sitemap-generator-block {
border: 1px dashed #9b9b9b;
padding: 10px;
background-color: #f0f0f0;
text-align: center;
}
.custom-sitemap-generator-block h3 {
margin-top: 0;
font-size: 1.1em;
color: #333;
}
/* Add styles for ToggleControl, Button etc. if needed */
.block-controls .components-base-control {
margin-bottom: 10px;
}
.block-controls .components-button {
margin-top: 15px;
}
.sitemap-preview pre {
background-color: #fff;
padding: 15px;
border: 1px solid #ccc;
overflow-x: auto;
font-size: 0.85em;
max-height: 300px;
}
.error-message {
color: #dc3232;
font-weight: bold;
margin-top: 10px;
}
/** style.scss */
.custom-sitemap-generator-block {
/* Styles for the block on the frontend if it were to render something directly */
/* For this example, the block primarily acts as a control panel in the editor */
/* and doesn't render content directly to the post_content. */
/* If you wanted to display a link to the sitemap, you'd add it here. */
}
Run the build process to compile your assets:
npm run build
This will create the build/ directory with index.js, index.css, and style-index.css. The index.asset.php file will also be generated, containing dependencies and version information for enqueuing.
Testing and Refinements
Activate your “Custom Sitemap Generator” plugin in the WordPress admin area. Navigate to the post or page editor and add the “XML Sitemap Generator” block. You should see the Vue-based interface. Test the “Include Posts” and “Include Pages” toggles and click “Generate Sitemap”. If everything is configured correctly, you should see the generated XML output below.
Potential Refinements:
- Sitemap Serving: Instead of just displaying the XML in the editor, implement logic to save the generated XML to a file (e.g.,
sitemap.xmlin the WordPress root) or serve it via a dedicated REST API endpoint that returns the XML content type. - Advanced Options: Add controls for custom post types, taxonomies, date inclusion/exclusion, priority, change frequency, and custom URL entries.
- Error Handling: Improve error messages and logging for failed generation attempts.
- Security: Ensure proper nonce verification and capability checks for the REST API endpoint.
- Performance: For large sites, optimize the post/page querying logic to avoid timeouts. Consider background processing for sitemap generation.
- Vue 3 Reactivity: For more complex state management within the Vue component, explore Vue 3’s Composition API or Pinia for state management.
- Accessibility: Ensure all controls and outputs are accessible.
By integrating Vue.js as a micro-frontend, you gain a powerful and maintainable way to build complex Gutenberg block interfaces, separating concerns and leveraging modern JavaScript tooling within the WordPress ecosystem.