The more blocks I build, the more I want a plugin structure that gets out of my way. I don’t want to rethink folder structures, rewrite the same registration logic, or fight with the build process every time I add something new.
In How to Build a Multi-Block Plugin, I shared a simple foundation for managing multiple blocks in a single plugin. That guide focused on core concepts like organizing files, registering blocks, and getting things working quickly, with a build strategy that bundled everything into a single file, which worked well for CDN delivery but didn’t reflect how individual blocks are ideally loaded in WordPress.
This article builds on that approach, shaped by real-world usage and feedback, and refines it into something more powerful. I’ll walk through creating a scalable setup that supports static, dynamic, and interactive blocks, separate global assets, coding standards, and a modular structure that stays manageable as your plugin grows.
Table of Contents
Pre-requisites
This article assumes you already have a local development environment running WordPress, either through WordPress Studio, wp-env or a custom Docker container using the official WordPress image.
You’ll also need a recent version of Node.js and npm to use @wordpress/create-block.
To view the required versions:
npm view @wordpress/create-block engines
To check your current setup:
node -vnpm -v
If your system versions are lower than required, you can update them using nvm
nvm install 20nvm use 20
This ensures compatibility and avoids build issues later in the process.
If you’d like to follow along or reference a working example, the full plugin built in this article is available on GitHub with each section of this article is represented in a branch.
Basic plugin setup
To start, I will scaffold a new plugin and restructure it to support multiple blocks in a clean, organized way. I will add one static and one dynamic block using @wordpress/create-block, placing each in its own folder. I will also update the registration function so new blocks are picked up automatically, no need to register each one manually, just drop them into the right folder.
Creating the plugin base
The first thing I will do is scaffold a new plugin by running npx @wordpress/create-block@latest advanced-multi-block in the plugins folder of my local WordPress environment. This sets me up with a single static block plugin.
Update plugin structure
Next, I quickly refactor the default structure to support a well-organized multi-block plugin that can grow with the project.
Instead of keeping blocks nested in a single folder, I move them into a top-level src/blocks directory. This setup not only makes the block structure easier to scan, but also leaves room to grow. As I add other global assets later, like global JavaScript or editor styles, having clear separation from the start helps keep things tidy and maintainable.
Inside the src directory, I make the following changes:
- Delete the
src/advanced-multi-blockfolder - Inside
srccreate a folder namedblocks
Creating static and dynamic blocks
Now that I have a clean, organized structure in place, I can start adding blocks. I use @wordpress/create-block with the --no-plugin flag so that I can generate each block within my existing directory layout rather than creating a new standalone plugin.
Depending on the block type, I also add the --variant flag. And to keep things consistent for internationalization, I specify a consistent --textdomain.
I create one static and one dynamic block by running the following commands inside the src/blocks directory:
// Create a static block
npx @wordpress/create-block@latest slider --textdomain advanced-multi-block --no-plugin
// Create a dynamic block
npx @wordpress/create-block@latest banner --textdomain advanced-multi-block --no-plugin --variant dynamic
Updating the block registration function
The function scaffolded by @wordpress/create-block already provides a solid base for registering multiple blocks. It defines a function named create_block_advanced_multi_block_block_init (which I’ll rename for clarity). To support the updated folder structure that places all blocks under src/blocks, I only needed to make a minimal change: updating the block path in three places to include the new directory structure.
Here’s the revised version of that function with a shorter name:
function register_blocks() {
$build_dir = __DIR__ . '/build/blocks';
$manifest = __DIR__ . '/build/blocks-manifest.php';
// WP 6.8+: one-call convenience.
if ( function_exists( 'wp_register_block_types_from_metadata_collection' ) ) {
wp_register_block_types_from_metadata_collection( $build_dir, $manifest );
return;
}
// WP 6.7: index the collection, then loop and register each block from metadata.
if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
wp_register_block_metadata_collection( $build_dir, $manifest );
$manifest_data = require $manifest;
foreach ( array_keys( $manifest_data ) as $block_type ) {
register_block_type_from_metadata( $build_dir . '/' . $block_type );
}
return;
}
// WP 5.5-6.6: no collection APIs; just loop the manifest directly.
if ( function_exists( 'register_block_type_from_metadata' ) ) {
$manifest_data = require $manifest;
foreach ( array_keys( $manifest_data ) as $block_type ) {
register_block_type_from_metadata( $build_dir . '/' . $block_type );
}
return;
}
}
add_action( 'init', 'register_blocks' );
With this setup, new blocks can be added by running the npx create-block command in src/blocks. The manifest handles registration automatically, no updates to the registration logic required.
Testing the basic setup
With everything in place, I run: npm run build
Once the build completes, I can find both the Slider and Banner blocks listed under the Widgets section in the Block Inserter.

