Step-by-Step Guide to building a custom bulk image watermarker block for Gutenberg using SolidJS high-performance reactive components
Gutenberg Block Development with SolidJS: A High-Performance Watermarking Solution
This guide details the construction of a custom Gutenberg block for WordPress that enables bulk image watermarking. We will leverage SolidJS, a modern reactive JavaScript library, to build a high-performance, user-friendly interface within the WordPress editor. This approach prioritizes efficient DOM manipulation and a declarative component model, leading to a smoother user experience compared to traditional jQuery-based solutions.
Project Setup and Dependencies
Before diving into the code, ensure your WordPress development environment is set up. We’ll use Node.js and npm (or yarn) for managing our JavaScript build process. The core tools for Gutenberg block development are `@wordpress/scripts` for compilation and `@wordpress/blocks` for block registration.
Initialize a new npm package in your theme’s or plugin’s JavaScript directory:
npm init -y npm install --save-dev @wordpress/scripts @wordpress/blocks solid-js
Next, configure your package.json to include build scripts. This will allow you to compile your SolidJS components into standard JavaScript that Gutenberg can understand.
{
"name": "gutenberg-watermarker",
"version": "1.0.0",
"description": "Gutenberg block for bulk image watermarking with SolidJS.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build --experimental-jsx --jsx-import source=solid-js",
"start": "wp-scripts start --experimental-jsx --jsx-import source=solid-js"
},
"keywords": ["wordpress", "gutenberg", "solidjs", "watermark"],
"author": "Your Name",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/blocks": "^12.0.0",
"@wordpress/scripts": "^26.0.0",
"solid-js": "^1.8.0"
}
}
The --experimental-jsx --jsx-import source=solid-js flags are crucial for enabling JSX compilation specifically for SolidJS. The build script will compile your source files into the build/ directory, and the start script will watch for changes and recompile automatically during development.
Block Registration and Server-Side Rendering
Gutenberg blocks are registered using the registerBlockType function. For blocks that require server-side processing (like image manipulation), we define both an edit and a save function. The edit function renders the block’s interface in the editor, while the save function defines how the block’s content is stored in the database. For dynamic blocks, the save function can return null, indicating that the rendering is handled entirely by a PHP callback.
Create a PHP file (e.g., gutenberg-watermarker.php) in your plugin or theme’s root directory to register the block and its server-side rendering callback.
<?php
/**
* Plugin Name: Gutenberg Watermarker Block
* Description: A custom Gutenberg block for bulk image watermarking using SolidJS.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
*/
function gutenberg_watermarker_register_block() {
register_block_type( 'gutenberg-watermarker/block', array(
'editor_script' => 'gutenberg-watermarker-editor-script',
'render_callback' => 'gutenberg_watermarker_render_callback',
'attributes' => array(
'images' => array(
'type' => 'array',
'default' => [],
),
'watermarkText' => array(
'type' => 'string',
'default' => '© My Company',
),
'watermarkPosition' => array(
'type' => 'string',
'default' => 'bottom-right',
),
'watermarkOpacity' => array(
'type' => 'number',
'default' => 0.7,
),
),
) );
}
add_action( 'init', 'gutenberg_watermarker_register_block' );
function gutenberg_watermarker_editor_assets() {
wp_enqueue_script(
'gutenberg-watermarker-editor-script',
plugins_url( 'build/index.js', __FILE__ ), // Adjust path if in theme
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
}
add_action( 'enqueue_block_editor_assets', 'gutenberg_watermarker_editor_assets' );
function gutenberg_watermarker_render_callback( $attributes ) {
// This function would typically handle the actual image processing
// and return the HTML for the watermarked images.
// For simplicity, we'll just return a placeholder.
$images = isset( $attributes['images'] ) ? $attributes['images'] : [];
$watermark_text = isset( $attributes['watermarkText'] ) ? esc_html( $attributes['watermarkText'] ) : '© My Company';
ob_start();
?>
<div class="wp-block-gutenberg-watermarker">
<p>Watermarked Images (Preview not available in frontend):</p>
<ul>
<?php foreach ( $images as $image_id ) : ?>
<li>Image ID: <?php echo $image_id; ?> (Watermarked with: <?php echo $watermark_text; ?>)</li>
</?php endforeach; ?>
</ul>
</div>
<?php
return ob_get_clean();
}
In this PHP file:
register_block_typeregisters our block with a unique namespacegutenberg-watermarker/block.- We define initial
attributesfor images, watermark text, position, and opacity. editor_scriptpoints to our compiled JavaScript file.render_callbackis set togutenberg_watermarker_render_callback, which will be executed on the frontend to display the watermarked images.enqueue_block_editor_assetsis hooked to load our editor script only when the editor is active.
SolidJS Component for the Editor Interface
Now, let’s build the SolidJS component for the block’s editor interface. This component will handle image selection, watermark configuration, and a preview of the watermarking effect. Create a new file, e.g., src/editor.jsx.
import { createSignal, For, Show } from 'solid-js';
import { registerBlockType } from '@wordpress/blocks';
import {
MediaUpload,
MediaUploadCheck,
InspectorControls,
} from '@wordpress/block-editor';
import { PanelBody, TextControl, RangeControl, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
// Placeholder for image watermarking logic
const applyWatermark = async (imageUrl, watermarkText, position, opacity) => {
// In a real-world scenario, this would involve:
// 1. Fetching the image data.
// 2. Using Canvas API or a server-side library to apply the watermark.
// 3. Returning a data URL or a processed image URL.
console.log('Applying watermark:', { imageUrl, watermarkText, position, opacity });
// Simulate a delay and return the original URL for now
await new Promise(resolve => setTimeout(resolve, 500));
return imageUrl; // Placeholder
};
const EditorComponent = (props) => {
const { attributes, setAttributes } = props;
const [isProcessing, setIsProcessing] = createSignal(false);
const [processedImages, setProcessedImages] = createSignal([]);
const onSelectImages = (media) => {
const newImages = media.map(item => ({
id: item.id,
url: item.url,
processedUrl: null,
}));
setAttributes({ images: [...(attributes.images || []), ...newImages.map(img => img.id)] });
setProcessedImages(prev => [...prev, ...newImages]);
};
const handleWatermark = async () => {
setIsProcessing(true);
const currentImages = processedImages().length > 0 ? processedImages() : (attributes.images || []).map(id => ({ id, url: `/wp-content/uploads/some-image-${id}.jpg` })); // Mock URLs
const results = await Promise.all(
currentImages.map(async (img) => {
const processedUrl = await applyWatermark(
img.url,
attributes.watermarkText,
attributes.watermarkPosition,
attributes.watermarkOpacity
);
return { ...img, processedUrl };
})
);
setProcessedImages(results);
setIsProcessing(false);
};
const removeImage = (idToRemove) => {
setAttributes({ images: attributes.images.filter(id => id !== idToRemove) });
setProcessedImages(prev => prev.filter(img => img.id !== idToRemove));
};
return (
<>
<InspectorControls>
<PanelBody title={__('Watermark Settings', 'gutenberg-watermarker')} initialOpen={true}>
<TextControl
label={__('Watermark Text', 'gutenberg-watermarker')}
value={attributes.watermarkText}
onChange={(value) => setAttributes({ watermarkText: value })}
/>
<RangeControl
label={__('Opacity', 'gutenberg-watermarker')}
value={attributes.watermarkOpacity}
onChange={(value) => setAttributes({ watermarkOpacity: value })}
min={0}
max={1}
step={0.1}
/>
<!-- Add controls for position, font size, etc. -->
</PanelBody>
</InspectorControls>
<div className="gutenberg-watermarker-editor">
<h3>{__('Bulk Image Watermarker', 'gutenberg-watermarker')}</h3>
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImages}
allowedTypes={['image']}
multiple
value={attributes.images}
render={({ open }) => (
<Button
variant="primary"
onClick={open}
isBusy={isProcessing()}
disabled={isProcessing()}
>
{__('Select Images', 'gutenberg-watermarker')}
</Button>
)}
/>
</MediaUploadCheck>
<Show when={attributes.images && attributes.images.length > 0}>
<div className="selected-images-list">
<h4>{__('Selected Images', 'gutenberg-watermarker')}</h4>
<ul>
<For each={processedImages() || attributes.images}>
{(imageOrId) => {
const image = typeof imageOrId === 'object' ? imageOrId : { id: imageOrId, url: `/wp-content/uploads/image-${imageOrId}.jpg` }; // Mock URL
return (
<li key={image.id}>
<img src={image.processedUrl || image.url} alt="" width="50" />
<span>Image ID: {image.id}</span>
<Button isSmall icon="trash" onClick={() => removeImage(image.id)} />
</li>
);
}}
</For>
</ul>
<Button
variant="secondary"
onClick={handleWatermark}
isBusy={isProcessing()}
disabled={isProcessing() || !attributes.images || attributes.images.length === 0}
>
{isProcessing() ? __('Watermarking...', 'gutenberg-watermarker') : __('Apply Watermark', 'gutenberg-watermarker')}
</Button>
</div>
</Show>
<!-- Add a preview area here if client-side processing is implemented -->
</div>
</>
);
};
registerBlockType('gutenberg-watermarker/block', {
title: __('Bulk Image Watermarker', 'gutenberg-watermarker'),
icon: 'format-image',
category: 'media',
attributes: {
images: {
type: 'array',
default: [],
},
watermarkText: {
type: 'string',
default: '© My Company',
},
watermarkPosition: {
type: 'string',
default: 'bottom-right',
},
watermarkOpacity: {
type: 'number',
default: 0.7,
},
},
edit: EditorComponent,
save: () => null, // Dynamic block, rendering handled by PHP
});
In this editor.jsx file:
- We import necessary components from
@wordpress/blocks,@wordpress/block-editor,@wordpress/components, and@wordpress/i18n. - SolidJS’s
createSignalis used for managing component state (isProcessing,processedImages). MediaUploadandMediaUploadCheckare WordPress components for handling media library interactions.InspectorControlsprovides a sidebar panel for settings like watermark text and opacity.- The
applyWatermarkfunction is a placeholder. In a production environment, this would contain the actual image processing logic, likely using the Canvas API for client-side processing or making an AJAX request to a server-side endpoint for more robust manipulation. - The
handleWatermarkfunction orchestrates the watermarking process for selected images. setAttributesis used to update the block’s attributes, which are then saved to the WordPress database.- The
save: () => nullindicates a dynamic block.
Styling the Block
Add some basic CSS to style your block in the editor. Create a file named src/style.scss:
.gutenberg-watermarker-editor {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
.selected-images-list {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 15px;
ul {
list-style: none;
padding: 0;
margin: 0;
li {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
img {
border: 1px solid #ccc;
}
span {
flex-grow: 1;
}
}
}
}
}
You’ll need to tell @wordpress/scripts to compile this SCSS file. Update your package.json scripts:
{
// ... other configurations
"scripts": {
"build": "wp-scripts build --experimental-jsx --jsx-import source=solid-js && wp-scripts build --style=src/style.scss",
"start": "wp-scripts start --experimental-jsx --jsx-import source=solid-js && wp-scripts start --style=src/style.scss"
},
// ... other configurations
}
Building and Activating the Block
Run the build command to compile your SolidJS components and SCSS:
npm run build
This will create a build/ directory containing index.js and style-index.css. Ensure your PHP file is correctly placed within your WordPress theme’s or plugin’s directory structure. If you placed the PHP file in the root of a plugin, the paths in the PHP file (e.g., plugins_url, plugin_dir_path) should be correct. If it’s in a theme, adjust accordingly.
Activate the plugin or ensure the theme is active. Then, navigate to the WordPress editor. You should now be able to add the “Bulk Image Watermarker” block to your posts or pages.
Advanced Considerations and Future Enhancements
Client-Side vs. Server-Side Processing: The provided applyWatermark is a placeholder. For true bulk watermarking, you’ll need a robust implementation. Client-side processing using the Canvas API can be performant for smaller images but has limitations. For larger images or more complex operations (like batch processing and saving to the media library), a server-side approach via WordPress REST API endpoints or AJAX handlers is recommended. This would involve sending image IDs and watermark settings to PHP, where libraries like GD or ImageMagick can perform the heavy lifting.
Error Handling and Feedback: Implement comprehensive error handling for image loading, processing, and saving. Provide clear feedback to the user during the watermarking process, especially for long-running operations.
Performance Optimization: For very large batches of images, consider debouncing or throttling the handleWatermark function. If using client-side processing, optimize Canvas operations and consider Web Workers to avoid blocking the main thread.
Internationalization (i18n): Ensure all user-facing strings are wrapped in __() or _x() for translation, as demonstrated in the example.
Accessibility: Pay close attention to ARIA attributes and keyboard navigation for all interactive elements within the block’s interface.
By combining SolidJS’s reactive paradigm with Gutenberg’s block architecture, you can create powerful and performant custom tools for WordPress users. This example provides a solid foundation for building sophisticated image manipulation blocks.