title: 'Dynamic Blocks in WordPress 7.0+: render.php, Interactivity API, and HTML API' description: >- Architectural guide to developing dynamic and interactive blocks in WordPress 7.0+ (PHP 8.4+): render.php templates with get_block_wrapper_attributes(), reactive islands powered by @wordpress/interactivity (data-wp-*), streaming DOM mutations via WP_HTML_Tag_Processor, and fragment caching. module: gutenberg-blocks order: 3 difficulty: advanced estimatedTime: 42 min topics:
Static blocks excel for immutable editorial prose, but across real-world corporate and e-commerce platforms, interface components frequently depend on live server state: recent case study cards, faceted catalog filters, pricing calculators, personalized account widgets, or infrastructure telemetry. Furthermore, even for purely visual design system elements, storing generated HTML inside wp_posts.post_content introduces a severe maintenance liability: modifying a single CSS class across 5,000 pages forces either batch database rewrites or an ever-growing deprecated array.
Dynamic Blocks (Server-Rendered Blocks) eliminate both bottlenecks. In the database, the engine persists strictly a self-closing comment containing pure JSON attributes:
<!-- wp:agentic-lab/live-benchmarks {"category":"edge","limit":4} /-->
On every page request (or cache warming cycle), the WordPress 7.0+ core deserializes these attributes and injects them into an isolated render.php server template, automatically exposing three scoped variables: $attributes (the validated associative array mapped from block.json), $content (rendered HTML of nested InnerBlocks), and $block (the full WP_Block instance, including ancestor context $block->context).
Moreover, in WordPress 6.5–7.0+, dynamic blocks gained their missing frontend component—the native Interactivity API (@wordpress/interactivity). Instead of importing heavy React client runtimes or ad-hoc jQuery scripts, the server-side render.php decorates markup with declarative directives (data-wp-interactive, data-wp-context, data-wp-on--click, data-wp-bind--hidden), while a micro-sized reactive signals engine (~10 KB gzipped) instantly hydrates client interactivity in the browser.
Think of a modern dynamic block in WordPress 7.0+ as a React Server Component (RSC) coupled with Client-Side Reactive Signals:
render.php): runs fast indexed database queries or reads WP_Object_Cache, generates semantic HTML with system design tokens via get_block_wrapper_attributes(), and embeds initial state (wp_interactivity_state() / wp_interactivity_data_wp_context()).WP_HTML_Tag_Processor): when injecting ARIA accessibility attributes, classes, or loading="lazy" before output, the built-in streaming lexer modifies HTML in fractions of a millisecond without regex and without heavy DOMDocument overhead.viewScriptModule + @wordpress/interactivity): hydrates interactive toggles, tabs, metric switches, and filters without triggering full-page rerenders.[DB: wp_posts.post_content]
<!-- wp:agentic-lab/live-benchmarks {"env":"prod"} /-->
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ SERVER PIPELINE (PHP 8.4+ / render.php) │
│ 1. Check WP_Object_Cache (Fragment Cache Hit?) │
│ 2. Initialize State: wp_interactivity_state('agentic/benchmarks') │
│ 3. Generate Wrapper: get_block_wrapper_attributes() │
│ 4. Post-process tags via WP_HTML_Tag_Processor │
└────────────────────────┬─────────────────────────────────────────────┘
│ Dispatch SSR HTML + JSON State
▼
┌──────────────────────────────────────────────────────────────────────┐
│ CLIENT BROWSER (@wordpress/interactivity ES Module) │
│ • Reads data-wp-interactive="agentic/benchmarks" │
│ • Binds DOM nodes to reactive signals (data-wp-text, on-click) │
│ • Toggles metrics instantly without page reloads (0ms RTT) │
└──────────────────────────────────────────────────────────────────────┘The block parser extracts JSON attributes and merges them with default values from the block.json attributes schema.
The core includes the template declared in 'render', passing $attributes, $content, and $block (WP_Block) into local scope.
The server-side Interactivity API processor computes data-wp-bind and data-wp-class directives in PHP, eliminating Layout Shift (CLS = 0).
WP_HTML_Tag_Processor safely updates attributes in a streaming single pass without constructing an expensive memory DOM tree.
The browser loads viewScriptModule via Import Maps, mounting store('agentic/benchmarks') reactive handlers upon user interaction.
Across Fullstack Agentic Lab architectures, dynamic blocks built on render.php and the Interactivity API are employed for:
LCP < 0.8s), while visitors toggle infrastructure environments (Edge CDN / Origin PHP 8.4 / WASM) in one click via data-wp-on--click without page refreshes.render.php file in Git immediately cascades across 10,000 pages with zero Block Validation Errors.WP_HTML_Tag_Processor: automated injection of accessibility tags (aria-expanded, role), external link hardening (rel="noopener noreferrer"), and interactive directive bindings into nested block output.Comparing legacy blocks, early dynamic implementations via render_callback, and modern WordPress 7.0+ dynamic blocks:
Below is the production-ready reference artifact for WordPress 7.0+ (PHP 8.4+ and TypeScript 5.8), demonstrating the full lifecycle of an interactive dynamic block: a server-side render.php template with fragment caching, wp_interactivity_state() hydration, WP_HTML_Tag_Processor sanitization, and a reactive client view.ts module built on @wordpress/interactivity.
<?php
/**
* PART 1: Server Template build/blocks/live-telemetry/render.php (WordPress 7.0+ / PHP 8.4+)
*
* Variables exposed in the template's local scope:
* @var array<string, mixed> $attributes Validated block attributes from block.json.
* @var string $content Rendered HTML of child blocks (InnerBlocks).
* @var WP_Block $block Current block instance with access to $block->context.
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$clusterRegion = sanitize_key((string) ($attributes['defaultRegion'] ?? 'eu-central-1'));
$showDetails = (bool) ($attributes['showDetails'] ?? true);
// 1. Retrieve data from fast Object Cache (or compute and cache for 5 minutes)
$cacheKey = "agentic_telemetry_{$clusterRegion}";
$telemetry = wp_cache_get($cacheKey, 'agentic_blocks');
if (!is_array($telemetry)) {
$telemetry = [
'eu-central-1' => ['latencyMs' => 12, 'rps' => 6400, 'cacheHitRatio' => '99.4%'],
'us-east-1' => ['latencyMs' => 19, 'rps' => 8120, 'cacheHitRatio' => '98.9%'],
'ap-northeast' => ['latencyMs' => 27, 'rps' => 4300, 'cacheHitRatio' => '99.1%'],
];
wp_cache_set($cacheKey, $telemetry, 'agentic_blocks', 300);
}
// 2. Register global Interactivity API store state for SSR hydration
wp_interactivity_state('agentic-lab/live-telemetry', [
'regions' => $telemetry,
]);
// 3. Construct isolated local context for this specific block instance
$blockContext = [
'activeRegion' => $clusterRegion,
'isExpanded' => $showDetails,
];
// 4. Generate system wrapper attributes (colors, spacing, classes from supports)
$wrapperAttributes = get_block_wrapper_attributes([
'class' => 'agentic-telemetry-widget',
'data-wp-interactive' => 'agentic-lab/live-telemetry',
]);
// Capture initial HTML markup
ob_start();
?>
<div <?php echo $wrapperAttributes; ?> <?php echo wp_interactivity_data_wp_context($blockContext); ?>>
<div class="agentic-telemetry-widget__controls" role="tablist" aria-label="Datacenter selection">
<?php foreach (array_keys($telemetry) as $regionCode) : ?>
<button
type="button"
class="agentic-region-btn"
data-region="<?php echo esc_attr($regionCode); ?>"
data-wp-on--click="actions.selectRegion"
data-wp-class--is-active="callbacks.isRegionActive"
>
<?php echo esc_html(strtoupper($regionCode)); ?>
</button>
<?php endforeach; ?>
</div>
<div class="agentic-telemetry-widget__metrics" data-wp-bind--hidden="!context.isExpanded">
<div class="metric-box">
<span class="metric-label">P95 Latency:</span>
<strong class="metric-value" data-wp-text="callbacks.currentLatency">
<?php echo esc_html((string) $telemetry[$clusterRegion]['latencyMs'] . ' ms'); ?>
</strong>
</div>
<div class="metric-box">
<span class="metric-label">Throughput:</span>
<strong class="metric-value" data-wp-text="callbacks.currentRps">
<?php echo esc_html((string) $telemetry[$clusterRegion]['rps'] . ' req/s'); ?>
</strong>
</div>
<div class="metric-box">
<span class="metric-label">Edge Hit Ratio:</span>
<strong class="metric-value" data-wp-text="callbacks.currentHitRatio">
<?php echo esc_html((string) $telemetry[$clusterRegion]['cacheHitRatio']); ?>
</strong>
</div>
</div>
</div>
<?php
$rawHtml = (string) ob_get_clean();
// 5. WordPress HTML API (WP_HTML_Tag_Processor):
// Safely augment root node and controls with telemetry metadata with zero regex!
$processor = new WP_HTML_Tag_Processor($rawHtml);
if ($processor->next_tag(['class_name' => 'agentic-telemetry-widget'])) {
$processor->set_attribute('data-rendered-at', gmdate('c'));
$processor->set_attribute('data-post-id', (string) ($block->context['postId'] ?? 0));
}
echo $processor->get_updated_html();
/*
* ============================================================================
* PART 2: Client ES Module src/blocks/live-telemetry/view.ts (TypeScript 5.8)
* Registered in block.json via: "viewScriptModule": "file:./view.js"
* ============================================================================
*
* import { store, getContext, getElement } from '@wordpress/interactivity';
*
* interface TelemetryContext {
* activeRegion: string;
* isExpanded: boolean;
* }
*
* interface RegionMetric {
* latencyMs: number;
* rps: number;
* cacheHitRatio: string;
* }
*
* const { state } = store('agentic-lab/live-telemetry', {
* actions: {
* selectRegion() {
* const ctx = getContext<TelemetryContext>();
* const { attributes } = getElement();
* const targetRegion = attributes['data-region'];
* if (typeof targetRegion === 'string') {
* ctx.activeRegion = targetRegion;
* }
* },
* },
* callbacks: {
* isRegionActive(): boolean {
* const ctx = getContext<TelemetryContext>();
* const { attributes } = getElement();
* return ctx.activeRegion === attributes['data-region'];
* },
* currentLatency(): string {
* const ctx = getContext<TelemetryContext>();
* const metrics = state.regions[ctx.activeRegion] as RegionMetric | undefined;
* return metrics ? `${metrics.latencyMs} ms` : 'N/A';
* },
* currentRps(): string {
* const ctx = getContext<TelemetryContext>();
* const metrics = state.regions[ctx.activeRegion] as RegionMetric | undefined;
* return metrics ? `${metrics.rps} req/s` : 'N/A';
* },
* currentHitRatio(): string {
* const ctx = getContext<TelemetryContext>();
* const metrics = state.regions[ctx.activeRegion] as RegionMetric | undefined;
* return metrics ? metrics.cacheHitRatio : 'N/A';
* },
* },
* });
*/
Declare 'render': 'file:./render.php', 'viewScriptModule': 'file:./view.js', and enable 'supports': { 'interactivity': true }.
Inside registerBlockType(), specify save: () => null (or save: () => <InnerBlocks.Content /> if handling child blocks).
Output root tag with get_block_wrapper_attributes(), pass local context via wp_interactivity_data_wp_context(), and cache database queries.
Invoke store('namespace/block-name', { actions, callbacks }) from @wordpress/interactivity for instantaneous UI state updates.