For the first time in eight years, you can register real Gutenberg blocks using only PHP - no React, no npm, no Webpack, no build step. Just a plugin file, register_block_type(), and a new autoRegister flag introduced in WordPress 7.0.
This guide walks you through building a complete plugin with two production-ready blocks - a Notice block and a Category Display block - from setup to shipping. The complete plugin, including all CSS and helper files, is on GitHub. Everything you need to follow along is in this article; everything you need to ship is in the repository.
A quick clarification before we start: Server-rendered Gutenberg blocks have existed for years, but until now they always required a JavaScript layer using ServerSideRender to scaffold the block in the editor - even though the actual rendering happened in PHP. You still needed block.json, an edit.js file, and a build step. WordPress 7.0's autoRegister => true flag removes that JavaScript ceremony entirely.
We'll cover architecture and comparisons at the end. First, let's build.
Who Is This Guide For?
WordPress developers comfortable with PHP who want to build blocks without a JavaScript build pipeline. Basic knowledge of register_block_type() helps but isn't required - this guide explains each step.
What Will You Build?
Notice Block: Title, description, and four skin variants (Alert, Warning, Success, Info)
Category Display Block: Show post categories in Grid, List, or Compact layout, with optional images (via ACF if installed, plugin placeholder if not), post counts, and recent post lists
Prerequisites
WordPress 7.0 or later
Local development environment (LocalWP, WordPress Studio, wp-env, or similar)
Code editor
Basic PHP knowledge
Optional: Advanced Custom Fields - the Category Display block supports ACF images if available and falls back to a placeholder otherwise
💡
Tips: Test all changes on a local or staging site first. WordPress 7.0 is recent and PHP-only blocks are a new API - verify everything before shipping to production.
Step 1: Set Up the Plugin
The plugin contains both blocks, organized cleanly so more blocks can be added later without refactoring.
1.1 - File structure
Create this folder inside wp-content/plugins/:
wpvibes-blocks/
├── wpvibes-blocks.php
├── includes/
│ └── class-blocks-loader.php
├── blocks/
│ ├── class-notice-block.php
│ └── class-category-display-block.php
└── assets/
├── notice.css
├── category-display.css
└── images/
└── category-placeholder.png
Why this structure?
The main plugin file loads everything. The loader class registers all blocks in one loop. Each block lives in its own file. Each block's CSS lives in its own file too. Adding a new block later means creating one new class file, adding one line to the loader, and registering one stylesheet. This pattern scales - a plugin with 20 blocks stays maintainable; a single 5,000-line file does not.
1.2 - The main plugin file
Create wpvibes-blocks.php:
<?php
/**
* Plugin Name: WPVibes Blocks
* Description: PHP-only Gutenberg blocks — Notice and Category Display.
* Version: 1.0.0
* Author: WPVibes
* Requires at least: 7.0
* Requires PHP: 7.4
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'WPVIBES_BLOCKS_VERSION', '1.0.0' );
define( 'WPVIBES_BLOCKS_PATH', plugin_dir_path( __FILE__ ) );
define( 'WPVIBES_BLOCKS_URL', plugin_dir_url( __FILE__ ) );
require_once WPVIBES_BLOCKS_PATH . 'blocks/class-notice-block.php';
require_once WPVIBES_BLOCKS_PATH . 'blocks/class-category-display-block.php';
require_once WPVIBES_BLOCKS_PATH . 'includes/class-blocks-loader.php';
WPVibes_Blocks_Loader::init();
The main file wires things together. Nothing more.
1.3 - The loader
Create includes/class-blocks-loader.php:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WPVibes_Blocks_Loader {
private static $block_classes = array(
'WPVibes_Notice_Block',
'WPVibes_Category_Display_Block',
);
public static function init() {
add_action( 'init', array( __CLASS__, 'register_styles' ), 5 );
add_action( 'init', array( __CLASS__, 'register_all_blocks' ), 10 );
}
public static function register_styles() {
wp_register_style(
'wpvibes-notice',
WPVIBES_BLOCKS_URL . 'assets/notice.css',
array(),
WPVIBES_BLOCKS_VERSION
);
wp_register_style(
'wpvibes-category-display',
WPVIBES_BLOCKS_URL . 'assets/category-display.css',
array(),
WPVIBES_BLOCKS_VERSION
);
}
public static function register_all_blocks() {
foreach ( self::$block_classes as $class_name ) {
if ( ! class_exists( $class_name ) ) {
continue;
}
$block = new $class_name();
$block->register();
}
}
}
Activate the plugin from Plugins → Installed Plugins. Nothing visible yet - the blocks come next.
Step 2: Build the Notice Block
The Notice block shows a message with a title, description, and one of four skin options (Alert, Warning, Success, Info). It's the simpler of the two blocks - a good starting point for learning the API.
2.1 - Create the block class
Create blocks/class-notice-block.php:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WPVibes_Notice_Block {
public function register() {
register_block_type(
'wpvibes/notice',
array(
'title' => __( 'Notice', 'wpvibes-blocks' ),
'description' => __( 'A notice or callout with 4 skins.', 'wpvibes-blocks' ),
'category' => 'text',
'icon' => 'warning',
'style' => 'wpvibes-notice',
'editor_style' => 'wpvibes-notice',
'attributes' => $this->get_attributes(),
'supports' => $this->get_supports(),
'render_callback' => array( $this, 'render' ),
)
);
}
// Attribute, supports, and render methods below
Two keys worth pointing out:
styleandeditor_style- attach the stylesheet registered in the loader (same handle for both). WordPress loads it only when the block is used.render_callback- the PHP function that returns the block's HTML.
2.2 - Define the attributes
Add this method to the class:
public function get_attributes() {
return array(
'title' => array(
'type' => 'string',
'default' => 'Important notice',
'label' => __( 'Title', 'wpvibes-blocks' ),
),
'description' => array(
'type' => 'string',
'default' => 'Add your notice message here.',
'label' => __( 'Description', 'wpvibes-blocks' ),
),
'skin' => array(
'type' => 'string',
'enum' => array( 'Alert', 'Warning', 'Success', 'Info' ),
'default' => 'Info',
'label' => __( 'Skin', 'wpvibes-blocks' ),
),
);
}
Three attributes:
title- a string, so WordPress auto-generates a text input in the sidebar.description- same, another text input.skin- string withenum, so WordPress auto-generates a dropdown with exactly those four options.
The label key controls what the user sees. Without it, WordPress derives a label from the attribute name.
2.3 - Add block supports
public function get_supports() {
return array(
'autoRegister' => true,
'color' => array( 'text' => true, 'background' => true ),
'spacing' => array( 'padding' => true, 'margin' => true ),
'border' => array( 'color' => true, 'radius' => true, 'style' => true, 'width' => true ),
);
}
Two things happen here:
autoRegister => true- the flag that tells WordPress to register this block from PHP alone, without JavaScript scaffolding.color,spacing,border- turn on native design panels in the sidebar. Users get Color, Spacing, and Border controls automatically.
2.4 - Write the render callback
public function render( $attributes ) {
$skin_class = 'wpvibes-notice--' . strtolower( $attributes['skin'] );
$wrapper = get_block_wrapper_attributes(
array(
'class' => 'wpvibes-notice ' . $skin_class,
'role' => 'alert',
)
);
$title = ! empty( $attributes['title'] )
? '<h4 class="wpvibes-notice__title">' . esc_html( $attributes['title'] ) . '</h4>'
: '';
$description = ! empty( $attributes['description'] )
? '<p class="wpvibes-notice__description">' . esc_html( $attributes['description'] ) . '</p>'
: '';
return sprintf(
'<div %1$s><div class="wpvibes-notice__icon"></div><div class="wpvibes-notice__content">%2$s%3$s</div></div>',
$wrapper,
$title,
$description
);
}
}
Key points:
get_block_wrapper_attributes()returns HTML attributes carrying every design choice the user made - color, background, padding, border. Always use it.esc_html()on all user-provided content. Standard WordPress escaping.BEM naming -
wpvibes-notice(base) pluswpvibes-notice--alert(variant). Predictable CSS.
2.5 - Add the CSS
.wpvibes-notice {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
border-left: 4px solid currentColor;
border-radius: 4px;
}
.wpvibes-notice--alert { background: #fee2e2; color: #991b1b; border-left-color: #dc2626; }
.wpvibes-notice--warning { background: #fef3c7; color: #92400e; border-left-color: #f59e0b; }
.wpvibes-notice--success { background: #d1fae5; color: #065f46; border-left-color: #10b981; }
.wpvibes-notice--info { background: #dbeafe; color: #1e3a8a; border-left-color: #3b82f6; }
💡
Notes: The full CSS (icon circles, title and description typography, hover states) is in the plugin repository at assets/notice.css. Copy the complete file into the plugin's assets/ folder before testing.
2.6 - See it working
Create a new page or post
Click + in the block inserter and search for Notice
Try each skin from the sidebar dropdown
Adjust the native Color and Padding controls
The first PHP-only block is done. No JavaScript. No build step. No block.json.
💡
Notes: The complete file for reference is at blocks/class-notice-block.php.
Step 3: Build the Category Display Block
The Category Display block shows post categories in Grid, List, or Compact layout. For each category: name, post count, an image (from ACF if installed, plugin placeholder otherwise), and a list of recent posts.
This block covers all four supported attribute types - string, integer, boolean, and enum - in one useful example.
3.1 - Create the block class
Create blocks/class-category-display-block.php:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WPVibes_Category_Display_Block {
public function register() {
register_block_type(
'wpvibes/category-display',
array(
'title' => __( 'Category Display', 'wpvibes-blocks' ),
'description' => __( 'Display categories with post count, image, and recent posts.', 'wpvibes-blocks' ),
'category' => 'widgets',
'icon' => 'category',
'style' => 'wpvibes-category-display',
'editor_style' => 'wpvibes-category-display',
'attributes' => $this->get_attributes(),
'supports' => $this->get_supports(),
'render_callback' => array( $this, 'render' ),
)
);
}
// Attributes, supports, and render methods below
3.2 - Define the attributes (all 4 types)
public function get_attributes() {
return array(
// ENUM — dropdown
'layout' => array(
'type' => 'string',
'enum' => array( 'Grid', 'List', 'Compact' ),
'default' => 'Grid',
'label' => __( 'Layout', 'wpvibes-blocks' ),
),
// INTEGER — number input with min/max
'columns' => array(
'type' => 'integer',
'default' => 3,
'minimum' => 2,
'maximum' => 4,
'label' => __( 'Columns (Grid only)', 'wpvibes-blocks' ),
),
// INTEGER
'postCount' => array(
'type' => 'integer',
'default' => 5,
'minimum' => 1,
'maximum' => 20,
'label' => __( 'Posts per Category', 'wpvibes-blocks' ),
),
// STRING — text input (ACF field key)
'acfFieldKey' => array(
'type' => 'string',
'default' => 'term_featured_image',
'label' => __( 'ACF Image Field Key', 'wpvibes-blocks' ),
),
// ENUM — dropdown
'orderBy' => array(
'type' => 'string',
'enum' => array( 'Name', 'Post Count', 'Slug' ),
'default' => 'Name',
'label' => __( 'Order By', 'wpvibes-blocks' ),
),
// BOOLEAN — toggle
'showImage' => array(
'type' => 'boolean',
'default' => true,
'label' => __( 'Show Image', 'wpvibes-blocks' ),
),
// BOOLEAN
'showPostCount' => array(
'type' => 'boolean',
'default' => true,
'label' => __( 'Show Post Count', 'wpvibes-blocks' ),
),
// BOOLEAN
'showRecentPosts' => array(
'type' => 'boolean',
'default' => true,
'label' => __( 'Show Recent Posts', 'wpvibes-blocks' ),
),
// BOOLEAN
'hideEmpty' => array(
'type' => 'boolean',
'default' => true,
'label' => __( 'Hide Empty Categories', 'wpvibes-blocks' ),
),
);
}
What each attribute type teaches:
layoutandorderBy- string withenum. WordPress auto-generates dropdowns with exactly those choices.columnsandpostCount- integers withminimumandmaximum. WordPress auto-generates number inputs that enforce the ranges.acfFieldKey- a plain string. Text input where the user types the ACF field name.showImage,showPostCount,showRecentPosts,hideEmpty- booleans. Each becomes a toggle switch in the sidebar.
That's every attribute type WordPress 7.0 auto-generates a control for.
3.3 - Block supports
public function get_supports() {
return array(
'autoRegister' => true,
'align' => array( 'wide', 'full' ),
'color' => array( 'text' => true, 'background' => true ),
'spacing' => array( 'padding' => true, 'margin' => true ),
'border' => array( 'color' => true, 'radius' => true, 'width' => true, 'style' => true ),
);
}
Same as the Notice block, with align added so users can center or full-width the block.
3.4 - Write the render callback
public function render( $attributes ) {
$order_by_map = array(
'Name' => 'name',
'Post Count' => 'count',
'Slug' => 'slug',
);
$categories = get_categories(
array(
'hide_empty' => ! empty( $attributes['hideEmpty'] ),
'orderby' => $order_by_map[ $attributes['orderBy'] ] ?? 'name',
'order' => 'ASC',
)
);
if ( empty( $categories ) ) {
return '<p>' . esc_html__( 'No categories to display.', 'wpvibes-blocks' ) . '</p>';
}
$layout = strtolower( $attributes['layout'] );
$layout_class = 'wpvibes-category-display--' . $layout;
$columns_class = ( 'grid' === $layout )
? ' wpvibes-category-display--cols-' . absint( $attributes['columns'] )
: '';
$wrapper = get_block_wrapper_attributes(
array(
'class' => 'wpvibes-category-display ' . $layout_class . $columns_class,
)
);
$items_html = '';
foreach ( $categories as $category ) {
$items_html .= $this->render_category_item( $category, $attributes );
}
return sprintf(
'<div %1$s>%2$s</div>',
$wrapper,
$items_html
);
}
What the render callback does:
Queries categories using the
hideEmptyandorderByattributesHandles empty state with a friendly message
Builds layout classes -
wpvibes-category-display--gridpluswpvibes-category-display--cols-3for column-based gridsLoops through categories and renders each via a helper method
3.5 - Render a single category
private function render_category_item( $category, $attributes ) {
$image_html = '';
if ( ! empty( $attributes['showImage'] ) ) {
$image_url = $this->get_category_image_url( $category, $attributes['acfFieldKey'] );
$image_html = sprintf(
'<div class="wpvibes-category-display__image"><img src="%s" alt="%s" loading="lazy"></div>',
esc_url( $image_url ),
esc_attr( $category->name )
);
}
$count_html = '';
if ( ! empty( $attributes['showPostCount'] ) ) {
$count_html = sprintf(
'<span class="wpvibes-category-display__count">%s</span>',
sprintf(
esc_html( _n( '%d post', '%d posts', $category->count, 'wpvibes-blocks' ) ),
(int) $category->count
)
);
}
$posts_html = '';
if ( ! empty( $attributes['showRecentPosts'] ) && $category->count > 0 ) {
$recent_posts = get_posts(
array(
'category' => $category->term_id,
'posts_per_page' => absint( $attributes['postCount'] ),
'orderby' => 'date',
'order' => 'DESC',
)
);
if ( ! empty( $recent_posts ) ) {
$posts_html .= '<ul class="wpvibes-category-display__posts">';
foreach ( $recent_posts as $post ) {
$posts_html .= sprintf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) )
);
}
$posts_html .= '</ul>';
}
}
return sprintf(
'<div class="wpvibes-category-display__item">%1$s<div class="wpvibes-category-display__content"><h3 class="wpvibes-category-display__name"><a href="%2$s">%3$s</a></h3>%4$s%5$s</div></div>',
$image_html,
esc_url( get_category_link( $category ) ),
esc_html( $category->name ),
$count_html,
$posts_html
);
}
Each part (image, count, recent posts) is built conditionally based on the toggles. If a toggle is off, that part's HTML variable stays empty and contributes nothing to the final output.
3.6 - The ACF image with fallback
The Category Display block uses an ACF image if the site has ACF installed and the field is set. If not, it falls back to a placeholder from the plugin's own assets folder:
private function get_category_image_url( $category, $acf_field_key ) {
// Check if ACF is active AND a field key is provided.
if ( function_exists( 'get_field' ) && ! empty( $acf_field_key ) ) {
// ACF term meta uses 'category_' . term_id as the object ID.
$acf_image = get_field( $acf_field_key, 'category_' . $category->term_id );
if ( $acf_image ) {
// ACF image field can return an array, an ID, or a URL string
// depending on the "Return Format" setting.
if ( is_array( $acf_image ) && ! empty( $acf_image['url'] ) ) {
return $acf_image['url'];
}
if ( is_string( $acf_image ) ) {
return $acf_image;
}
if ( is_numeric( $acf_image ) ) {
$url = wp_get_attachment_image_url( $acf_image, 'medium' );
if ( $url ) {
return $url;
}
}
}
}
// Fallback: plugin's placeholder image.
return WPVIBES_BLOCKS_URL . 'assets/images/category-placeholder.png';
}
}
Three things this pattern teaches:
function_exists()before using an external plugin's function - always. Never assume ACF (or any plugin) is active.The ACF term meta prefix - ACF stores term meta under
category_+term_id. This trips up many developers.Handling ACF's different return formats - ACF image fields can return an array, an ID, or a URL, depending on the "Return Format" setting. The code should handle at least the two common ones.
3.7 - Add the CSS
Create assets/category-display.css with the base structure:
.wpvibes-category-display {
display: grid;
gap: 24px;
}
.wpvibes-category-display--grid {
grid-template-columns: repeat(3, 1fr);
}
.wpvibes-category-display--grid.wpvibes-category-display--cols-2 { grid-template-columns: repeat(2, 1fr); }
.wpvibes-category-display--grid.wpvibes-category-display--cols-4 { grid-template-columns: repeat(4, 1fr); }
.wpvibes-category-display--list { grid-template-columns: 1fr; }
.wpvibes-category-display--compact { grid-template-columns: 1fr; gap: 0; }
💡
Important: The full CSS (card styling, hover states, image aspect ratios, list-layout flex rules, compact-layout borders, and mobile responsiveness) is in the plugin repository at assets/category-display.css. Copy the complete file into the plugin's assets/ folder before testing.
3.8 - Add a placeholder image
Place any image at assets/images/category-placeholder.png - an 800×450 image works well. The plugin repository includes one; a custom branded placeholder also works.
3.9 - See it working
Assign a few posts to different categories
Insert the Category Display block
In the sidebar, try:
* Each layout (Grid, List, Compact)
* Different column counts (2, 3, 4) while in Grid layout
* Toggling Show Image, Show Post Count, Show Recent Posts
* Changing Posts per Category
Two production-ready PHP-only blocks - one simple, one covering the full API - done.
💡
Notes: The complete file for reference is at blocks/class-category-display-block.php.
Debugging Tips
When something breaks during development, enable WordPress debug logging in wp-config.php:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Then check wp-content/debug.log after triggering the issue. Nine times out of ten, a PHP error during registration is the culprit — and it will be in the log with a line number.
For attribute-level debugging (checking what's being passed to the render callback), add temporary error_log() calls at the top of the render function:
error_log( 'Notice block attributes: ' . print_r( $attributes, true ) );
Remove these before shipping.
Get the Full Plugin
The complete plugin - with all CSS, placeholder image, and README - is on GitHub.
How PHP-Only Blocks Work Under the Hood
Now that the plugin builds, here's what actually happens when a PHP-only block is registered and rendered.
Before WordPress 7.0
Building a server-rendered block required these files, at minimum:
my-plugin/
├── src/block/
│ ├── block.json
│ ├── edit.js
│ └── index.js
├── package.json
└── my-plugin.php
The edit.js typically looked like this:
import { ServerSideRender } from "@wordpress/editor";
export default function edit( props ) {
return (
<ServerSideRender
block={ props.name }
attributes={ props.attributes }
/>
);
}
A JavaScript component whose only job was to say "please render this on the server." Then npm install, run a build, and hope everything worked.
With WordPress 7.0
WordPress 7.0 adds one flag: autoRegister => true. When set, WordPress:
Takes the block registration from PHP
Adds it to a JavaScript global in the editor settings
The editor picks it up and uses
ServerSideRenderinternally for the previewWhen a user changes an attribute, the editor calls the REST API, which runs the PHP
render_callback, and swaps in the new HTML
Zero JavaScript written. WordPress handles the ceremony.
The runtime flow
User changes an attribute in the sidebar
↓
Editor sends REST API request
↓
WordPress runs render_callback in PHP
↓
render_callback returns HTML string
↓
Editor swaps HTML into the block preview
This is why every attribute change causes a small delay - there's always a server round-trip. That's the deliberate trade-off: no client-side complexity, but no live reactivity either.
Auto-generated inspector controls
WordPress inspects the attributes array and generates sidebar controls automatically:
| Attribute definition | Generated control |
|---|---|
type: 'string' |
Text input |
type: 'string' + enum: [...]
|
Dropdown (SelectControl) |
type: 'integer' with minimum / maximum
|
Number input with enforced range |
| type: 'boolean' | Toggle switch |
Controls are not generated for object and array types, or for attribute names that conflict with reserved block-supports names like style.
When Should You Use PHP-Only Blocks?
An honest decision table:
| Use PHP-only blocks when | Use JavaScript-registered blocks when |
|---|---|
| Block is display-only (notice, category grid, author box) | Block needs InnerBlocks (nested blocks) |
| Content comes from server data (users, posts, terms, APIs) | Inline rich-text editing is required |
| Replacing a legacy shortcode | Editor experience needs live client-side reactivity |
| Avoiding a JavaScript build pipeline | Interactivity API is needed for state management |
| Simple enough that a server round-trip on each change is acceptable | Real-time visual manipulation (drag handles, instant previews) is essential |
Both approaches are first-class WordPress citizens. PHP-only blocks aren't a downgrade -they're a shortcut for a specific class of blocks that never needed JavaScript in the first place.
What Are the Limitations of PHP-Only Blocks?
Deliberate scope decisions, not bugs. Knowing them helps pick the right approach.
| Limitation | Impact | Status |
|---|---|---|
| No InnerBlocks / nested blocks | Can't build columns, accordions, sections | No timeline announced |
| Only 4 attribute types (string, integer, boolean, enum) | No media picker, rich text, date picker | DataForm being extended to PHP-only blocks in 7.1 |
| Block Bindings limited in 7.0 | Custom fields, post meta bindings need workarounds | ACF and Meta Box become native binding sources in 7.1 |
| Server round-trip on every attribute change | Preview feels less responsive than JS blocks | By design - the trade-off for zero JavaScript |
Custom style attribute conflicts with supports |
Must rename to skin, variant, etc. |
Reserved namespace, not a bug |
Frequently Asked Questions
Can PHP-only blocks contain InnerBlocks?
No. PHP-only blocks cannot contain nested blocks. For layouts like columns, accordions, or sections that wrap other blocks, use the traditional JavaScript block registration path.
Do PHP-only blocks work with the Interactivity API?
Not directly. The Interactivity API is designed for client-side state and reactivity, which PHP-only blocks don't provide. For blocks needing state (toggles, tabs, live filtering), JavaScript block registration is the correct choice.
Which attribute types get auto-generated controls?
Four types: string (text input), string with enum (dropdown), integer with minimum/maximum (number input), and boolean (toggle). Types like object and array do not generate controls.
Why is my custom style attribute causing errors?
WordPress reserves the style attribute name when block supports for color, spacing, or typography are enabled. Rename any custom attribute called style to something like skin, variant, or theme.
Does the editor preview update in real time?
No. Each attribute change triggers a REST API call to the PHP render_callback, then the editor swaps in the returned HTML. There's a small delay on every change - this is the trade-off for not writing JavaScript.
Can I use wp_enqueue_block_style() for PHP-only block CSS?
Not reliably. wp_enqueue_block_style() works for JavaScript-registered blocks but doesn't always fire for PHP-only blocks in the editor. Use wp_register_style() with the style and editor_style keys inside register_block_type() instead - the pattern shown in this guide.
What happens in the editor if the render callback throws an error?
The block shows "Error loading block" in the editor preview. Check wp-content/debug.log for the PHP error - the line number points directly to the issue.
Do PHP-only blocks support WordPress block themes and Full Site Editing?
Yes. PHP-only blocks work in block themes, templates, template parts, and the Site Editor. WordPress 7.1 enforces iframe mode for block themes - the examples in this guide use current APIs, so no changes are needed.
Wrapping Up
Building the WPVibes Blocks plugin has covered:
autoRegister => true is the entire "new API." Everything else is standard register_block_type().
The style and editor_style keys with wp_register_style() are the right way to attach CSS to a PHP-only block — cleaner and correctly loading in both editor and frontend.
get_block_wrapper_attributes() carries all block-supports styles into the render output. Always use it.
Auto-generated sidebar controls work well within their limits - string, integer, boolean, enum.
Server round-trip on every attribute change is the trade-off - keep render callbacks fast.
One file per block scales. Adding a new block later is one new file, one line in the loader.





Top comments (0)