MW
Tutorial

Building a Block Plugin

Package your block as a proper WordPress plugin — file structure, asset enqueueing, server-side render callbacks, plugin metadata.

intermediate 35 min May 13, 2026
A working custom block from earlier parts

BUILDING A BLOCK PLUGIN

@wordpress/create-block gave us a working plugin in Part 2. Now we look at every piece — file structure, the PHP entry, asset enqueueing, dynamic render — so you can build a plugin from scratch when you need to.

my-first-block/
my-first-block.php plugin bootstrap (header + register_block_type)
readme.txt wp.org plugin readme
package.json
.gitignore exclude node_modules + build/
src/
block.json block manifest
index.js registerBlockType call
edit.js
save.js
render.php server-side render (dynamic blocks only)
style.scss
editor.scss
build/ webpack output, gitignored
patterns/ pattern files (Part 8)
languages/ translation files

The plugin header

my-first-block.php must start with a WordPress plugin header. Without this, WP doesn’t know it’s a plugin:

<?php
/**
 * Plugin Name:       My First Block
 * Description:       A callout block for posts and pages.
 * Version:           1.0.0
 * Author:            Mahesh Waghmare
 * Author URI:        https://maheshwaghmare.com
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-first-block
 * Requires at least: 6.4
 * Requires PHP:      7.4
 *
 * @package MyFirstBlock
 */

if (!defined('ABSPATH')) {
  exit;
}

// Register the block from block.json
add_action('init', function() {
  register_block_type(__DIR__ . '/build');
});

The single register_block_type() call points at the build/ directory (where block.json is copied during the webpack build). WordPress reads block.json and wires everything — JS, CSS, attributes, supports — automatically.

Asset enqueueing — handled by block.json

You don’t manually enqueue scripts. Listing them in block.json is enough:

{
  "name": "my-first-block/callout",
  "editorScript": "file:./index.js",
  "viewScript":   "file:./view.js",
  "style":        "file:./style-index.css",
  "editorStyle":  "file:./index.css"
}
FieldLoaded
editorScriptEditor only (wp-admin post-edit screens)
viewScriptFront-end only (when block is on the page)
styleBoth editor and front-end
editorStyleEditor only

WordPress conditionally enqueues each based on context — no front-end loads index.js (the editor bundle).

Dynamic blocks — render server-side

A static block stores its HTML in post_content and serves it verbatim. A dynamic block stores only the block comment + attributes; PHP renders the HTML on each request.

Use dynamic when the output depends on database state (latest posts, current user, dates).

In block.json:

"render": "file:./render.php"

In src/render.php (NOT build/render.php — webpack copies it):

<?php
// $attributes is auto-populated from the block's attributes
// $content is the InnerBlocks content (if any)
// $block is the parsed block object

$type = $attributes['calloutType'] ?? 'info';
$title = $attributes['title'] ?? '';
?>
<div class="my-callout my-callout-<?php echo esc_attr($type); ?>">
  <h4 class="my-callout-title"><?php echo wp_kses_post($title); ?></h4>
  <div class="my-callout-body"><?php echo $content; ?></div>
</div>

In src/save.js, return null to opt into dynamic rendering:

export default function Save() {
  return null;
}

Building for distribution

npm run build produces the build/ directory. That + your PHP files are what you ship. Common .gitignore:

node_modules/
build/
*.zip

For wp.org submission, you typically build then zip the plugin folder (excluding node_modules and src/). A .wp-distignore file controls what gets included if you use WP’s wp-scripts plugin-zip command.

Verification

After packaging:

  1. Install fresh on a clean WordPress
  2. Activate without errors
  3. Block appears in inserter
  4. Insert + save a post — block survives a refresh
  5. View the front-end — block renders correctly

What’s next

Part 11 covers testing — @wordpress/scripts test, React DevTools, block validation, common pitfalls and how to spot them early.

You've completed this tutorial!

Get the next one in your inbox. Practical tips, no fluff.

Subscribe

Get weekly notes in your inbox

Practical tips, tutorials and resources. No spam.