Step-by-Step Guide to building a custom automated asset optimization manager block for Gutenberg using Vue micro-frontends
Architectural Overview: Micro-Frontends for Gutenberg Asset Optimization
This document details the construction of a custom Gutenberg block designed to automate asset optimization within a WordPress environment. Leveraging a micro-frontend architecture with Vue.js, we aim to create a modular, scalable, and maintainable solution that integrates seamlessly into the WordPress editor. The core objective is to provide editors with an intuitive interface to manage and optimize images, scripts, and stylesheets associated with specific content blocks, thereby enhancing website performance and SEO.
The chosen approach separates concerns: the Gutenberg block acts as the UI layer within the WordPress editor, while a Vue.js application handles the complex logic of asset analysis, optimization, and integration. This separation allows for independent development, testing, and deployment of the optimization engine and the editor interface. The communication between the Gutenberg block and the Vue.js micro-frontend will be managed via custom events and potentially a shared state management solution.
Setting Up the Development Environment
A robust development environment is crucial. We’ll utilize Node.js and npm/yarn for package management, and a modern PHP development server for WordPress. For Vue.js development, we’ll employ the Vue CLI for scaffolding and managing the micro-frontend application.
WordPress Plugin Structure
Begin by creating a new WordPress plugin. This plugin will house both the Gutenberg block registration and the necessary PHP logic to enqueue our micro-frontend assets.
- Create a directory for your plugin, e.g.,
wp-content/plugins/gutenberg-asset-optimizer. - Inside this directory, create a main plugin file, e.g.,
gutenberg-asset-optimizer.php.
The main plugin file will contain the plugin header and the hooks to register our block and enqueue assets.
<?php
/**
* Plugin Name: Gutenberg Asset Optimizer
* Description: Automates asset optimization for Gutenberg blocks using Vue micro-frontends.
* Version: 1.0.0
* Author: Antigravity
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: gutenberg-asset-optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register the Gutenberg block.
*/
function gaop_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'gaop_register_block' );
/**
* Enqueue micro-frontend assets.
*/
function gaop_enqueue_microfrontend_assets() {
// Enqueue the main Vue app script and stylesheet.
// These will be generated by the Vue CLI build process.
wp_enqueue_script(
'gaop-vue-app',
plugins_url( 'vue-app/dist/app.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'vue-app/dist/app.js' )
);
wp_enqueue_style(
'gaop-vue-app-style',
plugins_url( 'vue-app/dist/app.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'vue-app/dist/app.css' )
);
// Pass data to the Vue app if needed.
wp_localize_script( 'gaop-vue-app', 'gaop_data', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
// Add any other necessary data here.
) );
}
add_action( 'enqueue_block_editor_assets', 'gaop_enqueue_microfrontend_assets' );
Vue.js Micro-Frontend Setup
We’ll create a separate Vue.js application that will be built into distributable assets (JavaScript and CSS) and then included by our WordPress plugin. This application will serve as the micro-frontend.
- Navigate to your plugin directory:
cd wp-content/plugins/gutenberg-asset-optimizer. - Create a new directory for the Vue app:
mkdir vue-app. - Initialize a new Vue project within this directory using Vue CLI:
cd vue-app && vue create . --template webpack(or usevue create .and select manual options). - Configure the Vue project for building as a library or a standalone app that can be embedded. For simplicity, we’ll initially build it as a standard app and adjust the entry point.
In your vue-app/vue.config.js, configure the build to output assets suitable for WordPress enqueueing. For a single-file application, you might configure it as a library, or more commonly, ensure the output is standard JS/CSS files.
// vue-app/vue.config.js
module.exports = {
// Configure output directory to be within the WordPress plugin
// This path is relative to the vue-app directory.
// The WordPress plugin will reference this from its root.
outputDir: '../vue-app/dist', // Output to a 'dist' folder in the plugin root
// Configure for WordPress enqueueing, typically not as a library
// but as a standard app that gets loaded.
// If you need to expose specific components, you might use library mode.
// For this example, we'll stick to standard app build.
chainWebpack: config => {
// Ensure the output is standard JS/CSS files
config.output.filename('app.js');
config.output.chunkFilename('app.js'); // For chunked output if any
},
// If you need to build as a library for more advanced integration:
// configureWebpack: {
// output: {
// library: 'GutenbergAssetOptimizerVueApp',
// libraryTarget: 'umd',
// umdNamedDefine: true
// }
// }
};
Modify your vue-app/src/main.js to mount your Vue application into a specific DOM element that will be present in the Gutenberg editor.
// vue-app/src/main.js
import { createApp } from 'vue';
import App from './App.vue';
// Import any necessary Vue plugins or components
// Ensure this ID matches the one used in your Gutenberg block's edit function
const MOUNT_POINT_ID = 'gutenberg-asset-optimizer-app';
// Function to mount the Vue app
const mountVueApp = () => {
// Check if the mount point exists
let mountPoint = document.getElementById(MOUNT_POINT_ID);
// If it doesn't exist, try to find it within the Gutenberg editor's DOM
if (!mountPoint) {
// This is a simplified approach. In a real-world scenario,
// you might need to be more specific about where to look.
// Gutenberg's editor structure can change.
const editorCanvas = document.querySelector('.edit-post-visual-editor__content-wrapper');
if (editorCanvas) {
mountPoint = document.createElement('div');
mountPoint.id = MOUNT_POINT_ID;
editorCanvas.appendChild(mountPoint);
} else {
console.error('Gutenberg editor canvas not found. Cannot mount Vue app.');
return;
}
}
// Create and mount the Vue application
const app = createApp(App);
// Register global components, plugins, etc.
// app.use(MyPlugin);
app.mount(`#${MOUNT_POINT_ID}`);
console.log('Vue app mounted to:', `#${MOUNT_POINT_ID}`);
};
// Mount the app when the DOM is ready or when Gutenberg loads the block.
// For Gutenberg, it's often better to mount when the block is rendered.
// We'll handle this within the Gutenberg block's edit function.
// Export a function to allow mounting from Gutenberg block
export { mountVueApp };
// If you want to auto-mount on page load for testing purposes:
// document.addEventListener('DOMContentLoaded', mountVueApp);
After setting up the Vue project, build the assets:
cd vue-app npm run build # or yarn build
This will generate the app.js and app.css (or similar) files in the vue-app/dist directory. These are the files that your WordPress plugin will enqueue.
Gutenberg Block Development
We’ll use the WordPress `@wordpress/scripts` package for building our Gutenberg block’s JavaScript and CSS. This package handles Babel, Webpack, and other build tools.
Block Registration and `block.json`
Create a block.json file in the root of your plugin directory (alongside gutenberg-asset-optimizer.php). This file describes your block to WordPress.
{
"apiVersion": 2,
"name": "gutenberg-asset-optimizer/asset-manager",
"version": "0.1.0",
"title": "Asset Optimizer",
"category": "widgets",
"icon": "hammer",
"description": "Manages and optimizes assets for content.",
"supports": {
"html": false
},
"textdomain": "gutenberg-asset-optimizer",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
The editorScript points to the JavaScript file that will be loaded in the editor. This is where we’ll integrate our Vue micro-frontend.
`package.json` for Block Development
In the root of your plugin directory, create a package.json file to manage your block’s development dependencies.
{
"name": "gutenberg-asset-optimizer",
"version": "1.0.0",
"description": "Gutenberg Asset Optimizer Block",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"packages-update": "wp-scripts packages-update"
},
"keywords": ["wordpress", "gutenberg", "block", "vue", "micro-frontend"],
"author": "Antigravity",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
},
"dependencies": {
"vue": "^3.0.0"
}
}
Install the dependencies:
npm install # or yarn install
Now, you can build your block assets:
npm run build # or yarn build
This will create the build/index.js and build/index.css files.
Integrating Vue Micro-Frontend into the Gutenberg Block
The core of the integration lies within the edit function of your Gutenberg block’s JavaScript file (e.g., src/index.js, which will be compiled to build/index.js).
We need to ensure our Vue app is mounted when the block is active in the editor and unmounted when it’s not. We’ll also need to handle the communication between the Gutenberg block’s state and the Vue micro-frontend.
// src/index.js (or your block's main JS file)
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
// Import the Vue app and its mounting function
// Note: The path here depends on your Vue build setup.
// If Vue is built as a library, you might import it differently.
// For a standard app build, we assume it's globally available or imported.
// For this example, we'll assume the Vue app is globally registered or imported.
// A more robust approach would be to dynamically import the Vue app.
// Let's assume our Vue app is designed to be mounted into a specific div.
// We'll need to ensure this div is rendered by our block.
// Import the mount function from your Vue app
// Ensure your Vue build process makes this export available.
// If using `vue.config.js` with `libraryTarget: 'umd'`, you might access it globally.
// For simplicity, let's assume we can import it directly if Vue CLI is configured for it.
// If not, we'll rely on a global variable or dynamic import.
// For this example, let's assume we're dynamically importing the Vue app.
// This requires Webpack configuration to handle it.
// A simpler approach for initial setup:
// Assume the Vue app is mounted into a div rendered by the block.
// Import the Vue app's mount function (adjust path as needed)
// This requires your Vue build to export this function.
// If not, you'll need to find another way to trigger the mount.
// For now, let's simulate it by directly referencing the mount function.
// In a real scenario, you'd likely have a build step that makes this available.
// Example: import { mountVueApp } from '../vue-app/src/main'; // This path is conceptual
// Let's use a placeholder for the Vue app mounting logic.
// The actual mounting will happen when the block is rendered.
const BLOCK_CLIENT_ID = 'gutenberg-asset-optimizer/asset-manager';
const VUE_APP_MOUNT_ID = 'gutenberg-asset-optimizer-app';
const Edit = ( { attributes, setAttributes, clientId } ) => {
const blockProps = useBlockProps();
const vueAppInstance = useRef( null );
const mountPointRef = useRef( null );
// Function to find or create the mount point for the Vue app
const ensureMountPoint = () => {
if (!mountPointRef.current) {
// Try to find an existing mount point associated with this block instance
mountPointRef.current = document.getElementById(`${VUE_APP_MOUNT_ID}-${clientId}`);
if (!mountPointRef.current) {
// Create a new mount point if it doesn't exist
mountPointRef.current = document.createElement('div');
mountPointRef.current.id = `${VUE_APP_MOUNT_ID}-${clientId}`;
// Append it to the block's wrapper or a specific container
// For simplicity, let's append it to the blockProps.ref element
// This might require adjustments based on how Gutenberg renders blocks.
// A more reliable way is to append it to the editor's main container.
const editorCanvas = document.querySelector('.edit-post-visual-editor__content-wrapper');
if (editorCanvas) {
editorCanvas.appendChild(mountPointRef.current);
} else {
console.error('Editor canvas not found. Cannot append Vue mount point.');
return null;
}
}
}
return mountPointRef.current;
};
// Effect to mount and unmount the Vue app
useEffect( () => {
const mountPoint = ensureMountPoint();
if (!mountPoint) {
return; // Cannot mount if mount point is not available
}
// Dynamically import the Vue app and its mount function
// This assumes your Vue build is configured to output a module
// that exports `mountVueApp`.
// You might need to adjust the import path and the Vue build config.
import( /* webpackChunkName: "vue-app" */ '../vue-app/dist/app.js' )
.then( ( VueAppModule ) => {
// Assuming VueAppModule exports a function like mountVueApp
// Adjust this based on your Vue app's export structure.
if ( VueAppModule && typeof VueAppModule.mountVueApp === 'function' ) {
// Pass necessary props or data to the Vue app
vueAppInstance.current = VueAppModule.mountVueApp( {
el: mountPoint,
props: {
// Pass block attributes or other relevant data
blockAttributes: attributes,
clientId: clientId,
// Add event handlers for communication
onUpdateAttributes: ( newAttributes ) => {
setAttributes( newAttributes );
}
}
} );
console.log( 'Vue app mounted for block:', clientId );
} else {
console.error( 'Vue app mount function not found or invalid export.' );
}
} )
.catch( ( error ) => {
console.error( 'Failed to load Vue micro-frontend:', error );
} );
// Cleanup function to unmount the Vue app
return () => {
if ( vueAppInstance.current && typeof vueAppInstance.current.unmount === 'function' ) {
vueAppInstance.current.unmount();
console.log( 'Vue app unmounted for block:', clientId );
}
// Remove the mount point from the DOM
if ( mountPoint && mountPoint.parentNode ) {
mountPoint.parentNode.removeChild( mountPoint );
console.log( 'Vue mount point removed for block:', clientId );
}
mountPointRef.current = null; // Clear the ref
};
}, [ clientId, attributes, setAttributes ] ); // Re-run effect if clientId or attributes change
// Render a placeholder or the block's UI elements
// The Vue app will be mounted into the DOM element managed by mountPointRef.
return (
<div { ...blockProps }>
{ /* The Vue app will render into the div managed by mountPointRef */ }
{ /* You can also render standard Gutenberg controls here if needed */ }
<InspectorControls>
<PanelBody title={ __( 'Asset Settings', 'gutenberg-asset-optimizer' ) }>
<TextControl
label={ __( 'Optimization Level', 'gutenberg-asset-optimizer' ) }
value={ attributes.optimizationLevel || 'medium' }
onChange={ ( newValue ) => setAttributes( { optimizationLevel: newValue } ) }
/>
</PanelBody>
</InspectorControls>
<div id={ `${VUE_APP_MOUNT_ID}-${clientId}` }></div> {/* This is where Vue mounts */}
</div>
);
};
registerBlockType( BLOCK_CLIENT_ID, {
edit: Edit,
save: () => null, // We'll handle saving via attributes or custom data.
// For complex micro-frontends, often the 'save' function returns null
// and content is managed via attributes or custom fields.
// If you need to render static HTML, implement it here.
} );
Important Considerations for the Vue App (`vue-app/src/main.js` and `vue-app/src/App.vue`):
- The Vue app needs to be designed to accept props and emit events to communicate with the Gutenberg block.
- The `mountVueApp` function in `main.js` should accept an options object, including the DOM element to mount into and any initial props.
- The Vue app should be able to receive updated attributes from Gutenberg and emit events to trigger attribute updates in Gutenberg.
- Ensure your Vue build process (Webpack config in `vue.config.js`) correctly exports the `mountVueApp` function and bundles the necessary Vue runtime. You might need to configure Webpack to treat the output as a module that can be imported.
Example of how your Vue app might be structured to accept props and emit events:
// vue-app/src/main.js (modified for prop/event handling)
import { createApp } from 'vue';
import App from './App.vue';
const MOUNT_POINT_ID_PREFIX = 'gutenberg-asset-optimizer-app';
// Function to mount the Vue app, accepting props and an update callback
const mountVueApp = ( { el, props } ) => {
const app = createApp(App, {
// Pass props down to the root App component
...props
});
// Register global components, plugins, etc.
// app.use(MyPlugin);
// Mount the app
const vm = app.mount(el);
// Return the Vue app instance for unmounting
return {
unmount: () => {
app.unmount();
},
// You can expose other methods if needed
};
};
export { mountVueApp };
// vue-app/src/App.vue (example root component)
<template>
<div class="asset-optimizer-microfrontend">
<h3>Asset Optimization Manager</h3>
<p>Block Client ID: {{ clientId }}</p>
<p>Optimization Level: {{ blockAttributes.optimizationLevel }}</p>
<!-- Your optimization controls and logic here -->
<button @click="updateOptimizationLevel('high')">Set High Optimization</button>
<button @click="updateOptimizationLevel('low')">Set Low Optimization</button>
</div>
</template>
<script>
export default {
name: 'AssetOptimizerApp',
props: {
blockAttributes: {
type: Object,
required: true
},
clientId: {
type: String,
required: true
},
onUpdateAttributes: {
type: Function,
required: true
}
},
methods: {
updateOptimizationLevel( level ) {
// Emit an event to update the Gutenberg block's attributes
this.onUpdateAttributes( {
...this.blockAttributes,
optimizationLevel: level
} );
}
},
// You might use watch to react to prop changes from Gutenberg
watch: {
blockAttributes: {
handler( newAttributes ) {
console.log( 'Received updated attributes in Vue app:', newAttributes );
// Perform actions based on attribute changes if necessary
},
deep: true
}
}
};
</script>
<style scoped>
.asset-optimizer-microfrontend {
border: 1px solid #ccc;
padding: 15px;
margin-top: 10px;
background-color: #f9f9f9;
}
</style>
Asset Optimization Logic (Backend/Vue)
The actual asset optimization logic can reside within the Vue micro-frontend, or it can be triggered via AJAX calls to a WordPress backend (using `admin-ajax.php` or custom REST API endpoints). For complex operations like image compression or code minification, a backend approach is often more performant and secure.
AJAX Communication with WordPress Backend
The Vue micro-frontend can communicate with WordPress using AJAX. The `wp_localize_script` function in the PHP file makes `gaop_data.ajax_url` available to your Vue app.
// Example AJAX call from Vue component (e.g., within a method)
async optimizeImage(imageUrl) {
try {
const response = await fetch(window.gaop_data.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'gaop_optimize_image', // Your AJAX action hook
nonce: window.gaop_data.nonce, // If you add a nonce for security
image_url: imageUrl,
// Add other parameters like optimization level
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Optimization result:', result);
// Handle success (e.g., update UI, save new URL)
} catch (error) {
console.error('Error optimizing image:', error);
// Handle error
}
}
You’ll need to register the AJAX handler in your WordPress plugin’s PHP file:
admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'gaop_ajax_nonce' ), // Create a nonce
) );
}
add_action( 'enqueue_block_editor_assets', 'gaop_add_ajax_nonce' );
// AJAX handler for image optimization
function gaop_ajax_optimize_image() {
check_ajax_referer( 'gaop_ajax_nonce', 'nonce' ); // Verify nonce
if ( ! isset( $_POST['image_url'] ) ) {
wp_send_json_error( array( 'message' => 'Image URL is missing.' ) );
}
$image_url = sanitize_url( $_POST['image_url'] );
$optimization_level = isset( $_POST['optimization_level'] ) ? sanitize_text_field( $_POST['optimization_level'] ) : 'medium';
// --- Actual Optimization Logic ---
// This is where you'd integrate with an image optimization library
// or API (e.g., Imagick, GD, TinyPNG API, ShortPixel API).
// For demonstration, we'll just return a placeholder.
$optimized_data = array(
'original_url' => $image_url,
'optimized_url' => $image_url . '?optimized=' . $optimization_level, // Placeholder
'message' => 'Image optimized successfully (placeholder).',
'optimization_level_used' => $optimization_level,
);
wp_send_json_success( $optimized_data );
}
add_action( 'wp_ajax_gaop_optimize_image', 'gaop_optimize_image' ); // For logged-in users
add_action( 'wp_ajax_nopriv_gaop_optimize_image', 'gaop_optimize_image' ); // For non-logged-in users (if applicable)
// You would also need handlers for script/style optimization,
// potentially involving file manipulation or external services.
?>
Deployment and Workflow
The deployment process involves building both the Gutenberg block assets and the Vue micro-frontend assets, then placing them within the WordPress plugin directory.
- Development: Run
npm run startin the plugin root for the Gutenberg block andnpm run serve(or similar) in thevue-appdirectory for the Vue micro-frontend. This allows for hot-reloading and faster iteration. - Staging/Production Build: Execute
npm run buildin the plugin root andnpm run buildin thevue-appdirectory. - Deployment: Copy the entire plugin directory (including the
buildfolder for the block and thevue-app/distfolder for the micro-frontend) to your WordPress installation’swp-content/plugins/directory.
Advanced Considerations
State Management
For more complex interactions and shared state between the Gutenberg block and the Vue micro-frontend, consider using a state management library like Pinia (for Vue 3) or Vuex. This state can be synchronized with Gutenberg’s block attributes.
Asset Optimization Libraries
Integrate robust libraries for actual optimization:
- Images: PHP’s Imagick or GD extensions, or cloud-based APIs (e.g., Cloudinary, ShortPixel, TinyPNG).
- JavaScript/CSS: Minification libraries like UglifyJS, Terser (for JS), and CSSNano (for CSS). These can be run server-side or via build tools.
Performance Monitoring and Feedback
Provide visual feedback within the Gutenberg editor about the optimization status and potential performance gains. This could involve displaying metrics or progress indicators within the Vue micro-frontend.
Error Handling and Resilience
Implement comprehensive error handling for AJAX requests and optimization processes. Ensure the Gutenberg editor remains functional even if the micro-frontend encounters issues. Graceful degradation is key.
Security
Always sanitize inputs and validate data, especially when dealing with AJAX requests and file operations. Use WordPress nonces to protect against CSRF attacks.
Future Enhancements
Consider extending this architecture to handle other asset types, integrate with CDNs, or provide bulk optimization features. The micro-frontend approach makes it easier to add new modules and functionalities without impacting the core Gutenberg block logic.