Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using Svelte standalone templates
Setting Up Your SvelteKit Project for Gutenberg Integration
To build a custom Gutenberg block using Svelte, we’ll leverage SvelteKit for its robust project structure and build tooling. This approach allows us to manage our Svelte components and their build process efficiently. We’ll start by initializing a new SvelteKit project and configuring it to output static assets suitable for WordPress integration.
First, ensure you have Node.js and npm (or yarn/pnpm) installed. Then, create a new SvelteKit project:
npm create svelte@latest my-gutenberg-block cd my-gutenberg-block npm install
Next, we need to configure SvelteKit to output static assets. Edit your svelte.config.js file. We’ll set the adapter to adapter-static, which is ideal for generating files that can be directly included in a WordPress theme or plugin.
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
// Default options are:
// pages: 'build',
// assets: 'build',
// fallback: null
}),
paths: {
base: '/wp-content/plugins/my-custom-blocks/build', // Adjust this to your plugin's asset path
}
}
};
export default config;
The paths.base configuration is crucial. It tells SvelteKit where your compiled assets will reside within your WordPress installation. Adjust this path to match where you intend to place your plugin’s compiled JavaScript and CSS files.
Now, let’s configure Vite for optimal output. In vite.config.js, ensure the build.outDir is set correctly and that we’re generating a single entry point for our block’s JavaScript.
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
outDir: '../my-custom-blocks/build', // Relative path to the plugin's build directory
rollupOptions: {
input: {
'editor': 'src/editor.js', // Entry point for Gutenberg editor script
'frontend': 'src/frontend.js' // Entry point for frontend script (if needed)
},
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name]-[hash].[ext]'
}
}
}
});
We’ve defined two entry points: editor.js for the Gutenberg editor script and frontend.js for any scripts that need to run on the front end of the site. The output configuration ensures predictable file naming.
Creating the Svelte Component for the XML Sitemap Block
Let’s design the Svelte component that will form the core of our XML Sitemap generator block. This component will handle the user interface for configuring sitemap settings and will eventually trigger the generation process. For this example, we’ll keep it simple, focusing on a basic configuration option.
Create a new Svelte file at src/lib/SitemapGenerator.svelte:
<!-- src/lib/SitemapGenerator.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let settings = {
postTypes: ['post', 'page'],
includeImages: false,
priority: '0.8'
};
function updateSettings(key, value) {
settings = { ...settings, [key]: value };
dispatch('update', settings);
}
function generateSitemap() {
// In a real scenario, this would trigger an AJAX request to a WP REST API endpoint
// or a server-side function to generate the sitemap.
console.log('Generating sitemap with settings:', settings);
alert('Sitemap generation initiated. Check console for details.');
}
</script>
<div class="svelte-sitemap-generator">
<h3>XML Sitemap Settings</h3>
<div class="setting-group">
<label>
Post Types:
<select multiple bind:value={settings.postTypes} on:change={(e) => updateSettings('postTypes', Array.from(e.target.selectedOptions, option => option.value))} style="height: 80px;">
<option value="post">Posts</option>
<option value="page">Pages</option>
<option value="custom_post_type">Custom Post Type</option>
</select>
</label>
</div>
<div class="setting-group">
<label>
<input type="checkbox" bind:checked={settings.includeImages} on:change={(e) => updateSettings('includeImages', e.target.checked)} />
Include Images
</label>
</div>
<div class="setting-group">
<label>
Default Priority:
<input type="number" min="0" max="1.0" step="0.1" bind:value={settings.priority} on:input={(e) => updateSettings('priority', parseFloat(e.target.value))} />
</label>
</div>
<button on:click={generateSitemap}>Generate Sitemap</button>
</div>
<style>
.svelte-sitemap-generator {
font-family: sans-serif;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
.setting-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="checkbox"] {
margin-right: 5px;
}
input[type="number"] {
width: 60px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
select {
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
width: 100%;
}
button {
padding: 10px 15px;
background-color: #0073aa;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background-color: #005177;
}
</style>
This Svelte component defines a simple form with options for selecting post types, including images, and setting a default priority. The bind:value and bind:checked directives in Svelte handle two-way data binding, automatically updating the component’s state as the user interacts with the form. The updateSettings function is called on changes to propagate the updated settings upwards via a custom event.
Integrating Svelte with Gutenberg: The Editor Script
Now, we need to bridge our Svelte component with the WordPress Gutenberg editor. This involves creating a JavaScript file that registers the Gutenberg block and mounts our Svelte component into the block’s edit interface.
Create the src/editor.js file:
// src/editor.js
import { registerBlockType } from '@wordpress/blocks';
import { createElement, render } from '@wordpress/element';
import App from './lib/SitemapGenerator.svelte'; // Import our Svelte component
// Load the WordPress localization data
import { __ } from '@wordpress/i18n';
// Load the block styles
import './style.scss'; // We'll create this file next
registerBlockType('my-custom-blocks/sitemap-generator', {
title: __('XML Sitemap Generator', 'my-custom-blocks'),
icon: 'chart-bar', // Choose an appropriate Dashicon
category: 'widgets', // Or 'design', 'plugins', etc.
attributes: {
// Define attributes to store block settings persistently
settings: {
type: 'object',
default: {
postTypes: ['post', 'page'],
includeImages: false,
priority: '0.8'
},
},
},
edit: ({ attributes, setAttributes }) => {
const { settings } = attributes;
// Function to handle updates from the Svelte component
const handleUpdate = (newSettings) => {
setAttributes({ settings: newSettings });
};
// Render the Svelte component into the block's edit area
return createElement(App, {
settings: settings,
on: {
update: handleUpdate,
},
});
},
save: ({ attributes }) => {
// The save function determines what is rendered to the frontend.
// For a block that triggers server-side actions, we might save minimal HTML
// or a placeholder, and rely on JavaScript to handle the actual functionality.
// Here, we'll save the settings as a JSON attribute for potential frontend use.
const { settings } = attributes;
return createElement('div', {
'data-sitemap-settings': JSON.stringify(settings),
class: 'sitemap-generator-frontend'
}, __('Sitemap Generator Configuration (Frontend)', 'my-custom-blocks'));
},
});
In this file:
- We import necessary functions from
@wordpress/blocksand@wordpress/element. registerBlockTypeis the core function to define a new Gutenberg block.- We define the block’s
title,icon, andcategory. - The
attributesobject is crucial for saving block data. We define asettingsattribute to store our sitemap configuration. - The
editfunction is responsible for rendering the block’s interface in the editor. Here, we usecreateElementto render our SvelteAppcomponent. We pass the currentsettingsas props and set up an event listener for theupdateevent emitted by the Svelte component. - The
savefunction defines how the block’s content is saved to the database and rendered on the frontend. For a block that primarily triggers server-side actions, we can save a simple placeholder and store the configuration in adata-*attribute. This attribute can then be read by a frontend script (frontend.js) to initialize functionality or pass data to the backend.
Styling the Block
Good styling is essential for a user-friendly Gutenberg block. We’ll create a basic SCSS file for our block’s editor styles.
Create src/style.scss:
/* src/style.scss */
.wp-block-my-custom-blocks-sitemap-generator {
border: 1px dashed #ccc;
padding: 10px;
background-color: #f0f0f0;
h3 {
margin-top: 0;
color: #333;
}
.setting-group {
margin-bottom: 10px;
}
label {
display: flex;
align-items: center;
margin-bottom: 5px;
font-weight: bold;
}
input[type="checkbox"] {
margin-right: 8px;
}
input[type="number"],
select {
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
margin-left: 10px;
}
button {
padding: 8px 12px;
background-color: #0073aa;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.9em;
margin-top: 10px;
}
button:hover {
background-color: #005177;
}
}
/* Styles for the frontend output */
.sitemap-generator-frontend {
padding: 15px;
border: 1px solid #eee;
background-color: #fafafa;
font-family: sans-serif;
color: #555;
}
This SCSS file provides basic styling for the block in the editor. The styles are scoped to the block’s unique class name (e.g., .wp-block-my-custom-blocks-sitemap-generator) to prevent conflicts. We also include styles for the frontend output class.
Building and Integrating the Plugin
With our Svelte component and editor script in place, it’s time to build the project and integrate it into WordPress. First, run the build command for SvelteKit:
npm run build
This command will execute the SvelteKit build process, using the configurations in svelte.config.js and vite.config.js. It will output the compiled JavaScript and CSS files into the directory specified by build.outDir in vite.config.js (e.g., ../my-custom-blocks/build).
Now, let’s create the basic structure of our WordPress plugin. Create a new folder named my-custom-blocks in your WordPress installation’s wp-content/plugins/ directory. Inside this folder, create a main plugin file, for example, my-custom-blocks.php.
<?php
/**
* Plugin Name: My Custom XML Sitemap Block
* Description: A custom Gutenberg block to generate XML sitemaps using Svelte.
* Version: 1.0.0
* Author: Your Name
* Text Domain: my-custom-blocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Enqueue Gutenberg block assets.
*/
function my_custom_blocks_enqueue_assets() {
// Register block editor script
wp_register_script(
'my-custom-blocks-editor-script',
plugins_url( 'build/editor.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-editor' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/editor.js' )
);
// Register block editor styles
wp_register_style(
'my-custom-blocks-editor-style',
plugins_url( 'build/style.css', __FILE__ ), // Assuming Vite outputs style.css
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style.css' )
);
// Register block frontend script (if needed)
// wp_register_script(
// 'my-custom-blocks-frontend-script',
// plugins_url( 'build/frontend.js', __FILE__ ),
// array(), // Dependencies
// filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
// );
// Enqueue the block editor script and styles
register_block_type( 'my-custom-blocks/sitemap-generator', array(
'editor_script' => 'my-custom-blocks-editor-script',
'editor_style' => 'my-custom-blocks-editor-style',
// 'script' => 'my-custom-blocks-frontend-script', // Uncomment if you have a frontend script
) );
}
add_action( 'init', 'my_custom_blocks_enqueue_assets' );
/**
* Add REST API endpoint for sitemap generation.
*/
function my_custom_blocks_register_rest_route() {
register_rest_route( 'my-custom-blocks/v1', '/generate-sitemap', array(
'methods' => 'POST',
'callback' => 'my_custom_blocks_handle_sitemap_generation',
'permission_callback' => function () {
// Ensure only authenticated users with 'edit_posts' capability can access.
return current_user_can( 'edit_posts' );
},
) );
}
add_action( 'rest_api_init', 'my_custom_blocks_register_rest_route' );
/**
* Callback function to handle sitemap generation.
*
* @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 my_custom_blocks_handle_sitemap_generation( WP_REST_Request $request ) {
$settings = $request->get_json_params(); // Get settings from the request body
// Basic validation
if ( ! isset( $settings['postTypes'] ) || ! is_array( $settings['postTypes'] ) ) {
return new WP_Error( 'invalid_settings', __( 'Invalid post types provided.', 'my-custom-blocks' ), array( 'status' => 400 ) );
}
// --- Sitemap Generation Logic ---
// This is where you would implement the actual sitemap generation.
// You would query posts based on $settings['postTypes'], format them into XML,
// and potentially save the sitemap to a file or return its content.
// Example: Fetching posts of a specific type
$args = array(
'post_type' => $settings['postTypes'],
'posts_per_page' => -1, // Fetch all posts
'post_status' => 'publish',
);
$posts = get_posts( $args );
if ( empty( $posts ) ) {
return new WP_Error( 'no_posts', __( 'No posts found for the selected post types.', 'my-custom-blocks' ), array( 'status' => 404 ) );
}
// Construct XML (simplified example)
$xml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ( $posts as $post ) {
$url = get_permalink( $post->ID );
$lastmod = get_post_modified_time( 'c', true, $post );
$priority = isset( $settings['priority'] ) ? $settings['priority'] : '0.8'; // Use default if not set
$xml .= '<url>';
$xml .= '<loc>' . esc_url( $url ) . '</loc>';
$xml .= '<lastmod>' . esc_html( $lastmod ) . '</lastmod>';
$xml .= '<priority>' . esc_html( $priority ) . '</priority>';
// Add image sitemap entries if $settings['includeImages'] is true
if ( isset( $settings['includeImages'] ) && $settings['includeImages'] ) {
// Logic to fetch and add image URLs for the post
// This would typically involve querying post meta for image URLs
// or using a plugin like ACF or Yoast SEO.
}
$xml .= '</url>';
}
$xml .= '</urlset>';
// For this example, we'll return the XML content.
// In a production environment, you might save this to a file (e.g., sitemap.xml)
// and configure WordPress to serve it.
$response = new WP_REST_Response( $xml );
$response->set_header( 'Content-Type', 'application/xml; charset=' . get_option( 'blog_charset' ) );
return $response;
}
// Add a frontend script to handle the 'Generate Sitemap' button click
function my_custom_blocks_enqueue_frontend_script() {
// Only enqueue on the frontend if the block is present
if ( has_block( 'my-custom-blocks/sitemap-generator' ) ) {
wp_enqueue_script(
'my-custom-blocks-frontend-script',
plugins_url( 'build/frontend.js', __FILE__ ),
array( 'wp-element', 'wp-api-fetch' ), // wp-api-fetch for REST API calls
filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
);
// Localize script with REST API URL
wp_localize_script( 'my-custom-blocks-frontend-script', 'myCustomBlocks', array(
'restUrl' => esc_url_raw( rest_url( 'my-custom-blocks/v1/generate-sitemap' ) ),
) );
}
}
add_action( 'wp_enqueue_scripts', 'my_custom_blocks_enqueue_frontend_script' );
In the PHP file:
- We define the standard WordPress plugin header.
- The
my_custom_blocks_enqueue_assetsfunction registers and enqueues theeditor.jsscript andstyle.scss(compiled tostyle.cssby Vite) for the Gutenberg editor. It also registers the block type usingregister_block_type, linking the editor script and style. - We’ve added a REST API endpoint using
register_rest_route. This endpoint,/wp-json/my-custom-blocks/v1/generate-sitemap, will be used by our frontend script to trigger the sitemap generation process on the server. - The
my_custom_blocks_handle_sitemap_generationfunction is the callback for the REST API endpoint. It receives the sitemap settings from the frontend, performs basic validation, and includes placeholder logic for generating the sitemap XML. In a real-world scenario, this function would contain the complete logic for querying posts, formatting the XML, and potentially saving the sitemap file. - The
my_custom_blocks_enqueue_frontend_scriptfunction enqueues afrontend.jsscript if the block is present on the page. It also localizes the script with the REST API URL, making it accessible in JavaScript.
After running npm run build, copy the contents of your SvelteKit project’s build directory (which should be relative to your plugin’s root, e.g., ../my-custom-blocks/build) into the my-custom-blocks/build folder within your WordPress plugin directory.
Activate the “My Custom XML Sitemap Block” plugin in your WordPress admin area. You should now be able to add the “XML Sitemap Generator” block to your posts or pages. When you interact with the block in the editor, the Svelte component will render, and changes to settings will be saved as block attributes.
Frontend Interaction and Sitemap Generation
To make the “Generate Sitemap” button functional on the frontend, we need a frontend.js script that reads the saved settings and makes an API call. Create src/frontend.js:
// src/frontend.js
document.addEventListener('DOMContentLoaded', () => {
const sitemapBlocks = document.querySelectorAll('.sitemap-generator-frontend');
sitemapBlocks.forEach(block => {
const settingsJson = block.dataset.sitemapSettings;
if (!settingsJson) return;
try {
const settings = JSON.parse(settingsJson);
const generateButton = block.querySelector('button'); // Assuming button is inside the saved HTML
if (generateButton) {
generateButton.addEventListener('click', async () => {
// Ensure myCustomBlocks.restUrl is available (localized in PHP)
if (typeof myCustomBlocks === 'undefined' || !myCustomBlocks.restUrl) {
console.error('REST API URL not found.');
alert('Error: Sitemap generation endpoint not configured.');
return;
}
try {
const response = await fetch(myCustomBlocks.restUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpApiSettings.nonce // WordPress REST API nonce
},
body: JSON.stringify(settings)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const sitemapXml = await response.text();
// Handle the generated sitemap XML.
// For example, you could display it, offer a download link,
// or trigger a process to save it to a file.
console.log('Sitemap generated successfully:', sitemapXml);
alert('Sitemap generated! Check the console for the XML output.');
// Example: Offer download
const blob = new Blob([sitemapXml], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sitemap.xml';
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error generating sitemap:', error);
alert(`Failed to generate sitemap: ${error.message}`);
}
});
}
} catch (e) {
console.error('Error parsing sitemap settings:', e);
}
});
});
In the frontend.js:
- We wait for the DOM to be fully loaded.
- We select all elements with the class
.sitemap-generator-frontend, which are the placeholders saved by our block’ssavefunction. - For each block, we parse the
data-sitemap-settingsattribute to retrieve the saved configuration. - We find the “Generate Sitemap” button within the block’s saved HTML.
- An event listener is attached to the button’s click event.
- Inside the click handler, we use the browser’s
fetchAPI to send aPOSTrequest to our WordPress REST API endpoint (myCustomBlocks.restUrl, which is localized in PHP). We include the sitemap settings in the request body and the WordPress nonce for authentication. - Upon a successful response, we receive the XML content. The example demonstrates how to log it, display an alert, and offer it as a downloadable file.
Remember to update your vite.config.js to include frontend.js as an entry point if you haven’t already, and ensure it’s correctly enqueued in your PHP file.
After building your SvelteKit project again (npm run build) and copying the new frontend.js to your plugin’s build directory, you should be able to see the “Sitemap Generator Configuration (Frontend)” placeholder on the frontend. Clicking the “Generate Sitemap” button within this placeholder should now trigger the REST API call and initiate the sitemap generation process.