Add an interactivity block
Now that I’ve added static and dynamic blocks, the next step is to add an interactive one. Interactive blocks are built a little differently, and they require a few updates to the registration function and build process to work correctly.
Creating an interactive block
Unlike dynamic blocks, interactive blocks don’t use the --variant flag. Instead, I use the --template option and point it to a starter template provided by WordPress. This generates the necessary files for a client-side interactive block.
Inside src/blocks, I run the following:
// Create an interactive block
npx @wordpress/create-block@latest toggle --textdomain advanced-multi-block --template @wordpress/create-block-interactive-template --no-plugin
Modifying the build process
Interactive blocks also require a small change to the build configuration. I update the build and start commands in package.json to include the --experimental-modules flag. This ensures the scripts compile correctly:
// Updated build command
"build": "wp-scripts build --experimental-modules --blocks-manifest"
// Updated start command
"start": "wp-scripts start --experimental-modules --blocks-manifest"
Testing the three block types
With all three block types in place, I run: npm run build
Once the build completes I can see the Slider, Banner, and Toggle blocks listed under the Widgets section in the Block Inserter.

Enqueue additional assets
This plugin isn’t just about registering blocks. The block editor provides a lot of flexibility, and I often want to include enhancements that go beyond individual blocks — things like registering block variations, defining style options, or adding contextual tools to the editor.
I compile two standalone scripts: one for the editor and one for the frontend. These live outside of individual block folders and provide a centralized way to manage features that span multiple blocks. Each script has a matching .asset.php file that ensures dependencies and versioning are handled automatically during the build.
Adding an enqueues class
To register these shared assets, I create a simple enqueue function for each script — one for the block editor and one for the frontend. These are kept separate from the block registration logic, helping maintain a modular structure as the plugin evolves.
I place these in my functions.php file below the block registration function:
/**
* Enqueues the block assets for the editor
*/
function enqueue_block_assets() {
$asset_file = include plugin_dir_path( __FILE__ ) . 'build/editor-script.asset.php';
wp_enqueue_script(
'editor-script-js',
plugin_dir_url( __FILE__ ) . 'build/editor-script.js',
$asset_file['dependencies'],
$asset_file['version'],
false
);
}
add_action( 'enqueue_block_editor_assets', 'enqueue_block_assets' );
/**
* Enqueues the block assets for the frontend
*/
function enqueue_frontend_assets() {
$asset_file = include plugin_dir_path( __FILE__ ) . 'build/frontend-script.asset.php';
wp_enqueue_script(
'frontend-script-js',
plugin_dir_url( __FILE__ ) . 'build/frontend-script.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
}
add_action( 'wp_enqueue_scripts', 'enqueue_frontend_assets' );
Adding script assets
Editor Script
I create a file named editor-script.js in the src directory. This will be compiled and loaded into the block editor whenever it’s active.
/**
* Block Editor Script Functionality
*
* The following scripts are compiled into a single asset and loaded into the block editor.
*
*/
// import editor scripts here.
Frontend Script
I also create a file named frontend-script.js in the src directory for frontend-specific behavior. This will be compiled and loaded on the frontend when blocks or pages that need it are displayed.
/**
* Frontend Script Functionality
*
* The following scripts are compiled into a single asset and loaded into the frontend.
*
*/
// import frontend scripts here.
Adding a webpack file
To build global scripts separately from the core block build process, I create a custom Webpack configuration file named webpack.config.js in the plugin root. This allows me to reuse and extend the default configuration provided by @wordpress/scripts, without interfering with how WordPress builds interactive blocks.
Here’s what the file looks like:
const [ scriptConfig, moduleConfig, ] = require('@wordpress/scripts/config/webpack.config');
const path = require('path');
module.exports = [
{
...scriptConfig,
entry: {
...scriptConfig.entry(),
'editor-script': path.resolve(__dirname, 'src/editor-script.js'),
'frontend-script': path.resolve(__dirname, 'src/frontend-script.js'),
},
},
moduleConfig,
];
This file exports an array with two configurations:
- The first (
scriptConfig) is the standard Webpack configuration used by WordPress for traditional (non-interactive) scripts. Here, I extend it by adding two new entry points: one for editor-side JavaScript and one for frontend behavior. These scripts will be compiled into thebuilddirectory with predictable filenames. - The second (
moduleConfig) is required to support ES module output, which WordPress uses for interactive blocks when built with the--experimental-modulesflag. Including this config ensures compatibility with that modern module system, even though this specific file focuses on building non-interactive assets.
By extending the default Webpack config to include editor and frontend scripts, I can bundle global assets alongside block builds in a single process. This keeps everything modular while ensuring compatibility with WordPress’s interactive block system via the moduleConfig.
Conclusion
Building a multi-block plugin doesn’t have to mean starting from scratch every time. By refining the default structure, streamlining registration, and supporting different block types and shared assets, you can create a setup that scales with your needs — whether you’re building one block or twenty.
This approach is the result of real-world usage, iteration, and community feedback. It’s flexible enough to support custom workflows and powerful enough to keep your plugin organized as it grows.
If you have questions, ideas, or improvements, I’d love to hear them. The beauty of working in open source is that we’re all building on top of each other’s work.
Props to @meszarosrob and @milana_cap for reviewing this article and offering feedback.
Leave a Reply