Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using Vanilla CSS shadow DOM style layers
Leveraging Shadow DOM for Isolated Gutenberg Block Styling
When developing custom Gutenberg blocks, maintaining style isolation is paramount. Uncontrolled CSS can easily cascade and interfere with the WordPress admin interface or other plugins. While WordPress provides mechanisms for block-specific CSS, a more robust solution for truly encapsulated styles lies within the Shadow DOM. This guide details the construction of a custom XML sitemap generator block, focusing on applying styles via Shadow DOM’s style layers, ensuring complete isolation.
Plugin Structure and Block Registration
We’ll begin by setting up a basic WordPress plugin structure and registering our custom block. This involves creating a main plugin file and a JavaScript file for the block’s editor interface.
Create a new directory in your WordPress plugins folder, e.g., wp-content/plugins/custom-sitemap-generator. Inside this directory, create two files:
custom-sitemap-generator.php(main plugin file)build/index.js(compiled JavaScript for the block)src/index.js(source JavaScript for the block)src/editor.scss(SCSS for the block editor)src/style.scss(SCSS for the frontend)package.json(for build tools)
The custom-sitemap-generator.php file will handle plugin activation and enqueueing our block’s assets. For simplicity in this example, we’ll assume a build process (like Webpack or `@wordpress/scripts`) is set up to compile src/index.js and src/editor.scss into build/index.js and build/index.asset.php respectively. The src/style.scss will be handled separately for frontend styles.
Here’s the content for custom-sitemap-generator.php:
<?php
/**
* Plugin Name: Custom Sitemap Generator
* Description: A custom Gutenberg block for generating XML sitemaps with Shadow DOM styling.
* 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', array(
'editor_script' => 'custom-sitemap-generator-editor-script',
'editor_style' => 'custom-sitemap-generator-editor-style',
'style' => 'custom-sitemap-generator-style',
) );
}
add_action( 'init', 'custom_sitemap_generator_block_init' );
/**
* Enqueue block editor assets.
*/
function custom_sitemap_generator_enqueue_editor_assets() {
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
wp_enqueue_script(
'custom-sitemap-generator-editor-script',
plugins_url( 'build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_style(
'custom-sitemap-generator-editor-style',
plugins_url( 'build/index.css', __FILE__ ), // Assuming SCSS compiles to index.css
array(),
$asset_file['version']
);
}
add_action( 'enqueue_block_editor_assets', 'custom_sitemap_generator_enqueue_editor_assets' );
/**
* Enqueue frontend assets.
*/
function custom_sitemap_generator_enqueue_frontend_assets() {
wp_enqueue_style(
'custom-sitemap-generator-style',
plugins_url( 'build/style-index.css', __FILE__ ), // Assuming SCSS compiles to style-index.css
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
);
}
add_action( 'wp_enqueue_scripts', 'custom_sitemap_generator_enqueue_frontend_assets' );
Gutenberg Block Registration and Shadow DOM Integration
The core of our block will be defined in src/index.js. Here, we’ll register the block and, crucially, instruct Gutenberg to render it within a Shadow DOM root. This is achieved by setting the supports.html property to false and utilizing the render_block hook in PHP to attach the Shadow DOM.
First, let’s define the block’s attributes and its editor interface. We’ll need attributes to store sitemap settings, such as post types to include and update frequency.
// src/index.js
const { registerBlockType } = wp.blocks;
const { InspectorControls, useBlockProps } = wp.blockEditor;
const { PanelBody, ToggleControl, SelectControl } = wp.components;
const { useState } = wp.element;
import './editor.scss';
import './style.scss';
registerBlockType('custom-sitemap-generator/block', {
title: 'Custom Sitemap Generator',
icon: 'admin-site-alt3',
category: 'widgets',
attributes: {
includePosts: {
type: 'boolean',
default: true,
},
includePages: {
type: 'boolean',
default: true,
},
updateFrequency: {
type: 'string',
default: 'daily',
},
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps({
// This is where we'll attach our Shadow DOM root later via PHP.
// For now, we'll use a placeholder class for identification.
className: 'custom-sitemap-generator-block',
});
return (
<>
<InspectorControls>
<PanelBody title="Sitemap Settings" initialOpen={ true }>
<ToggleControl
label="Include Posts"
checked={ attributes.includePosts }
onChange={ ( value ) => setAttributes( { includePosts: value } ) }
/>
<ToggleControl
label="Include Pages"
checked={ attributes.includePages }
onChange={ ( value ) => setAttributes( { includePages: value } ) }
/>
<SelectControl
label="Update Frequency"
value={ attributes.updateFrequency }
options={ [
{ label: 'Always', value: 'always' },
{ label: 'Hourly', value: 'hourly' },
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
{ label: 'Yearly', value: 'yearly' },
{ label: 'Never', value: 'never' },
] }
onChange={ ( value ) => setAttributes( { updateFrequency: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<p>Custom Sitemap Generator Block (Editor View)</p>
<p>Settings will be applied here.</p>
</div>
</>
);
},
save: function(props) {
const blockProps = useBlockProps.save({
className: 'custom-sitemap-generator-block',
});
return (
<div { ...blockProps }>
<p>Custom Sitemap Generator Block (Frontend View)</p>
<p>Sitemap generation logic will be applied here.</p>
</div>
);
},
});
Server-Side Rendering with Shadow DOM Attachment
To enable Shadow DOM, we need to intercept the block’s rendering on the server-side and attach a Shadow Root. This is done using the render_block filter in PHP. We’ll look for our specific block and then use JavaScript to attach the Shadow DOM after the block’s HTML has been rendered.
Modify your custom-sitemap-generator.php file to include the following function:
/**
* Renders the block and attaches Shadow DOM.
*
* @param string $block_content The block content.
* @param array $block The block data.
* @return string The modified block content.
*/
function custom_sitemap_generator_render_block_with_shadow_dom( $block_content, $block ) {
// Check if it's our block and if we are in the editor context.
// For frontend rendering, we'll handle Shadow DOM attachment via JS.
if ( isset( $block['blockName'] ) && 'custom-sitemap-generator/block' === $block['blockName'] ) {
// In the editor, Gutenberg handles Shadow DOM attachment for blocks
// that are configured to use it. Our JS in src/index.js will manage this.
// For the frontend, we'll enqueue a script to attach the Shadow DOM.
// We'll add a marker class to the block's wrapper for our frontend JS to find.
// The actual Shadow DOM attachment will be done by a separate JS file.
if ( ! is_admin() ) {
// Enqueue a script to attach Shadow DOM on the frontend.
wp_enqueue_script(
'custom-sitemap-generator-shadow-dom',
plugins_url( 'build/shadow-dom-attachment.js', __FILE__ ),
array( 'wp-element' ), // Depends on wp-element for React/JSX compatibility if needed
filemtime( plugin_dir_path( __FILE__ ) . 'build/shadow-dom-attachment.js' ),
true // Load in footer
);
// Add a data attribute to the block's wrapper to signal our JS to attach Shadow DOM.
$block_content = str_replace( 'class="custom-sitemap-generator-block', 'class="custom-sitemap-generator-block" data-attach-shadow-dom="true"', $block_content );
}
}
return $block_content;
}
add_filter( 'render_block', 'custom_sitemap_generator_render_block_with_shadow_dom', 10, 2 );
Now, create a new JavaScript file, src/shadow-dom-attachment.js, to handle the Shadow DOM attachment on the frontend:
// src/shadow-dom-attachment.js
document.addEventListener('DOMContentLoaded', function() {
const blocks = document.querySelectorAll('[data-attach-shadow-dom="true"]');
blocks.forEach(block => {
// Check if Shadow DOM is already attached (e.g., by another plugin or process)
if (block.shadowRoot) {
return;
}
const shadowRoot = block.attachShadow({ mode: 'open' });
// Move the block's content into the Shadow DOM
while (block.firstChild) {
shadowRoot.appendChild(block.firstChild);
}
// Create a style element within the Shadow DOM
const style = document.createElement('style');
style.textContent = `
/* Styles for the sitemap generator block within Shadow DOM */
:host { /* Selects the host element (the block's wrapper) */
display: block;
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 15px;
background-color: #f9f9f9;
border-radius: 4px;
}
h3 { /* Example of styling a heading within the block */
color: #333;
margin-top: 0;
}
p { /* Example of styling paragraphs */
color: #555;
line-height: 1.6;
}
.sitemap-settings {
margin-top: 10px;
font-size: 0.9em;
color: #777;
}
/* Add more specific styles as needed */
`;
shadowRoot.appendChild(style);
// Remove the data attribute to prevent re-processing
block.removeAttribute('data-attach-shadow-dom');
});
});
Styling with Shadow DOM Style Layers
The power of Shadow DOM for styling lies in its encapsulation. Styles defined within the Shadow DOM do not leak out, and external styles generally do not penetrate without explicit mechanisms like CSS custom properties or specific selectors. We can leverage this by defining our block’s styles directly within the Shadow DOM, either inline or by linking to a stylesheet.
In the src/shadow-dom-attachment.js, we’ve already included a basic `