WordPress 6.9 continues the Gutenberg Phase 3 roadmap with collaborative editing foundations, expanded block-locking controls, and a redesigned template part focus mode. For plugin and theme developers the most impactful changes are: the new wp_block_supports() API for registering custom block support flags, improved WP_HTML_Tag_Processor with bookmark support, and interoperability improvements to the Interactivity API store.
Problem: A WordPress site is running 6.8 and the release notes for 6.9 list broad improvements — it is unclear which changes require plugin or theme code updates versus which are automatic engine improvements.
Solution: WordPress 6.9 focuses on the Gutenberg merge phase: improved block locking UI, a stabilised Interactivity API store API, and the wp_admin_notice() function becoming the standard for admin notices. Review any plugin that uses admin_notices directly and migrate to wp_admin_notice(). Test block themes for compatibility with updated block templates shipped in core.
The code below demonstrates the new block support registration API, the bookmark API on WP_HTML_Tag_Processor for multi-pass HTML mutation, and the updated Interactivity API getServerState() helper introduced in 6.9.
<?php
// ── 1. wp_block_supports() — register custom block support flag ───────────
// Allows blocks to declare support for a custom feature that your plugin provides
add_action( 'init', function () {
wp_register_block_support(
'my-plugin/lazy-load',
[
'label' => __( 'Lazy Load', 'my-plugin' ),
'apply_to_blocks' => [ 'core/image', 'core/cover', 'my-plugin/hero' ],
]
);
} );
// Read the support flag in render_callback or a block filter:
add_filter( 'render_block', function ( string $block_content, array $block ): string {
$supports_lazy = wp_get_block_support( $block['blockName'], 'my-plugin/lazy-load' );
if ( $supports_lazy && isset( $block['attrs']['myPluginLazyLoad'] ) && $block['attrs']['myPluginLazyLoad'] ) {
// Add loading="lazy" to any img tags in the block output
$block_content = str_replace( '<img ', '<img loading="lazy" ', $block_content );
}
return $block_content;
}, 10, 2 );
// ── 2. WP_HTML_Tag_Processor: bookmarks for multi-pass mutation ───────────
$html = '<div class="card"><img src="a.jpg"><p>Hello</p></div>';
$p = new WP_HTML_Tag_Processor( $html );
// First pass: find the img and set a bookmark
while ( $p->next_tag( 'img' ) ) {
$p->set_bookmark( 'first-img' );
$p->set_attribute( 'alt', 'Card image' );
break;
}
// Second pass: jump back to the bookmarked position
$p->seek( 'first-img' );
$p->set_attribute( 'loading', 'lazy' );
echo $p->get_updated_html();
// <div class="card"><img src="a.jpg" alt="Card image" loading="lazy"><p>Hello</p></div>
// ── 3. Interactivity API: getServerState() (WordPress 6.9) ────────────
// PHP: pass server-rendered state flag
wp_interactivity_state( 'my-namespace', [
'isLoggedIn' => is_user_logged_in(),
'postId' => get_the_ID(),
] );
// view.js — Interactivity API: access server state
import { store, getServerState } from '@wordpress/interactivity';
store( 'my-namespace', {
state: {
// getServerState() returns the PHP-provided initial state (read-only)
get isLoggedIn() {
return getServerState().isLoggedIn;
},
},
actions: {
handleClick() {
const { isLoggedIn } = getServerState();
if ( ! isLoggedIn ) {
window.location.href = '/wp-login.php';
}
},
},
} );
NOTE: WP_HTML_Tag_Processor bookmarks are invalidated if any mutation changes the byte offset before the bookmark position — always set bookmarks in a read-only first pass, then apply mutations in reverse document order (last bookmark first) or use a fresh processor instance for each mutation round to avoid stale bookmark offsets.