Skip to main content
Adzbyte
TutorialsWordPress

Build a PHP-Only Dynamic Block with block.json

Adrian Saycon
Adrian Saycon
September 17, 20264 min read
Build a PHP-Only Dynamic Block with block.json

A PHP-only dynamic block is useful when the editor UI can rely on standard block supports and the front-end output must stay current. WordPress can auto-register that block in the editor without a custom JavaScript bundle. You provide metadata in block.json, register it on the server, and return escaped markup from a PHP render callback. This tutorial builds a “Latest Update” block that displays the most recently modified post and inherits spacing, color, and typography controls from WordPress. It also avoids shipping an editor bundle that the block does not need.

Choose dynamic rendering for changing data

A static block stores its final markup in post content. A dynamic block stores its attributes but generates markup on each render. That is a good fit for a latest-post query because the answer changes without editing the page. It also keeps one rendering implementation in PHP.

Dynamic rendering is not automatically better. Static output is cheaper and more resilient when content does not change independently. Start by asking whether saved markup would become stale or whether server context is necessary.

Create the block metadata

Place block.json beside the plugin file for this minimal example. Use API version 3, define the attribute schema, point render to a PHP template, and enable automatic registration.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "adz/latest-update",
  "title": "Latest Update",
  "category": "widgets",
  "description": "Displays the most recently modified post.",
  "textdomain": "adz-latest-update",
  "attributes": {
    "showDate": { "type": "boolean", "default": true }
  },
  "supports": {
    "autoRegister": true,
    "align": [ "wide", "full" ],
    "color": { "background": true, "text": true },
    "spacing": { "padding": true },
    "typography": { "fontSize": true },
    "html": false
  },
  "render": "file:./render.php"
}

The namespace and name become part of saved content. Choose them carefully because renaming a published block requires a migration.

Register from metadata on init

Server registration lets WordPress discover supports, assets, render behavior, and REST metadata consistently.

add_action( 'init', function () {
    register_block_type( __DIR__ );
} );

If the plugin later contains several compiled blocks, use a build directory and metadata collection rather than scanning arbitrary paths on every request. For a single unbundled block, the direct path is clear and sufficient.

Render with a bounded query

The render.php template receives $attributes, $content, and $block. Query only the fields required, ignore sticky ordering, and request one published post.

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

$query = new WP_Query( array(
    'post_type'              => 'post',
    'post_status'            => 'publish',
    'posts_per_page'         => 1,
    'orderby'                => 'modified',
    'order'                  => 'DESC',
    'ignore_sticky_posts'    => true,
    'no_found_rows'          => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
) );

if ( ! $query->have_posts() ) {
    return;
}

$post = $query->posts[0];
$url  = get_permalink( $post );
?>

The performance flags avoid pagination and caches this block does not use. Bounded queries matter because a block may appear several times in a template.

Use block wrapper attributes and contextual escaping

get_block_wrapper_attributes() applies generated classes and styles for supports. Escape each dynamic value for where it appears.

<article <?php echo get_block_wrapper_attributes(); ?>>
    <p class="adz-latest-update__eyebrow">
        <?php esc_html_e( 'Latest update', 'adz-latest-update' ); ?>
    </p>
    <h2>
        <a href="<?php echo esc_url( $url ); ?>">
            <?php echo esc_html( get_the_title( $post ) ); ?>
        </a>
    </h2>
    <?php if ( ! empty( $attributes['showDate'] ) ) : ?>
        <time datetime="<?php echo esc_attr( get_post_modified_time( DATE_W3C, false, $post ) ); ?>">
            <?php echo esc_html( get_the_modified_date( '', $post ) ); ?>
        </time>
    <?php endif; ?>
</article>

Core produces the wrapper attributes, so output that helper directly as documented. Values inside the wrapper remain your responsibility.

Add caching only when measurement justifies it

Full-page caches may already absorb this query. If profiling shows repeated uncached rendering is material, cache the post ID with a short transient and delete it when a post is published or updated. Cache identifiers rather than complete markup so translations, block styles, and URLs are generated in the current context.

Do not introduce invalidation complexity before observing a problem. A stale “latest” block undermines its promise more than a tiny query helps performance.

Verify editor and front-end behavior

  • Insert the block and confirm it appears without a custom JavaScript file.
  • Test background, text, padding, font-size, wide, and full alignment supports.
  • Toggle showDate through copied block markup or future attribute controls and confirm the schema is respected.
  • Modify a different published post and confirm output changes without resaving the page.
  • Test zero published posts and titles containing HTML-sensitive characters.
  • View the block in the editor, front end, template, and REST-rendered contexts used by the site.

Internationalization still applies even without a JavaScript bundle. Give the metadata a text domain, wrap PHP strings in translation functions, and generate translation files as part of the release. If attributes store editor-authored content, do not translate those values at render time; translate interface labels and plugin-owned defaults. Test a non-English locale because longer labels and different date formats often reveal layout assumptions that the default locale hides.

Keep the block contract small

PHP-only auto-registration removes build tooling, not engineering discipline. Stable metadata, bounded queries, escaped output, native supports, and clear empty states still define the quality of the block. WordPress’s current block registration guide documents PHP-only blocks and block metadata lists the supported properties. Use those APIs to keep the implementation close to core instead of rebuilding editor controls yourself.

Photo by Negative Space on Pexels.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.

Latest Articles

From the Blog

View all articles