Step-by-Step Guide to building a custom automated coupon generator block for Gutenberg using Vue micro-frontends
Project Setup: WordPress Plugin and Vue Micro-Frontend
We’ll begin by establishing the foundational structure for our custom Gutenberg block. This involves creating a standard WordPress plugin that will house our block registration and asset enqueuing. Concurrently, we’ll set up a separate Vue.js project to serve as our micro-frontend for the block’s editor interface.
First, create a new WordPress plugin directory. For this example, we’ll name it gutenberg-coupon-generator.
- Navigate to your WordPress installation’s
wp-content/plugins/directory. - Create a new folder:
mkdir gutenberg-coupon-generator - Change into the new directory:
cd gutenberg-coupon-generator - Create the main plugin file:
touch gutenberg-coupon-generator.php
Populate gutenberg-coupon-generator.php with the essential plugin header and registration logic.
'gutenberg-coupon-generator-block-editor',
'editor_style' => 'gutenberg-coupon-generator-block-editor',
'render_callback' => 'gutenberg_coupon_generator_render_block',
) );
}
add_action( 'init', 'gutenberg_coupon_generator_register_block' );
/**
* Server-side rendering for the coupon block.
*
* @param array $attributes Block attributes.
* @return string HTML output.
*/
function gutenberg_coupon_generator_render_block( $attributes ) {
$coupon_code = isset( $attributes['couponCode'] ) ? sanitize_text_field( $attributes['couponCode'] ) : 'SAMPLECODE';
$discount_value = isset( $attributes['discountValue'] ) ? sanitize_text_field( $attributes['discountValue'] ) : '10%';
$expiry_date = isset( $attributes['expiryDate'] ) ? sanitize_text_field( $attributes['expiryDate'] ) : '';
ob_start();
?>
Coupon Code:
Discount:
Expires On:
Next, we'll set up the Vue micro-frontend. This will be managed by a separate build process, typically using Vite or Vue CLI. For this guide, we'll assume a Vite setup.
In the root of your plugin directory (gutenberg-coupon-generator/), create a new directory for your Vue application.
- Create directory:
mkdir vue-app - Change into the directory:
cd vue-app - Initialize a new Vue project (using Vite):
npm create vite@latest . --template vue(or yarn/pnpm equivalent) - Install dependencies:
npm install
Now, we need to configure Vite to build our Vue application into a format that WordPress can consume. This involves setting the correct build target and output directory.
Modify your vue-app/vite.config.js file as follows:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
outDir: path.resolve(__dirname, '../build'), // Output to the plugin's build directory
emptyOutDir: true, // Clear the build directory before each build
rollupOptions: {
output: {
entryFileNames: `index.js`, // Single JS entry file
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return `index.css`; // Single CSS file
}
return assetInfo.name;
},
manualChunks: undefined, // Ensure no chunking for a single file output
},
},
lib: {
entry: path.resolve(__dirname, 'src/main.js'), // Your main Vue entry point
name: 'GutenbergCouponGenerator',
fileName: (format) => `index.js`, // Output filename for JS
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
The build.lib configuration is crucial for creating a library-like output suitable for WordPress. We're directing the output to the parent directory's build folder, which is where our PHP file expects to find index.js and index.css.
Gutenberg Block Registration and Editor Component
Now, let's define the Gutenberg block's attributes and register it using the WordPress Script API. This registration will point to the JavaScript file generated by our Vue build process.
The wp_register_script and register_block_type calls in gutenberg-coupon-generator.php are set up to load build/index.js and build/index.css. The $asset_file variable dynamically loads dependencies and the version number from build/index.asset.php, which is automatically generated by `@wordpress/scripts` or our Vite build if configured correctly.
The render_callback in register_block_type is essential for server-side rendering. This ensures that the coupon block displays correctly even when JavaScript is disabled or before it loads on the frontend.
Vue Editor Component Implementation
Within your Vue application (vue-app/src/), create the main entry point and the editor component.
vue-app/src/main.js:
import { registerBlockType } from '@wordpress/blocks';
import { App } from './App'; // Our main Vue component
// Import styles for the editor
import './editor.scss';
registerBlockType('gutenberg-coupon-generator/coupon-block', {
edit: App,
save: () => null, // We'll use a server-side render_callback for saving
});
The save: () => null is a common pattern when using server-side rendering callbacks. WordPress will not attempt to serialize the block's content from the editor; instead, it will rely entirely on the PHP callback to generate the frontend HTML.
vue-app/src/App.vue:
<template>
<div class="coupon-block-editor">
<Panel header="Coupon Settings">
<TextControl
label="Coupon Code"
v-model="attributes.couponCode"
@input="updateAttributes"
/>
<TextControl
label="Discount Value (e.g., 10%, $5)"
v-model="attributes.discountValue"
@input="updateAttributes"
/>
<DateTimePicker
label="Expiry Date"
v-model="attributes.expiryDate"
@input="updateAttributes"
/>
</Panel>
<div class="coupon-preview">
<h4>Preview:</h4>
<div class="coupon-code">
<strong>Coupon Code:</strong> <span>{{ attributes.couponCode || 'N/A' }}</span>
</div>
<div class="discount-info">
<strong>Discount:</strong> <span>{{ attributes.discountValue || 'N/A' }}</span>
</div>
<div v-if="attributes.expiryDate" class="expiry-info">
<strong>Expires On:</strong> <span>{{ formattedExpiryDate }}</span>
</div>
</div>
</div>
</template>
<script>
import { useBlockProps } from '@wordpress/block-editor';
import { Panel, TextControl, DateTimePicker } from '@wordpress/components';
import { computed } from 'vue';
export default {
components: {
Panel,
TextControl,
DateTimePicker,
},
props: {
attributes: {
type: Object,
required: true,
},
setAttributes: {
type: Function,
required: true,
},
},
setup(props) {
const blockProps = useBlockProps();
const updateAttributes = (value, key) => {
props.setAttributes({ [key]: value });
};
const formattedExpiryDate = computed(() => {
if (!props.attributes.expiryDate) return '';
try {
const date = new Date(props.attributes.expiryDate);
return date.toLocaleDateString(); // Adjust formatting as needed
} catch (e) {
console.error("Invalid date format:", props.attributes.expiryDate, e);
return 'Invalid Date';
}
});
return {
blockProps,
updateAttributes,
formattedExpiryDate,
};
},
};
</script>
<style lang="scss">
.coupon-block-editor {
padding: 15px;
border: 1px solid #ddd;
background-color: #f9f9f9;
.coupon-preview {
margin-top: 20px;
padding: 10px;
border: 1px dashed #ccc;
background-color: #fff;
h4 {
margin-top: 0;
}
}
}
</style>
In this Vue component:
- We import necessary components from
@wordpress/block-editorand@wordpress/components. useBlockPropsis used to get the necessary props for the block wrapper.TextControlandDateTimePickerare used for input fields.- The
attributesprop is bound to our input fields, andsetAttributesis called on input to update the block's state. - A computed property
formattedExpiryDateis used for cleaner date display in the preview. - The
save: () => nullinmain.jsmeans the editor component is responsible for rendering the preview, and the actual saved content is handled by the PHP callback.
We also need a basic editor.scss file for styling the block in the editor.
.coupon-block-editor {
// Add any specific styles for the editor interface here
border: 1px solid #e0e0e0;
padding: 10px;
background-color: #ffffff;
}
Build Process and Integration
With the Vue application structured, we need to define the build script in vue-app/package.json to compile the Vue code into the WordPress plugin's build directory.
Modify vue-app/package.json:
{
"name": "vue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"@wordpress/block-editor": "^12.10.0",
"@wordpress/blocks": "^12.10.0",
"@wordpress/components": "^26.10.0",
"@wordpress/i18n": "^4.45.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"sass": "^1.69.5",
"vite": "^4.4.5"
}
}
Note the addition of WordPress packages as dependencies. These are typically installed via npm within the Vue project's scope. You might need to run npm install @wordpress/block-editor @wordpress/blocks @wordpress/components @wordpress/i18n separately if they aren't automatically included.
To build the Vue application, navigate to the vue-app directory in your terminal and run:
cd gutenberg-coupon-generator/vue-app npm run build
This command will execute Vite's build process, creating index.js and index.css in the parent directory's build/ folder. WordPress will then be able to enqueue these assets.
After the build, WordPress will automatically generate the build/index.asset.php file. This file contains metadata about the script's dependencies and version, which is crucial for WordPress to correctly load the script.
Finalizing and Testing
Ensure your plugin directory structure looks like this:
gutenberg-coupon-generator/
├── build/
│ ├── index.js
│ ├── index.css
│ └── index.asset.php
├── gutenberg-coupon-generator.php
└── vue-app/
├── src/
│ ├── App.vue
│ ├── main.js
│ └── editor.scss
├── vite.config.js
└── package.json
└── ... (other Vue project files)
Activate the "Gutenberg Coupon Generator" plugin in your WordPress admin area. Then, navigate to the post or page editor. You should be able to search for "Coupon Generator" and add the block. The Vue-powered editor interface will appear, allowing you to input coupon details. Save the post, and view it on the frontend to verify the server-rendered output.
This setup provides a robust way to leverage modern JavaScript frameworks within Gutenberg blocks, offering a more dynamic and maintainable development experience for complex editor interfaces.