Contents
Overview: Server-rendered blocks with render_callback in WordPress
This article is a comprehensive, step-by-step guide to creating a Gutenberg block that is rendered on the server using a PHP render_callback. It covers block metadata, editor JavaScript, registering the block in PHP, secure output, passing attributes, rendering inner blocks, caching strategies, and best practices for performance and security. Example code blocks are included and are ready to drop into a plugin or theme.
Why use server-side rendering?
- Dynamic content: The block output depends on current server state (post meta, custom queries, user state, dates, transient values).
- Centralized rendering logic: Complex HTML generation and escaping is easier and safer in PHP.
- SEO caching: HTML is available on first load CDNs and page caches can cache final HTML.
- Consistent output on front and editor: The editor can preview server output (via ServerSideRender) and the front always uses the same renderer.
High-level workflow
- Create block metadata (block.json) or register your block using register_block_type.
- Create an editor script that registers the block type in JavaScript and sets save() to return null.
- Register the block server-side in PHP and provide a render_callback function that outputs the final HTML.
- Secure and sanitize all attributes and output with esc_ and wp_kses_ functions.
- Optionally implement caching (transients/object cache) and invalidation logic.
Minimum example structure
A simple plugin folder layout:
- my-plugin-block/
- block.json
- build/ (editor script and asset manifest)
- src/editor.js
- render.php (optional or register via PHP)
- my-plugin-block.php (main plugin bootstrap)
block.json (API v2) example
This block.json registers editor script and marks the block as server-rendered by making save return null in JS. Optionally you can reference a PHP render file using render: file:./render.php and use register_block_type_from_metadata() in PHP.
{ apiVersion: 2, name: my-plugin/server-rendered-block, title: Server-rendered Block, category: widgets, icon: admin-site, description: An example server-rendered block using a PHP render_callback., attributes: { title: { type: string, default: Hello from PHP }, postId: { type: integer, default: 0 } }, supports: { html: false }, editorScript: file:./build/index.js, style: file:./build/style.css }
Editor JavaScript: register the block and make save return null
In the editor, blocks that use server rendering should have a save function that returns null. You can also present a live preview in the editor using the ServerSideRender component from @wordpress/server-side-render or fetch rendered HTML via a REST call.
/ src/editor.js / import { registerBlockType } from @wordpress/blocks import { TextControl } from @wordpress/components import { useState } from @wordpress/element import ServerSideRender from @wordpress/server-side-render registerBlockType( my-plugin/server-rendered-block, { edit: ( props ) => { const { attributes, setAttributes } = props const { title, postId } = attributes return () }, // Save returns null — block is rendered dynamically on the server. save: () => null, } )setAttributes( { title: val } ) } />
Enqueueing and registering the editor script
Use the build output and the asset manifest (if created by @wordpress/scripts) to enqueue editor script and styles. The PHP registration will also register the block type.
// my-plugin-block.php (main plugin file)
Registering with explicit render_callback in PHP
If you prefer to register the block programmatically and pass a render_callback, use register_block_type and include a callable for render_callback. This example uses attributes passed from the editor.
my-plugin-block-editor-script, // must be previously registered render_callback => my_plugin_render_server_block, attributes => array( title => array( type => string, default => Hello from PHP ), postId => array( type => number, default => 0 ), ), ) ) } add_action( init, my_plugin_register_server_block ) function my_plugin_render_server_block( attributes, content, block ) { // attributes: attributes as sent from the editor // content: innerBlocks saved content (if any) // block: full block object, includes context and clientId title = isset( attributes[title] ) ? sanitize_text_field( attributes[title] ) : post_id = isset( attributes[postId] ) ? absint( attributes[postId] ) : get_the_ID() // Build HTML safely using escaping html =html .=return html } ?>. esc_html( title ) .
if ( post_id ) { post = get_post( post_id ) if ( post ) { html .=html .= wp_kses_post( wpautop( post->post_content ) ) html .=} } html .=
Alternative: using block metadata with render file
If your block.json contains a render: file:./render.php (or you include a render.php in the block dir) and you call register_block_type_from_metadata() (or register_block_type(__DIR__)), WordPress will look for that render file. The render file should return the final HTML or callable.
. esc_html( title ) .