When developing custom blocks for WordPress, it’s best practice to register them within plugins rather than themes. This guide details the file structure as produced by the create-block tool.
Registering blocks in plugins ensures they stay accessible even when users switch themes.
Plugin file structure
my-block-plugin/
├── plugin.php
├── package.json
├── src/
│ ├── block.json
│ ├── index.js
│ ├── edit.js
│ ├── save.js
│ ├── style.scss
│ ├── editor.scss
│ ├── render.php (optional)
│ └── view.js (optional)
└── build/
├── block.json
├── index.js
├── index.asset.php
├── style-index.css
└── index.css
Main plugin file (plugin.php)
The main PHP file registers the block on the server using the register_block_type() function:
<?php
/**
* Plugin Name: My Block Plugin
* Description: A custom block plugin
* Version: 1.0.0
*/
function my_block_plugin_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'my_block_plugin_register_block' );
Package configuration (package.json)
The package.json file configures the Node.js project, defining dependencies and build scripts:
{
"name": "my-block-plugin",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^27.0.0"
}
}
Source folder (src/)
The src folder contains raw, uncompiled code that gets processed during the build.
block.json
Defines the block’s metadata, streamlining registration across client and server:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/my-block",
"title": "My Custom Block",
"category": "widgets",
"icon": "smiley",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
Key properties in block.json:
editorScript: Path to bundled index.js (built from src/index.js)
style: Path to bundled style-index.css (built from src/style.scss)
editorStyle: Path to bundled index.css (built from src/editor.scss)
render: Path to render.php for dynamic blocks
index.js
The entry point for JavaScript loaded in the Block Editor:
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: Edit,
save,
} );
edit.js
The React component that renders the block’s editing interface:
import { useBlockProps } from '@wordpress/block-editor';
export default function Edit() {
return (
<p { ...useBlockProps() }>
Hello World - Block Editor
</p>
);
}
save.js
Returns the static HTML markup saved to the database:
import { useBlockProps } from '@wordpress/block-editor';
export default function save() {
return (
<p { ...useBlockProps.save() }>
Hello World - Frontend
</p>
);
}
style.scss
Styles loaded in both the Block Editor and front end:
.wp-block-my-plugin-my-block {
padding: 20px;
background-color: #f0f0f0;
}
editor.scss
Additional styles applied only in the Block Editor:
.wp-block-my-plugin-my-block {
border: 2px dashed #ccc;
}
render.php (optional)
Defines server-side rendering for dynamic blocks:
<?php
$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
<p><?php echo esc_html( $attributes['content'] ); ?></p>
</div>
view.js (optional)
JavaScript loaded on the front end when the block is displayed:
console.log( 'Block loaded on frontend' );
Build folder (build/)
The build folder contains compiled and optimized code generated by wp-scripts:
Source compilation
Modern JavaScript is transpiled to be compatible with wider browser support
Asset bundling
Files are minified and bundled for efficient loading
WordPress integration
WordPress enqueues files from the build folder
Always point register_block_type() to the build directory, not src.
Build process
Run these commands to build your block:
For development with automatic rebuilding:
Next steps
Block metadata
Learn about block.json properties
Block registration
Understand how blocks are registered