WordPress.org

WordPress Developer Blog

Refactoring the multi-block plugin: Build smarter, register cleaner, scale easier

Refactoring the multi-block plugin: Build smarter, register cleaner, scale easier

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.

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 -v
  • npm -v

If your system versions are lower than required, you can update them using nvm

  • nvm install 20
  • nvm 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-block folder
  • Inside src create a folder named blocks

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.

A static and dynamic block showing 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.

An interactivity block showing 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 the build directory with predictable filenames.
  • The second (moduleConfig) is required to support ES module output, which WordPress uses for interactive blocks when built with the --experimental-modules flag. 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.

7 responses to “Refactoring the multi-block plugin: Build smarter, register cleaner, scale easier”

  1. John Russell Avatar

    I finally got around to implementing this new way of registering blocks in a plugin that has 24 custom blocks and we saw an improvement of about 17% reduced page load times on the front-end of the site. That’s a pretty substantial difference!

    1. Troy Chaplin Avatar

      That’s a pretty substantial difference! I would assume that relates to the significant improvements made to block loading with the newer metadata_collection functions?

      1. John Russell Avatar

        That’s correct. We used to use register_block_type for each block (and pointed to the directory where the block.json file was located) and now we incorporated —blocks-manifest into our build process and use wp_register_block_types_from_metadata_collection to register all blocks.

  2. Marco Torres Walker Avatar
    Marco Torres Walker

    Hi, I’ve tried with no success. How would you include scss files for the editor. I have a editor-styles.scss file in src, and I tried two different approaches to enqueue the file in the editor, but it’s not working. How would you do it?

    1. Britton Walker Avatar

      You need to create a webpack entry point that importa your editor styles and enqueue that from your build asset editor-styles.css.

    2. Troy Chaplin Avatar

      Hey Marco, thanks for the question, I’m sure others would also be interested. When I have some time I’ll look at adding a follow up section to cover this!

  3. Mark Dionnie Bulingit Avatar
    Mark Dionnie Bulingit

    Nice Tutorial but What’s the motive behind “Enqueue additional assets” what are these assets? fonts? images? and what are the things that I can put on editor-script.js, frontend-script.js? how about CSS files e.g. the editor.scss or frontend.scss

Leave a Reply

Your email address will not be published. Required fields are marked *