@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.
Plugin file structure (recommended)
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"
}
| Field | Loaded |
|---|---|
editorScript | Editor only (wp-admin post-edit screens) |
viewScript | Front-end only (when block is on the page) |
style | Both editor and front-end |
editorStyle | Editor 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:
- Install fresh on a clean WordPress
- Activate without errors
- Block appears in inserter
- Insert + save a post — block survives a refresh
- 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.
Get the next one in your inbox. Practical tips, no fluff.
More tutorials
View all- WordPress
How to Prepare Your Plugin or Theme for WordPress 7.0
A step-by-step compatibility process for shipping a WordPress 7.0-ready release — set up a test site, audit your blocks, fix the breaking changes, and ship with confidence.
22 min
- WordPress
WordPress 7.0 for Block Developers: Breaking Changes
WordPress 7.0 enforces the iframed editor, broadens contentOnly mode, and drops PHP 7.3. Here are the breaking changes block and plugin developers must fix before users upgrade.
25 min
- WordPress
WordPress 7.0 AI: Client, Abilities & Connectors
The headline feature of WordPress 7.0 is native AI in Core. Here is what the WP AI Client, the Abilities API, and the Connectors system actually mean for plugin developers.
25 min