Contents
Introduction — what dynamic blocks and render_callback mean
A WordPress dynamic block is a block whose frontend output is generated on the server at render time, instead of being stored as static HTML in post_content. In the block API, that server-side rendering is implemented with a PHP callback provided via the render_callback parameter when you register a block type. The render callback receives the block attributes (and optional other context) and must return the HTML string that will be saved in the post content output.
Why use render_callback (server-side rendering)
- Dynamic data: show latest posts, user-specific content, live API data, or content that depends on global state (current user, time, transient cache, etc.).
- Single source of truth: rendering logic lives in PHP so the output is consistent between editor preview (if server-side rendered) and frontend.
- Security and sanitization: you can centralize escaping/sanitizing in PHP.
- Internationalization: use translation functions in PHP for strings rendered on the server.
- Flexible caching: you can cache generated HTML and invalidate smartly on related hooks.
Anatomy of a server-rendered block
- Block metadata (block.json) and editor JS that defines the block attributes and edit UI.
- Editor script registers the block in the block editor (save returns null for server-side rendered blocks).
- Plugin PHP registers the block type and provides a render_callback that receives attributes and returns HTML.
- The final markup returned by render_callback is what appears on the frontend. In the editor, you can either use a live server-side preview (ServerSideRender component) or build an approximate preview in JS.
File and folder structure (example plugin)
Typical minimal plugin layout for a block that uses render_callback:
- my-dynamic-block/
- my-dynamic-block.php (main plugin file)
- block.json
- build/
- index.js (editor script built with wp-scripts)
- style.css (frontend styles)
- editor.css (editor styles)
- index.asset.php (generated by wp-scripts)
block.json example
{ apiVersion: 2, name: my-plugin/dynamic-latest-posts, title: Dynamic Latest Posts, category: widgets, icon: list-view, description: Show a configurable list of latest posts rendered server-side., supports: { html: false }, attributes: { postsToShow: { type: number, default: 5 }, showExcerpt: { type: boolean, default: true }, excerptLength: { type: number, default: 20 }, order: { type: string, default: desc }, categories: { type: array, items: { type: number }, default: [] } }, editorScript: file:./build/index.js, style: file:./build/style.css, editorStyle: file:./build/editor.css }
Editor JS (registerBlockType) — key points
For a server-rendered block you still register attributes and provide an edit UI. The save function should return null (or be omitted in block.json v2 with serverSideRender). In practical editor code, you either:
- Use the ServerSideRender component from @wordpress/server-side-render to show a true preview by requesting the render callback via REST for the editor preview.
- Or provide a client-side preview that approximates the output while not being exact.
Example editor index.js
import { registerBlockType } from @wordpress/blocks import { PanelBody, RangeControl, ToggleControl } from @wordpress/components import { InspectorControls } from @wordpress/block-editor import ServerSideRender from @wordpress/server-side-render registerBlockType( my-plugin/dynamic-latest-posts, { edit: ( props ) => { const { attributes, setAttributes } = props const { postsToShow, showExcerpt, excerptLength } = attributes return ( <>{/ Server-side preview in the editor using block attributes /} setAttributes( { postsToShow: value } ) } min={ 1 } max={ 20 } /> setAttributes( { showExcerpt: value } ) } /> > ) }, save: () => { // Server renders the output. Save returns null. return null }, } )
Plugin PHP — registration and render callback
There are two common registration patterns in PHP:
- register_block_type with a path to block.json and additional args (works on older WP installs).
- register_block_type_from_metadata (introduced in WP 5.5) or register_block_type with metadata — but using register_block_type and path to block.json is commonly used.
Minimal plugin file that registers the block and provides render_callback
my_dynamic_latest_posts_render ) ) } add_action( init, my_dynamic_block_register ) / Render callback for the block. @param array attributes The block attributes. @param string content The inner content (not used for blocks that save null). @param WP_Block block_instance Block instance with context data. @return string HTML output for the block. / function my_dynamic_latest_posts_render( attributes, content, block_instance ) { // Attributes with defaults as fallback. posts_to_show = isset( attributes[postsToShow] ) ? intval( attributes[postsToShow] ) : 5 show_excerpt = isset( attributes[showExcerpt] ) ? boolval( attributes[showExcerpt] ) : true excerpt_length = isset( attributes[excerptLength] ) ? intval( attributes[excerptLength] ) : 20 order = isset( attributes[order] ) ? sanitize_text_field( attributes[order] ) : desc category_ids = isset( attributes[categories] ) is_array( attributes[categories] ) ? array_map( intval, attributes[categories] ) : array() // Build query args. query_args = array( posts_per_page => posts_to_show, orderby => date, order => in_array( strtolower( order ), array( asc, desc ), true ) ? strtoupper( order ) : DESC, post_status => publish, no_found_rows => true, ) if ( ! empty( category_ids ) ) { query_args[category__in] = category_ids } // Simple transient caching to avoid expensive queries on every page view. transient_key = my_dyn_posts_ . md5( wp_json_encode( query_args ) ) cached_html = get_transient( transient_key ) if ( false !== cached_html ) { return cached_html } // Query posts. query = new WP_Query( query_args ) if ( ! query->have_posts() ) { return. esc_html__( No posts found., my-dynamic-block ) .
} // Start building markup. Return string (not echo). html =html .=// Set transient for e.g. 10 minutes. Invalidate on post save/delete via hooks (example below). set_transient( transient_key, html, 10 MINUTE_IN_SECONDS ) // Reset postdata. wp_reset_postdata() return html } / Invalidate cached transient(s) when posts are updated. This is a simple strategy: flush all transients with the prefix. In production you might use smarter invalidation. / function my_dynamic_block_invalidate_cache( post_id ) { global wpdb prefix = my_dyn_posts_ like = wpdb->esc_like( prefix ) . % // Find matching options in wp_options, this is MySQL-specific and may be heavy keep this approach in mind. options = wpdb->get_col( wpdb->prepare( SELECT option_name FROM wpdb->options WHERE option_name LIKE %s, _transient_ . like ) ) foreach ( options as option ) { // option format is _transient_{transient_key} transient_key = str_replace( _transient_, , option ) delete_transient( transient_key ) } } add_action( save_post, my_dynamic_block_invalidate_cache ) add_action( deleted_post, my_dynamic_block_invalidate_cache ) add_action( edit_post, my_dynamic_block_invalidate_cache )foreach ( query->posts as post ) { title = get_the_title( post ) permalink = get_permalink( post ) html .=
html .=- html .= . esc_html( title ) . if ( show_excerpt ) { raw_excerpt = get_the_excerpt( post ) // Truncate safely and escape. Use wp_kses_post if you allow HTML here we convert to plain text and escape. truncated = wp_strip_all_tags( wp_trim_words( raw_excerpt, excerpt_length, ... ) ) html .=
} html .=. esc_html( truncated ) .
} html .=
Key implementation details and best practices
- Return a string, don’t echo. The render_callback should return a string that becomes the block output on the frontend. If you echo, the editor or REST rendering may break.
-
Sanitize and escape. Treat attributes as untrusted. Use appropriate functions:
- Strings: sanitize_text_field() for plain text wp_kses_post() for HTML that you whitelist.
- URLs: esc_url() on output, esc_url_raw() on input if storing.
- Integers/booleans: cast with intval() and boolval() or use filter_var.
- On output use esc_html(), esc_attr(), or esc_url() as appropriate.
- Use translatable strings in PHP. When returning static text, wrap with __() / esc_html__() so translators can translate it.
- Block context and block parameter. The third parameter passed to the callback is a WP_Block instance (object) or an array depending on WP version. It can contain context attributes such as postId when using block context. Inspect it to get extra context, but avoid relying on fragile internals.
- Cache safely. If your render_callback is heavy, cache the returned HTML in a transient or object cache. Ensure you invalidate intelligently on post updates, taxonomy updates, or any related changes.
- Use ServerSideRender for editor previews. It uses the render_callback via REST to show an accurate preview. Note: that causes an HTTP request in the editor for each preview—keep render_callback performant.
- Prefer block.json metadata for registration. With block.json you can keep metadata and asset references centralized PHP can register the block with register_block_type( __DIR__ . /block.json, array( render_callback => … ) ).
Advanced topics and patterns
Rendering nested blocks / innerBlocks server-side
If your server-rendered block accepts inner blocks and you want to render them server-side, use render_block() or parse_blocks() to render stored nested blocks. Example:
// content is the inner blocks saved content (if present). inner_html = if ( ! empty( content ) ) { // parse and render so nested blocks are rendered properly blocks = parse_blocks( content ) foreach ( blocks as inner_block ) { inner_html .= render_block( inner_block ) } }
Providing dynamic data to the editor without live server rendering
- Use REST endpoints to fetch dynamic data (e.g., category list) for controls in the editor.
- Use wp.apiFetch or custom endpoints in the block edit() function to populate controls asynchronously.
- If you need the exact server output in-editor, use ServerSideRender otherwise show approximate preview to keep editor responsive.
Contextual rendering (per-page/per-post customization)
Render callbacks can look at global state like get_the_ID() or provided context keys. If a block needs to behave differently in the editor preview vs frontend (or in different contexts), check is_admin(), REST request type, or block context keys. When using context keys in block.json, WordPress will provide them into the block instance in the render callback.
Common pitfalls
- Forgetting to return instead of echoing — breaks REST editor preview.
- Making render_callback heavy and slowing the editor/REST responses — cache or optimize queries.
- Not properly sanitizing attributes — XSS risk.
- Using functions that rely on the global post without ensuring global state use get_post() or set up post data carefully in server environments.
- Not invalidating caches when related content changes.
Examples: several focused render_callback patterns
1) Simple dynamic greeting based on current user
function my_dynamic_greeting_render( attributes ) { if ( is_user_logged_in() ) { user = wp_get_current_user() name = esc_html( user->display_name ) return. sprintf( esc_html__( Hello, %s!, my-dynamic-block ), name ) .
} return. esc_html__( Hello, visitor! Please log in., my-dynamic-block ) .
}
2) Latest posts with transient caching (already shown above)
See the long example in the plugin PHP section for a robust pattern with caching and sanitization.
3) Using render callback to include dynamic markup from template parts
function my_template_part_block_render( attributes ) { ob_start() // Make attributes safe and extract only what you need. context = array( title => isset( attributes[title] ) ? sanitize_text_field( attributes[title] ) : , ) // Load a PHP template part that outputs markup. Template should escape output itself. locate_template( array( template-parts/blocks/my-block.php ), true, false ) return ob_get_clean() }
Testing and debugging tips
- Use error_log() or WP_DEBUG_LOG for server-side debugging. Avoid printing debug messages that will break the HTML output returned by render_callback.
- Test the block in the editor with ServerSideRender and verify REST responses in the browser devtools network tab (route: /wp/v2/block-renderer/
). - Switch on SCRIPT_DEBUG in wp-config.php to ensure you are not caching older JS while testing editor code.
- Use unit tests for heavy rendering logic where appropriate (PHP unit tests for helper functions used by render_callback).
Helpful references
- WordPress Block Editor Handbook
- register_block_type()
- save_post (cache invalidation hook)
- ServerSideRender component
Summary / checklist
- Define block.json with attributes and metadata.
- Implement editor script use ServerSideRender if you want live server preview.
- Register the block in PHP and provide a render_callback.
- In render_callback, validate attributes, run queries or logic, assemble and return escaped HTML.
- Use caching for heavy operations, and invalidate caches on related content changes.
- Sanitize input, escape output, and use internationalization functions for strings.
Final note
Follow WordPress coding standards for readability and security. Server-side rendering with render_callback is a powerful tool that, when implemented carefully with sanitization and caching, can deliver flexible, dynamic content while keeping editor and front-end output consistent.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |