Step-by-Step Guide to building a custom bulk image watermarker block for Gutenberg using Svelte standalone templates
Setting Up the WordPress Plugin Environment
Before diving into the Gutenberg block development, we need a foundational WordPress plugin structure. This involves creating a main plugin file and a directory for our block assets. We’ll use a standard WordPress plugin header to define our plugin’s metadata.
Create a new directory in your WordPress installation’s wp-content/plugins/ folder. Let’s name it custom-bulk-watermarker. Inside this directory, create the main plugin file, custom-bulk-watermarker.php.
Plugin Main File: custom-bulk-watermarker.php
This file will contain the plugin header and the necessary hooks to register our Gutenberg block.
<?php
/**
* Plugin Name: Custom Bulk Watermarker
* Plugin URI: https://example.com/plugins/custom-bulk-watermarker/
* Description: Adds a Gutenberg block to bulk watermark uploaded images.
* Version: 1.0.0
* Author: Your Name
* Author URI: https://yourwebsite.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: custom-bulk-watermarker
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register the Gutenberg block.
*/
function custom_bulk_watermarker_register_block() {
// Automatically load dependencies and version
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
wp_register_script(
'custom-bulk-watermarker-block-editor-script',
plugins_url( 'build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_register_style(
'custom-bulk-watermarker-block-editor-style',
plugins_url( 'build/index.css', __FILE__ ),
array( 'wp-edit-blocks' ),
$asset_file['version']
);
register_block_type( 'custom-bulk-watermarker/block', array(
'editor_script' => 'custom-bulk-watermarker-block-editor-script',
'editor_style' => 'custom-bulk-watermarker-block-editor-style',
'render_callback' => 'custom_bulk_watermarker_render_block',
) );
}
add_action( 'init', 'custom_bulk_watermarker_register_block' );
/**
* Server-side rendering callback for the block.
*
* @param array $attributes Block attributes.
* @return string HTML output.
*/
function custom_bulk_watermarker_render_block( $attributes ) {
// This function will handle the frontend rendering if needed.
// For a block that primarily operates in the editor or via AJAX,
// this might be minimal or empty.
return '<div class="wp-block-custom-bulk-watermarker">Custom Bulk Watermarker Block</div>';
}
/**
* Enqueue frontend styles if needed.
*/
function custom_bulk_watermarker_enqueue_frontend_styles() {
// If your block has specific frontend styles, enqueue them here.
// wp_enqueue_style( 'custom-bulk-watermarker-frontend-style', plugins_url( 'build/style-index.css', __FILE__ ) );
}
add_action( 'wp_enqueue_scripts', 'custom_bulk_watermarker_enqueue_frontend_styles' );
In this PHP file:
- We define the standard WordPress plugin header.
- The
custom_bulk_watermarker_register_blockfunction is hooked into theinitaction. - It uses
wp_register_scriptandwp_register_styleto enqueue our compiled JavaScript and CSS for the block editor. plugin_dir_path( __FILE__ ) . 'build/index.asset.php'is crucial. This file is automatically generated by the build process and contains the script’s dependencies and version, ensuring correct loading.register_block_typeregisters our block with a unique namespace (custom-bulk-watermarker/block) and associates it with the editor scripts and styles.- A basic
custom_bulk_watermarker_render_blockis included for server-side rendering, though the primary logic will likely be handled client-side or via AJAX. custom_bulk_watermarker_enqueue_frontend_stylesis a placeholder for any styles needed on the frontend.
Project Structure and Build Tools
For modern WordPress block development, we’ll leverage Node.js and npm (or yarn) for managing dependencies and a build process. The official WordPress `@wordpress/scripts` package simplifies this significantly. It provides pre-configured scripts for compiling JavaScript (using ESBuild), CSS (using PostCSS), and managing asset files.
First, ensure you have Node.js and npm installed. Then, navigate to your plugin directory in the terminal and initialize a Node.js project:
cd wp-content/plugins/custom-bulk-watermarker npm init -y
Next, install the necessary development dependencies:
npm install @wordpress/scripts --save-dev
Now, we need to configure our build scripts in the package.json file. Open package.json and add the following to the scripts section:
{
"name": "custom-bulk-watermarker",
"version": "1.0.0",
"description": "",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"packages-update": "wp-scripts packages-update"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
The key scripts are:
build: Compiles your block’s JavaScript and CSS for production. This will create thebuild/directory containingindex.js,index.css, and importantly,index.asset.php.start: Watches for changes in your source files and automatically recompiles them. This is invaluable during development.packages-update: Updates the `@wordpress/scripts` package and its dependencies.
Create a src/ directory within your plugin folder. This is where your block’s source code (JavaScript and Svelte files) will reside.
Developing the Gutenberg Block with Svelte
We’ll use Svelte for building our block’s user interface. Svelte compiles your components into efficient, imperative code that directly manipulates the DOM. For Gutenberg blocks, we’ll use the @wordpress/element and @wordpress/blocks packages, which provide React-like APIs that Svelte can integrate with.
The core of our block will be in src/index.js. This file will import our Svelte component and register the block using registerBlockType from @wordpress/blocks.
src/index.js: Block Registration
This file acts as the entry point for our block’s JavaScript. It imports the necessary WordPress packages and our Svelte component, then registers the block.
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import Edit from './edit'; // Our Svelte component for the editor
import save from './save'; // The save function for the frontend
import './style.scss'; // Styles for both editor and frontend
import './editor.scss'; // Editor-specific styles
registerBlockType( 'custom-bulk-watermarker/block', {
title: __( 'Bulk Watermarker', 'custom-bulk-watermarker' ),
icon: 'format-image', // Choose an appropriate icon
category: 'media', // Or 'common', 'layout', etc.
attributes: {
// Define attributes here. For example, watermark text, image, opacity, etc.
watermarkText: {
type: 'string',
default: '© Your Brand',
},
watermarkImageId: {
type: 'number',
default: 0,
},
watermarkImageSrc: {
type: 'string',
default: '',
},
opacity: {
type: 'number',
default: 0.5,
},
position: {
type: 'string',
default: 'bottom-right',
},
},
edit: Edit,
save: save,
} );
Key points:
registerBlockTypeis the core function.- We provide a unique name (
custom-bulk-watermarker/block), title, icon, and category. - The
attributesobject defines the data our block will store. These attributes will be accessible in both theeditandsavefunctions, and persisted in the post content. edit: Editpoints to our Svelte component that will render in the Gutenberg editor.save: savepoints to a function that defines how the block’s content is saved to the database. For blocks with dynamic rendering or complex client-side logic, this might returnnull, indicating that the block should be rendered server-side.- We import Svelte components (
./edit) and styles (./style.scss,./editor.scss).
src/edit.js: The Svelte Editor Component
This file will contain the Svelte component that renders in the Gutenberg editor. It will use WordPress components from @wordpress/components and @wordpress/block-editor to provide a familiar UI.
First, we need to create a Svelte file, let’s call it src/WatermarkerEditor.svelte. The src/edit.js will then act as a bridge, rendering this Svelte component within the WordPress block editor context.
src/WatermarkerEditor.svelte
This is our main Svelte component for the block editor. It will handle user input for watermark settings and display a preview.
<script>
import { InspectorControls, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { PanelBody, Button, TextControl, RangeControl, SelectControl, Placeholder } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from 'svelte';
export let attributes;
export let setAttributes;
// Local state for Svelte component
let watermarkText = attributes.watermarkText;
let watermarkImageId = attributes.watermarkImageId;
let watermarkImageSrc = attributes.watermarkImageSrc;
let opacity = attributes.opacity;
let position = attributes.position;
// Watch for attribute changes from WordPress
$: {
watermarkText = attributes.watermarkText;
watermarkImageId = attributes.watermarkImageId;
watermarkImageSrc = attributes.watermarkImageSrc;
opacity = attributes.opacity;
position = attributes.position;
}
const onSelectImage = ( media ) => {
if ( ! media || ! media.url ) {
setAttributes( { watermarkImageId: 0, watermarkImageSrc: '' } );
return;
}
setAttributes( { watermarkImageId: media.id, watermarkImageSrc: media.url } );
};
const onRemoveImage = () => {
setAttributes( { watermarkImageId: 0, watermarkImageSrc: '' } );
};
const onUpdateText = ( value ) => {
setAttributes( { watermarkText: value } );
};
const onUpdateOpacity = ( value ) => {
setAttributes( { opacity: value } );
};
const onUpdatePosition = ( value ) => {
setAttributes( { position: value } );
};
const positions = [
{ label: __('Top Left', 'custom-bulk-watermarker'), value: 'top-left' },
{ label: __('Top Center', 'custom-bulk-watermarker'), value: 'top-center' },
{ label: __('Top Right', 'custom-bulk-watermarker'), value: 'top-right' },
{ label: __('Middle Left', 'custom-bulk-watermarker'), value: 'middle-left' },
{ label: __('Center', 'custom-bulk-watermarker'), value: 'center' },
{ label: __('Middle Right', 'custom-bulk-watermarker'), value: 'middle-right' },
{ label: __('Bottom Left', 'custom-bulk-watermarker'), value: 'bottom-left' },
{ label: __('Bottom Center', 'custom-bulk-watermarker'), value: 'bottom-center' },
{ label: __('Bottom Right', 'custom-bulk-watermarker'), value: 'bottom-right' },
];
// Placeholder for when no image is selected
const PlaceholderContent = () => (
<Placeholder
icon="format-image"
label={__('Watermark Image', 'custom-bulk-watermarker')}
instructions={__('Upload or select an image to use as a watermark.', 'custom-bulk-watermarker')}
>
<MediaUploadCheck>
<MediaUpload
onSelect={ onSelectImage }
allowedTypes={ ['image'] }
value={ watermarkImageId }
render={ ( { open } ) => (
<Button
variant="primary"
isLarge
onClick={ open }
>
{__('Upload Image', 'custom-bulk-watermarker')}
</Button>
) }
/>
</MediaUploadCheck>
</Placeholder>
);
</script>
<div>
<InspectorControls>
<PanelBody title={__('Watermark Settings', 'custom-bulk-watermarker')} initialOpen={true}>
<TextControl
label={__('Watermark Text', 'custom-bulk-watermarker')}
value={ watermarkText }
onChange={ onUpdateText }
/>
<MediaUploadCheck>
<MediaUpload
onSelect={ onSelectImage }
allowedTypes={ ['image'] }
value={ watermarkImageId }
render={ ( { open } ) => (
<Button
variant="secondary"
onClick={ open }
icon="upload"
label={__('Change Watermark Image', 'custom-bulk-watermarker')}
>
{__('Select Image', 'custom-bulk-watermarker')}
</Button>
) }
/>
</MediaUploadCheck>
{#if watermarkImageSrc}
<div style="margin-top: 10px;">
<img src={ watermarkImageSrc } alt="Selected Watermark" style="max-width: 100px; height: auto;" />
<Button
variant="link"
isDestructive
onClick={ onRemoveImage }
icon="trash"
>
{__('Remove Image', 'custom-bulk-watermarker')}
</Button>
</div>
{/if}
<RangeControl
label={__('Opacity', 'custom-bulk-watermarker')}
value={ opacity }
onChange={ onUpdateOpacity }
min={ 0 }
max={ 1 }
step={ 0.1 }
/>
<SelectControl
label={__('Position', 'custom-bulk-watermarker')}
value={ position }
options={ positions }
onChange={ onUpdatePosition }
/>
</PanelBody>
</InspectorControls>
{#if watermarkImageSrc || watermarkText}
<div class="watermark-preview">
<h4>{__('Preview', 'custom-bulk-watermarker')}</h4>
<div class="preview-area">
{#if watermarkImageSrc}
<img src={ watermarkImageSrc } alt="Watermark Preview" class="watermark-image" style="opacity: {opacity}; position: {position};" />
{/if}
{#if watermarkText}
<span class="watermark-text" style="opacity: {opacity}; position: {position};">{watermarkText}</span>
{/if}
</div>
</div>
{:else}
<PlaceholderContent />
{/if}
</div>
src/edit.js: Svelte Renderer
This JavaScript file acts as the bridge between WordPress and our Svelte component. It uses wp.element.createElement (which is compatible with Svelte’s virtual DOM) to render the Svelte component.
import { createElement } from '@wordpress/element';
import WatermarkerEditor from './WatermarkerEditor.svelte'; // Import the Svelte component
/**
* The edit function for the block.
*
* @param {Object} props - Block props.
* @param {Object} props.attributes - Block attributes.
* @param {Function} props.setAttributes - Function to update block attributes.
* @returns {Object} React element.
*/
const Edit = ( { attributes, setAttributes } ) => {
// Render the Svelte component, passing props
return createElement( WatermarkerEditor, {
attributes: attributes,
setAttributes: setAttributes,
} );
};
export default Edit;
src/save.js: Frontend Rendering
The save function determines what gets saved to the database and rendered on the frontend. For a block that primarily manipulates media or performs actions via AJAX, you might not need to save any complex HTML. However, if you want to display a placeholder or some basic info on the frontend, you can define it here.
For this block, we’ll likely rely on server-side processing or JavaScript on the frontend to apply the watermark. Thus, the save function can be minimal.
/**
* The save function for the block.
*
* @returns {null} This block does not render anything on the frontend directly.
*/
const save = () => {
// We will handle the actual watermarking via AJAX or a separate process.
// Returning null means the block will not render any HTML on the frontend
// from this save function. The PHP render_callback can provide a fallback.
return null;
};
export default save;
Returning null here tells Gutenberg not to render any static HTML for this block on the frontend. The actual watermarking logic will be triggered by a separate mechanism, such as a button click in the editor that sends an AJAX request, or a post-save hook.
Styling the Block
We’ve imported style.scss and editor.scss in src/index.js. These files will be compiled by `@wordpress/scripts` into build/index.css and build/style-index.css respectively.
src/style.scss: Global Styles
Styles that apply to both the editor and the frontend.
.wp-block-custom-bulk-watermarker {
border: 1px dashed #ccc;
padding: 10px;
text-align: center;
}
src/editor.scss: Editor-Only Styles
Styles specific to the block editor interface. This is where we’ll style our preview area.
.watermark-preview {
margin-top: 20px;
border: 1px solid #e0e0e0;
padding: 15px;
background-color: #f9f9f9;
border-radius: 4px;
h4 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.preview-area {
position: relative;
width: 100%;
height: 150px; // Fixed height for preview area
background-color: #eee;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 2px;
.watermark-image,
.watermark-text {
position: absolute;
max-width: 90%;
max-height: 90%;
object-fit: contain; // Ensure image scales correctly
pointer-events: none; // Prevent interaction with preview elements
z-index: 10;
}
.watermark-text {
font-size: 1.2em;
font-weight: bold;
color: rgba(0, 0, 0, 0.5); // Default text color, opacity handled by inline style
background-color: rgba(255, 255, 255, 0.3); // Slight background for readability
padding: 5px 10px;
border-radius: 3px;
}
// Position classes for preview
&.top-left, .watermark-image[style*="top-left"], .watermark-text[style*="top-left"] { position: absolute; top: 10px; left: 10px; transform: translate(0, 0); }
&.top-center, .watermark-image[style*="top-center"], .watermark-text[style*="top-center"] { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); }
&.top-right, .watermark-image[style*="top-right"], .watermark-text[style*="top-right"] { position: absolute; top: 10px; right: 10px; transform: translate(0, 0); }
&.middle-left, .watermark-image[style*="middle-left"], .watermark-text[style*="middle-left"] { position: absolute; top: 50%; left: 10px; transform: translateY(-50%); }
&.center, .watermark-image[style*="center"], .watermark-text[style*="center"] { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
&.middle-right, .watermark-image[style*="middle-right"], .watermark-text[style*="middle-right"] { position: absolute; top: 50%; right: 10px; transform: translateY(-50%); }
&.bottom-left, .watermark-image[style*="bottom-left"], .watermark-text[style*="bottom-left"] { position: absolute; bottom: 10px; left: 10px; transform: translate(0, 0); }
&.bottom-center, .watermark-image[style*="bottom-center"], .watermark-text[style*="bottom-center"] { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); }
&.bottom-right, .watermark-image[style*="bottom-right"], .watermark-text[style*="bottom-right"] { position: absolute; bottom: 10px; right: 10px; transform: translate(0, 0); }
}
}
Note the use of inline styles in the Svelte component for dynamic properties like opacity and position. The CSS classes for positioning are added to the .preview-area and then specific styles are applied to the watermark elements based on the selected position. The transform property is key for centering and aligning elements correctly.
Building the Block Assets
With the source files in place, it’s time to build the block assets. Navigate to your plugin’s root directory in the terminal and run the build script:
cd wp-content/plugins/custom-bulk-watermarker npm run build
This command will execute the @wordpress/scripts build process. It will:
- Compile
src/index.js(and any imported Svelte files) intobuild/index.js. - Compile
src/style.scssintobuild/style-index.css. - Compile
src/editor.scssintobuild/index.css. - Generate
build/index.asset.php, which contains the dependencies and version information required by WordPress to load the script correctly.
During development, you’ll use npm run start. This command watches your src/ directory for changes and automatically recompiles the assets, making the development cycle much faster.
Implementing the Bulk Watermarking Logic
The current setup allows users to configure watermark settings in the editor. However, the actual watermarking of uploaded images needs a mechanism. This typically involves:
- A button in the block editor (or a dedicated admin page) to trigger the watermarking process.
- An AJAX request to a WordPress REST API endpoint or a custom AJAX handler.
- Server-side PHP code to process the images (using GD or Imagick libraries) and apply the watermark.
- Updating the media library with the watermarked images.
This part is complex and depends on your specific requirements (e.g., watermarking existing images vs. new uploads, image formats, quality settings). Here’s a conceptual outline for an AJAX approach:
AJAX Handler (Conceptual PHP)
In your custom-bulk-watermarker.php, you would add an AJAX handler:
// Add this to custom-bulk-watermarker.php
add_action( 'wp_ajax_custom_watermark_images', 'custom_watermark_images_callback' );
function custom_watermark_images_callback() {
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to watermark images.', 'custom-bulk-watermarker' ) ) );
}
// Check nonce for security
check_ajax_referer( 'custom_watermark_nonce' );
$image_ids = isset( $_POST['image_ids'] ) ? array_map( 'intval', $_POST['image_ids'] ) : array();
$watermark_settings = isset( $_POST['watermark_settings'] ) ? $_POST['watermark_settings'] : array();
if ( empty( $image_ids ) ) {
wp_send_json_error( array( 'message' => __( 'No images selected.', 'custom-bulk-watermarker' ) ) );
}
$processed_images = array();
foreach ( $image_ids as $image_id ) {
$attachment = get_post( $image_id );
if ( $attachment && $attachment->post_type === 'attachment' ) {
$file_path = get_attached_file( $image_id );
if ( $file_path && file_exists( $file_path ) ) {
// Call your watermarking function here
$result = apply_watermark_to_image( $file_path, $watermark_settings );
if ( is_wp_error( $result ) ) {
$processed_images[] = array( 'id' => $image_id, 'status' => 'error', 'message' => $result->get_error_message() );
} else {
// Update attachment metadata if needed, or create a new attachment
// For simplicity, let's assume apply_watermark_to_image overwrites or returns new path
$processed_images[] = array( 'id' => $image_id, 'status' => 'success' );
}
}
}
}
wp_send_json_success( array( 'message' => __( 'Watermarking complete.', 'custom-bulk-watermarker' ), 'processed' => $processed_images ) );
}
// Placeholder for the actual watermarking function
function apply_watermark_to_image( $file_path, $settings ) {
// Implement image manipulation using GD or Imagick here.
// This is a complex function involving image loading, watermarking, and saving.
// Example:
// $image = wp_get_image_editor( $file_path );
// if ( is_wp_error( $image ) ) { return $image; }
// $watermark_path = $settings['watermarkImageSrc']; // Need to get actual file path from URL
// $image->watermark( $watermark_path, ... );
// $image->save( $file_path ); // Or save to a new file
return new WP_Error( 'not_implemented', __( 'Watermarking logic not yet implemented.', 'custom-bulk-watermarker' ) );
}
// Enqueue script