Step-by-Step Guide to building a custom automated asset optimization manager block for Gutenberg using React components
Project Setup and Environment Configuration
Before diving into component development, establish a robust development environment. This involves setting up a local WordPress instance and configuring the necessary build tools for React and Gutenberg development. We’ll leverage the official WordPress `@wordpress/scripts` package, which provides a pre-configured Webpack setup for blocks.
First, ensure you have Node.js and npm (or yarn) installed. Navigate to your WordPress theme or plugin directory where you intend to build the block. Initialize a new project using npm:
npm init -y
Next, install the essential WordPress development scripts and React dependencies:
npm install --save-dev @wordpress/scripts react react-dom
In your `package.json` file, add the following scripts to manage the build process:
{
"name": "asset-optimizer-block",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^26.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
The `build` script compiles your React components into production-ready JavaScript and CSS. The `start` script watches for changes and recompiles automatically, ideal for development.
Create a `src` directory in your project root. Inside `src`, create an `index.js` file which will be the entry point for your block’s JavaScript. Also, create a `block.json` file to define your block’s metadata.
Defining the Block Metadata (`block.json`)
The `block.json` file is crucial for registering your Gutenberg block with WordPress. It defines the block’s name, title, icon, categories, attributes, and script/style handles.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/asset-optimizer-block",
"version": "0.1.0",
"title": "Automated Asset Optimizer",
"category": "optimization",
"icon": "performance",
"description": "Manages and optimizes assets for better performance.",
"keywords": [ "asset", "optimization", "performance", "images", "scripts" ],
"attributes": {
"optimizeImages": {
"type": "boolean",
"default": true
},
"minifyScripts": {
"type": "boolean",
"default": false
},
"lazyLoadImages": {
"type": "boolean",
"default": true
}
},
"textdomain": "asset-optimizer-block",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
Key fields:
name: A unique identifier for your block (namespace/block-name).title: The human-readable name displayed in the block inserter.category: The category the block belongs to.icon: An icon representing the block.attributes: Defines the data that your block will store. Here, we define boolean flags for image optimization, script minification, and lazy loading.editorScript: Points to the compiled JavaScript file for the block editor.editorStyle: Points to the compiled CSS file for the block editor.style: Points to the compiled CSS file for the frontend.
Registering the Block and Entry Point (`src/index.js`)
The `src/index.js` file serves as the main entry point for your block’s JavaScript. It imports necessary Gutenberg components and registers the block using `registerBlockType`.
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './style.scss'; // Frontend styles
import './editor.scss'; // Editor-only styles
import Edit from './Edit';
import save from './save';
registerBlockType( 'my-plugin/asset-optimizer-block', {
edit: Edit,
save,
} );
This file imports the `Edit` and `save` components (which we’ll define next), along with frontend and editor-specific stylesheets. `registerBlockType` takes the block name from `block.json` and maps the `edit` and `save` functions to their respective components.
Developing the Editor Component (`src/Edit.js`)
The `Edit` component is responsible for rendering the block’s interface within the Gutenberg editor. It uses React hooks and WordPress components to manage state and provide controls.
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const { optimizeImages, minifyScripts, lazyLoadImages } = attributes;
const updateAttribute = ( key, value ) => {
setAttributes( { [ key ]: value } );
};
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Asset Optimization Settings', 'asset-optimizer-block' ) }>
<ToggleControl
label={ __( 'Optimize Images', 'asset-optimizer-block' ) }
checked={ optimizeImages }
onChange={ ( value ) => updateAttribute( 'optimizeImages', value ) }
/>
<ToggleControl
label={ __( 'Minify Scripts', 'asset-optimizer-block' ) }
checked={ minifyScripts }
onChange={ ( value ) => updateAttribute( 'minifyScripts', value ) }
/>
<ToggleControl
label={ __( 'Lazy Load Images', 'asset-optimizer-block' ) }
checked={ lazyLoadImages }
onChange={ ( value ) => updateAttribute( 'lazyLoadImages', value ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<p>{ __( 'Asset Optimizer Settings', 'asset-optimizer-block' ) }</p>
<p>{ __( 'Configure optimization settings in the sidebar.', 'asset-optimizer-block' ) }</p>
<ul>
<li>{ __( 'Optimize Images:', 'asset-optimizer-block' ) } { optimizeImages ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
<li>{ __( 'Minify Scripts:', 'asset-optimizer-block' ) } { minifyScripts ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
<li>{ __( 'Lazy Load Images:', 'asset-optimizer-block' ) } { lazyLoadImages ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
</ul>
</div>
</>
);
}
In this component:
useBlockProps: A hook that provides necessary props for the block’s root element, including classes and attributes.InspectorControls: A component that renders controls in the block’s sidebar (Inspector).PanelBody: A container for controls within the Inspector.ToggleControl: A standard WordPress component for boolean toggles.attributes: An object containing the current values of the block’s attributes.setAttributes: A function to update the block’s attributes.
The `updateAttribute` helper function simplifies updating specific attributes. The `Edit` component renders the toggle controls in the sidebar and displays the current status of the settings within the block’s editable area.
Developing the Save Component (`src/save.js`)
The `save` component defines how the block’s content is rendered on the frontend. It receives the block’s attributes and should return a static representation of the block’s output.
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
const { optimizeImages, minifyScripts, lazyLoadImages } = attributes;
// In a real-world scenario, this component would not directly control
// the optimization process. It would likely output data attributes or
// classes that a separate PHP or JS process would hook into.
// For demonstration, we'll just display the settings.
return (
<div { ...blockProps } data-optimize-images={ optimizeImages } data-minify-scripts={ minifyScripts } data-lazy-load-images={ lazyLoadImages }>
<p>{ __( 'Asset Optimizer Status:', 'asset-optimizer-block' ) }</p>
<ul>
<li>{ __( 'Optimize Images:', 'asset-optimizer-block' ) } { optimizeImages ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
<li>{ __( 'Minify Scripts:', 'asset-optimizer-block' ) } { minifyScripts ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
<li>{ __( 'Lazy Load Images:', 'asset-optimizer-block' ) } { lazyLoadImages ? __( 'Enabled', 'asset-optimizer-block' ) : __( 'Disabled', 'asset-optimizer-block' ) }</li>
</ul>
</div>
);
}
Crucially, the `save` component should return a static HTML structure. It should not contain interactive elements that rely on JavaScript to function on the frontend, unless that JavaScript is enqueued separately and designed to enhance the static output. Here, we’re adding data attributes to the block’s wrapper element. These attributes can be read by a PHP function hooked into `the_content` or by a frontend JavaScript file to trigger actual optimization processes.
Styling the Block (`src/style.scss` and `src/editor.scss`)
Create two SCSS files for styling:
src/style.scss (for frontend styles):
.wp-block-my-plugin-asset-optimizer-block {
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 15px;
background-color: #f9f9f9;
ul {
list-style: disc inside;
margin-top: 10px;
}
}
src/editor.scss (for editor-only styles):
.wp-block-my-plugin-asset-optimizer-block {
border-style: dashed;
background-color: #eef7ff;
}
The `@wordpress/scripts` package automatically compiles these SCSS files into CSS and links them according to the `editorStyle` and `style` properties in `block.json`.
Building the Block Assets
With the components and metadata defined, run the build script:
npm run build
This command will generate the `build` directory containing `index.js` (compiled editor script) and `index.css` (editor styles), as well as `style-index.css` (frontend styles). These files will be automatically enqueued by WordPress when the block is used, thanks to the `editorScript` and `style` paths in `block.json`.
Implementing Backend Optimization Logic (PHP)
The frontend rendering of the block is just the UI. The actual asset optimization needs to happen server-side or via a background process. This block acts as a control panel. A common approach is to hook into WordPress actions and filters.
For instance, to process images when content is saved or displayed, you might use:
post_content;
// Regex to find our block. This is a simplified example.
// A more robust solution would use parse_blocks().
$pattern = '//i';
if ( preg_match( $pattern, $content, $matches ) ) {
$attributes_json = $matches[1];
$attributes = json_decode( $attributes_json, true );
if ( $attributes && isset( $attributes['optimizeImages'] ) && $attributes['optimizeImages'] ) {
// Trigger image optimization for images within this post's content.
// This would involve finding image URLs and processing them.
error_log( "Asset Optimizer: Image optimization requested for post ID {$post_id}." );
// Example: Call a function to optimize images found in $content.
// my_optimize_images_in_content( $content );
}
if ( $attributes && isset( $attributes['minifyScripts'] ) && $attributes['minifyScripts'] ) {
error_log( "Asset Optimizer: Script minification requested for post ID {$post_id}." );
// Example: Call a function to minify scripts.
// my_minify_scripts_in_content( $content );
}
if ( $attributes && isset( $attributes['lazyLoadImages'] ) && $attributes['lazyLoadImages'] ) {
error_log( "Asset Optimizer: Lazy loading requested for post ID {$post_id}." );
// Example: Add lazy loading attributes via a filter.
}
}
}
add_action( 'save_post', 'my_asset_optimizer_process_content', 10, 3 );
/**
* Filter content to add lazy loading attributes if enabled via the block.
*/
function my_asset_optimizer_filter_content( $content ) {
// Similar block parsing logic as above.
$pattern = '//i';
if ( preg_match( $pattern, $content, $matches ) ) {
$attributes_json = $matches[1];
$attributes = json_decode( $attributes_json, true );
if ( $attributes && isset( $attributes['lazyLoadImages'] ) && $attributes['lazyLoadImages'] ) {
// Use DOMDocument for safer HTML manipulation to add 'loading="lazy"'
// to img tags that don't already have it.
$dom = new DOMDocument();
// Suppress warnings for malformed HTML, as we'll clean it up.
@$dom->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
$images = $dom->getElementsByTagName('img');
foreach ( $images as $img ) {
if ( ! $img->hasAttribute('loading') ) {
$img->setAttribute('loading', 'lazy');
}
}
$content = $dom->saveHTML();
}
}
return $content;
}
add_filter( 'the_content', 'my_asset_optimizer_filter_content', 9 ); // Run before most filters.
/**
* Helper function to parse blocks and extract attributes for a specific block type.
*
* @param string $block_name The name of the block to find.
* @param string $content The content to parse.
* @return array|null An array of attributes if found, null otherwise.
*/
function my_asset_optimizer_get_block_attributes( $block_name, $content ) {
$blocks = parse_blocks( $content );
foreach ( $blocks as $block ) {
if ( $block['blockName'] === $block_name ) {
return $block['attrs'];
}
}
return null;
}
// Example usage within the filter:
function my_asset_optimizer_filter_content_robust( $content ) {
$optimizer_attrs = my_asset_optimizer_get_block_attributes( 'my-plugin/asset-optimizer-block', $content );
if ( $optimizer_attrs && isset( $optimizer_attrs['lazyLoadImages'] ) && $optimizer_attrs['lazyLoadImages'] ) {
$dom = new DOMDocument();
@$dom->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
$images = $dom->getElementsByTagName('img');
foreach ( $images as $img ) {
if ( ! $img->hasAttribute('loading') ) {
$img->setAttribute('loading', 'lazy');
}
}
$content = $dom->saveHTML();
}
return $content;
}
add_filter( 'the_content', 'my_asset_optimizer_filter_content_robust', 9 );
?>
This PHP code demonstrates:
- Hooking into
save_postto potentially trigger backend processes when content is saved. - Using
parse_blocks()for a more reliable way to extract block attributes from content. - Hooking into
the_contentfilter to modify output, such as addingloading="lazy"attributes to images if the corresponding block setting is enabled. - Using
DOMDocumentfor safe HTML manipulation.
Actual image optimization (resizing, compression) and script minification would require integrating with external libraries (like Imagick or GD for images, and potentially JS minifiers) or using WordPress’s built-in media handling capabilities. The block’s role is to provide the user interface to control these backend operations.
Further Enhancements and Considerations
This guide provides a foundational structure. For a production-ready asset optimizer, consider:
- Error Handling and Logging: Implement robust error handling for optimization processes and log outcomes.
- Asynchronous Processing: For intensive tasks like image optimization, consider using WP-Cron or a dedicated job queue system to avoid blocking the user request.
- User Feedback: Provide visual feedback in the editor or admin area about the status of optimization tasks.
- Configuration Options: Allow users to specify optimization levels, formats (e.g., WebP conversion), and exclusion rules.
- Security: Sanitize all inputs and ensure that any file operations are secure.
- Performance Monitoring: Integrate with performance testing tools or provide basic metrics.
- Asset Enqueuing Management: For script minification, you might need to dynamically enqueue or modify script handles based on block settings.
By combining a well-structured Gutenberg block with appropriate backend logic, you can create powerful tools for managing and optimizing website assets directly within the WordPress editor.