Block Bindings API
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
A block binding replaces the value of a block attribute with a value computed elsewhere: a custom field, the post’s
permalink, a term’s name, or anything a plugin can look up. The block stays an ordinary core Paragraph, Heading, Image
or Button, but its text or URL comes from the source when the page is rendered. Bindings are how a block theme shows
custom fields without a custom block, and how synced patterns let each instance override some content. This page is for theme and block developers: it binds core blocks to the
Lumen Press book meta, uses the four core sources, and builds the custom lumen/book-data source of the lumen-catalog
plugin on the server and in the editor.
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), PHP 7.4 minimum and 8.3+ recommended, @wordpress/scripts 37.0.0 and WP-CLI 2.12.0, and
checked on 2026-10-28. The HTML below comes from do_blocks()
and front-end requests on a 7.1.3 site running lumen-catalog, and the editor script from a real wp-scripts build.
WordPress 7.2, in beta and due in December 2026, adds one supported attribute so far.
What a binding is
Before bindings, a custom field reached a page through a shortcode, a PHP template (classic themes only) or a custom block. The Block Bindings API, added in WordPress 6.5, connects an attribute of a supported block to a source:
-
The binding is data in the block’s comment delimiter, under
metadata.bindings. No JavaScript runs on the front end. -
The source is a named PHP callback (
register_block_bindings_source());WP_Blockcalls it while rendering. -
The editor shows the same value through a JavaScript half of the source (
registerBlockBindingsSource(), 6.7), which can also write edits back.
Core ships four sources: core/post-meta, core/pattern-overrides, core/post-data and core/term-data. Themes and
plugins add their own: on the Lumen site, wp eval 'print_r( array_keys( get_all_registered_block_bindings_sources() ) );'
also lists lumen/book-data from lumen-catalog and twentytwentyfive/format (the post format name) from Twenty
Twenty-Five.
Anatomy of a binding
The book facts group below belongs in the single-lp_book.html template of the lumen theme (see
Block Theme Templates and Template Parts), and works
the same in a pattern or in the book’s content. Each bound block keeps its normal markup, whose text is the
fallback shown when the source returns nothing:
<!-- wp:group {"className":"lumen-book-facts","layout":{"type":"constrained"}} -->
<div class="wp-block-group lumen-book-facts">
<!-- wp:image {"sizeSlug":"medium","metadata":{"bindings":{"url":{"source":"lumen/book-data","args":{"field":"cover"}}}}} -->
<figure class="wp-block-image size-medium"><img alt=""/></figure>
<!-- /wp:image -->
<!-- wp:heading {"level":3,"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"lp_isbn"}}}}} -->
<h3 class="wp-block-heading">ISBN not assigned yet</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"lumen/book-data","args":{"field":"pages"}}}}} -->
<p>Page count</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/post-data","args":{"field":"link"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">Share this book</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
</div>
<!-- /wp:group -->
Source: Block Editor Handbook: Bindings
Every block has a metadata attribute; its bindings map attribute names to a source and args (any JSON the
source understands: key for post meta, field for the others). On "The Lighthouse Ledger" the group renders as:
<div class="wp-block-group lumen-book-facts has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<figure class="wp-block-image size-medium"><img decoding="async" src="https://lumenpress.test/wp-content/uploads/2026/10/lighthouse-ledger-200x300.png" alt=""/></figure>
<h3 class="wp-block-heading">978-0-00-000001-9</h3>
<p class="wp-block-paragraph">312 pages</p>
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a href="https://lumenpress.test/books/the-lighthouse-ledger/" class="wp-block-button__link wp-element-button">Share this book</a></div>
</div>
</div>
The stored markup keeps the fallbacks; values are computed on every render, so a changed ISBN shows everywhere at once.
How a binding is resolved
WP_Block::render() resolves bindings first (process_block_bindings()), then writes the values with the HTML API:
-
nullkeeps the fallback; any other value, even an empty string, replaces it. The Image above disappears for a book without a cover, because the Image block renders nothing without asrc. -
Context comes from the source. WordPress copies the source’s
uses_contextkeys (postId,postType;termId,taxonomy) into the bound block, although a Paragraph’sblock.jsondoes not ask for them. The post is the main query’s on a single page, the current item in a Query Loop. -
Unsupported attributes and unknown sources are skipped silently, and the fallback stays.
-
Replacement follows the attribute’s
sourceinblock.json. Rich-text and HTML attributes replace the inner HTML of theselectorelement (since 7.1 without touching nested inner blocks, which made List Item bindable);attributesources set one HTML attribute; an attribute with no source (typical of dynamic blocks) only reaches therender_callback.
Supported blocks and attributes
get_block_bindings_supported_attributes() (6.9) returns, for a block type, the attributes that accept bindings. In
7.1.3 the core list is:
| Block | Attributes | Since |
|---|---|---|
|
|
6.5 |
|
|
6.5 |
|
|
6.5; 6.9 |
|
|
6.5 |
|
|
6.9 |
|
|
6.9 |
|
|
7.1 |
The 7.2 betas add core/icon with its icon attribute; until 7.2 ships that is upcoming. Two filters (both 6.9) change
the list: block_bindings_supported_attributes receives the attributes and the block type name for every block, and
the dynamic block_bindings_supported_attributes_{$block_type} receives only the attributes of one block type. The
result is used for rendering, for pattern overrides (a __default binding expands to it), and in the editor, which gets
it as the __experimentalBlockBindingsSupportedAttributes editor setting.
The filter is how a plugin makes its own block bindable. lumen/latest-books is a dynamic block whose genre
attribute limits the list to one genre (see Static and Dynamic Blocks).
Declaring genre bindable (the latest_books_attributes() method of the Bindings class below) lets the book
template show "more books in this genre" without any new block code:
<!-- wp:lumen/latest-books {"count":3,"showDate":false,"metadata":{"bindings":{"genre":{"source":"lumen/book-data","args":{"field":"genre"}}}}} /-->
Source: Block Editor Handbook: Bindings
On "The Lighthouse Ledger" (genre mystery) the block lists the other two mystery books; with the filter removed the
binding is ignored and "Paper Moons", a literary fiction title, appears too. A static block’s attribute is bindable
only when its source is rich-text, html or attribute with a plain tag name as selector: the HTML replacement
does not support CSS selectors yet.
The core sources
core/post-meta
Binds to a meta value of the post in context, with args.key. The callback returns null (fallback kept) unless all
of these hold:
-
the post is publicly viewable, or the current user can
read_postit, and it is not password-protected; -
the key is not protected:
is_protected_meta()isfalse, so keys starting with_are never bound; -
the key is registered for the post type (or for all post types) with
show_in_rest.
The Lumen book meta (lp_isbn, lp_pages, lp_published_on) is registered with show_in_rest, a label and a
default in Metadata and Custom Fields, so it qualifies. The
stock-sync timestamp _lp_stock_synced_at is protected and does not, even though it is in REST output:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"lp_pages"}}}}} -->
<p>Pages</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"_lp_stock_synced_at"}}}}} -->
<p>Stock date not public</p>
<!-- /wp:paragraph -->
The first renders 312, the raw value, not "312 pages"; the second keeps "Stock date not public". Formatting and
translation belong in a custom source, which is why lumen/book-data exists. The Attributes panel only offers fields
whose type matches the attribute, so the integer lp_pages is not listed for a Paragraph’s string content, although
a binding typed in the code editor renders.
core/post-data
Added in 6.9, with args.field set to date (ISO 8601), modified (only when later than the publication date, else
an empty string) or link (the permalink). The Share button above uses link. Since 6.9 the Post
Date block has a datetime attribute that it binds to core/post-data itself, and the Navigation Link and Submenu
blocks bind url to post or term data using their own id attribute instead of context, so a menu item follows a page
whose slug changes. The Attributes panel is not shown for these three blocks.
core/term-data
Added in 6.9, with args.field set to id, name, slug, description (filtered with wp_kses_post()), parent,
count or link. It needs termId and taxonomy in context, which the Term Template block inside a Terms Query
block (also 6.9) provides for each term. A genre index for the book archive:
<!-- wp:terms-query {"termQuery":{"perPage":10,"taxonomy":"lp_genre","order":"asc","orderBy":"name","include":[],"hideEmpty":true,"showNested":false,"inherit":false}} -->
<div class="wp-block-terms-query"><!-- wp:term-template -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/term-data","args":{"field":"name"}}}}} -->
<p>Genre name</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/term-data","args":{"field":"link"}},"text":{"source":"core/term-data","args":{"field":"count"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">0</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- /wp:term-template --></div>
<!-- /wp:terms-query -->
On the book archive this renders one list item per genre, such as "Mystery" with a button reading 3 linked to
https://lumenpress.test/books/genre/mystery/. For a taxonomy that is not
publicly queryable the source returns values only to logged-in users with read.
core/pattern-overrides
Synced patterns (wp_block posts, inserted as core/block) are identical everywhere they are used. Overrides (6.6;
the source is registered since 6.5) let each instance change selected blocks. A block in the pattern opts in with a
metadata.name and a __default binding to core/pattern-overrides, which WordPress expands to every supported
attribute of that block; the instance stores its values in the content attribute of core/block, keyed by name:
<!-- wp:block {"ref":9,"content":{"Book title":{"content":"The Glass Orchard"},"Blurb":{"content":"An orchard that remembers every visitor."}}} /-->
The source reads the pattern/overrides context that core/block provides and returns the value stored for the
block’s name and attribute, or null so the pattern’s own text shows. Renaming a block in the pattern orphans the
values stored under the old name. Overridable patterns are built in
Patterns for Site Builders and Theme Patterns.
A custom source in PHP
register_block_bindings_source( $source_name, $source_properties ) registers a source on init. 7.1.3 accepts
exactly three properties: label (required, shown in the editor), get_value_callback (required; receives
$source_args, the WP_Block instance and the attribute name, and returns the value or null) and uses_context
(optional, the context keys the callback reads).
lumen/book-data turns the book’s data into display-ready strings: the ISBN, a translated page count, the first
genre’s slug and the cover URL. The class lives in src/Bindings.php of
lumen-catalog, is autoloaded by Composer, and Plugin::boot() calls Bindings::boot(); next to Blocks::boot();:
<?php
/**
* The lumen/book-data block bindings source.
*
* @package Lumen\Catalog
*/
namespace Lumen\Catalog;
defined( 'ABSPATH' ) || exit;
/**
* Exposes formatted book data to core blocks through the Block Bindings API.
*/
final class Bindings {
const SOURCE = 'lumen/book-data';
// Called from Plugin::boot().
public static function boot(): void {
add_action( 'init', array( self::class, 'register_source' ) );
add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_source' ) );
add_filter( 'block_bindings_supported_attributes_lumen/latest-books', array( self::class, 'latest_books_attributes' ) );
add_filter( 'block_bindings_source_value', array( self::class, 'keep_fallback_for_empty_meta' ), 10, 5 );
}
// The editor receives the name, label and context of a server source automatically.
public static function register_source(): void {
register_block_bindings_source(
self::SOURCE,
array(
'label' => __( 'Book data', 'lumen-catalog' ),
'get_value_callback' => array( self::class, 'get_value' ),
'uses_context' => array( 'postId', 'postType' ),
)
);
}
// Returns one book field for $source_args['field'], or null to keep the block's own content.
public static function get_value( array $source_args, $block_instance, string $attribute_name ) {
$post_id = (int) ( $block_instance->context['postId'] ?? 0 );
$post = $post_id ? get_post( $post_id ) : null;
if ( ! $post || 'lp_book' !== $post->post_type ) {
return null;
}
// Same visibility rule as the core sources: no data from drafts or protected posts for visitors.
if ( ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post ) ) {
return null;
}
switch ( $source_args['field'] ?? '' ) {
case 'isbn':
$isbn = (string) get_post_meta( $post_id, 'lp_isbn', true );
return '' === $isbn ? null : $isbn;
case 'pages':
$pages = (int) get_post_meta( $post_id, 'lp_pages', true );
/* translators: %d: number of pages. */
return $pages ? sprintf( _n( '%d page', '%d pages', $pages, 'lumen-catalog' ), $pages ) : null;
case 'genre':
$terms = get_the_terms( $post_id, 'lp_genre' );
return is_array( $terms ) ? $terms[0]->slug : null;
case 'cover':
$url = get_the_post_thumbnail_url( $post_id, 'medium' );
return $url ? esc_url_raw( $url ) : null;
}
return null;
}
// Lets the dynamic lumen/latest-books block bind its "genre" attribute (WordPress 6.9+).
public static function latest_books_attributes( array $attributes ): array {
$attributes[] = 'genre';
return $attributes;
}
// core/post-meta returns '' for an unset book field, which would blank the block; null keeps its own text.
public static function keep_fallback_for_empty_meta( $value, $name, $source_args, $block_instance, $attribute_name ) {
$is_book_meta = 'core/post-meta' === $name && 0 === strpos( $source_args['key'] ?? '', 'lp_' );
return ( $is_book_meta && 'content' === $attribute_name && '' === $value ) ? null : $value;
}
// Loads the editor half of the source, built by wp-scripts into build/editor/.
public static function enqueue_editor_source(): void {
$asset = require dirname( LUMEN_CATALOG_FILE ) . '/build/editor/book-data.asset.php';
wp_enqueue_script(
'lumen-catalog-book-data',
plugins_url( 'build/editor/book-data.js', LUMEN_CATALOG_FILE ),
$asset['dependencies'],
$asset['version'],
true
);
wp_set_script_translations( 'lumen-catalog-book-data', 'lumen-catalog' );
}
}
Points worth noting in the class:
-
Visibility is the source’s job. The check copied from
core/post-metakeeps a draft’s data off public pages: a visitor sees the fallbacks,maria.editorthe values. -
block_bindings_source_value(6.7) filters the value of any source.core/post-metareturns the registered default''for a book without an ISBN, and an empty string replaces the fallback: withoutkeep_fallback_for_empty_meta()the heading renders as an empty<h3>on "The Glass Orchard"; with it, "ISBN not assigned yet". -
Registration errors are
_doing_it_wrong()notices, and the function returnsfalse: names with uppercase letters or no namespace, a namealready registered, a missinglabel, orinvalid properties(any other key). -
unregister_block_bindings_source(),get_block_bindings_source()andget_all_registered_block_bindings_sources()complete the PHP API (all 6.5).
The source in the editor
The editor must show, and possibly edit, bound values from unsaved data. Since 6.7 every server source is
registered in the editor automatically (name, label, usesContext), so a bound block shows the source label. Real
values need a JavaScript half, registered with registerBlockBindingsSource() from @wordpress/blocks:
| Property | Since | Meaning |
|---|---|---|
|
6.7 |
The same name as on the server. |
|
6.7 |
Optional when the server registered one; a different label overrides it with a console warning. |
|
6.7 |
Extra editor context, merged with the server’s |
|
6.7 |
Returns |
|
6.7 |
Called when the user edits a bound value; each binding carries |
|
6.7 |
Whether the bound value is editable in place. Ignored unless |
|
6.9 |
Fields offered in the Attributes panel: objects with |
src/editor/book-data.js mirrors Bindings::get_value() with the editor’s data stores. It reads the edited book
record, so the page count updates as the user types in a meta field, and it lets users edit the raw ISBN in place:
/**
* Editor half of the lumen/book-data block bindings source.
*/
import { registerBlockBindingsSource } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';
import { __, _n, sprintf } from '@wordpress/i18n';
const FIELDS = [
{ label: __( 'ISBN-13', 'lumen-catalog' ), type: 'string', args: { field: 'isbn' } },
{ label: __( 'Page count', 'lumen-catalog' ), type: 'string', args: { field: 'pages' } },
{ label: __( 'Genre slug', 'lumen-catalog' ), type: 'string', args: { field: 'genre' } },
{ label: __( 'Cover URL', 'lumen-catalog' ), type: 'string', args: { field: 'cover' } },
];
const isBook = ( context ) => context?.postType === 'lp_book' && !! context?.postId;
// Mirrors Bindings::get_value() in PHP, reading the unsaved edits of the book.
function readField( select, context, field ) {
const data = select( coreDataStore );
const book = data.getEditedEntityRecord( 'postType', 'lp_book', context.postId );
const meta = book?.meta ?? {};
switch ( field ) {
case 'isbn':
return meta.lp_isbn || null;
case 'pages':
return meta.lp_pages
? sprintf(
/* translators: %d: number of pages. */
_n( '%d page', '%d pages', meta.lp_pages, 'lumen-catalog' ),
meta.lp_pages
)
: null;
case 'genre': {
const termId = book?.lp_genre?.[ 0 ];
return termId ? data.getEntityRecord( 'taxonomy', 'lp_genre', termId )?.slug ?? null : null;
}
case 'cover': {
const media = book?.featured_media && data.getEntityRecord( 'postType', 'attachment', book.featured_media );
return media?.media_details?.sizes?.medium?.source_url ?? media?.source_url ?? null;
}
}
return null;
}
registerBlockBindingsSource( {
name: 'lumen/book-data',
label: __( 'Book data', 'lumen-catalog' ),
getValues( { select, context, bindings } ) {
const values = {};
for ( const [ attribute, { args } ] of Object.entries( bindings ) ) {
const field = FIELDS.find( ( item ) => item.args.field === args?.field );
const value = isBook( context ) ? readField( select, context, args?.field ) : null;
// Outside a book (a template, a pattern preview) show the field's label as a placeholder.
values[ attribute ] = value ?? field?.label ?? args?.field;
}
return values;
},
setValues( { dispatch, context, bindings } ) {
const meta = {};
for ( const { args, newValue } of Object.values( bindings ) ) {
if ( args?.field === 'isbn' ) {
meta.lp_isbn = newValue;
}
}
dispatch( coreDataStore ).editEntityRecord( 'postType', context.postType, context.postId, { meta } );
},
// Only the raw ISBN is editable, only on the book itself, never inside a Query Loop.
canUserEditValue( { select, context, args } ) {
if ( args?.field !== 'isbn' || ! isBook( context ) || context?.query || context?.queryId ) {
return false;
}
return !! select( coreDataStore ).canUser( 'update', {
kind: 'postType',
name: 'lp_book',
id: context.postId,
} );
},
getFieldsList( { context } ) {
return context?.postType === 'lp_book' ? FIELDS : [];
},
} );
After wp-scripts format rewraps the long lines, the file passes npm run lint:js. setValues only stages the edit:
it is saved with the post through the REST API, where the auth_callback of lp_isbn (Editors and above) still
decides, so an Author’s ISBN edit is rejected on save.
Building and loading the script
The script is not a block, so it is built as a separate entry next to the blocks of
block.json and Registration. The editor packages are listed as
dependencies so the linter can resolve the imports; webpack still loads them from WordPress:
{
"name": "lumen-catalog",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "npm run build:blocks && npm run build:editor",
"build:blocks": "wp-scripts build --source-path=src/blocks --output-path=build/blocks --blocks-manifest --experimental-modules",
"build:editor": "wp-scripts build book-data.js --source-path=src/editor --output-path=build/editor",
"lint:js": "wp-scripts lint-js src/editor"
},
"dependencies": {
"@wordpress/blocks": "17.0.0",
"@wordpress/core-data": "9.0.0",
"@wordpress/i18n": "7.0.0"
},
"devDependencies": {
"@wordpress/scripts": "37.0.0"
}
}
An entry passed on the command line is resolved relative to --source-path; without --source-path=src/editor the
build also copies the blocks' block.json files from the default src/ into build/editor/. npm run build writes
build/editor/book-data.js and book-data.asset.php, whose dependency list (wp-blocks, wp-core-data,
wp-i18n) and content hash enqueue_editor_source() passes to wp_enqueue_script().
enqueue_block_editor_assets loads it in the post and Site Editors after wp-blocks has bootstrapped the server
sources, so the client registration completes the bootstrapped one.
Binding in the editor
Users create bindings in the Attributes panel of the block settings sidebar (6.7). Since 6.9 each bindable
attribute can be connected to a field of any source with getFieldsList() and disconnected with one click. Only
fields whose type matches the attribute are listed (rich-text counts as string). core/post-meta lists the
registered, non-protected meta of the post type by its label (6.7), also used as the placeholder for an empty value.
Who may change bindings is controlled by the edit_block_binding meta capability (6.7): in the post editor it maps
to editing the post, in the Site Editor to edit_theme_options. The result reaches the editor as the
canUpdateBlockBindings setting, which block_editor_settings_all can override. For a book written by the admin,
get_block_editor_settings() returns true for admin and maria.editor and false for tom.author.
Editing a value is up to the source’s canUserEditValue. For core/post-meta the value is editable when the key
is registered for the post type, the user can update the post, the block is not in a Query Loop, and the classic
Custom Fields panel is off in the editor preferences.
Version history
| Version | Change |
|---|---|
6.5 |
Block Bindings API: |
6.6 |
Pattern overrides in synced patterns, with |
6.7 |
Editor API: |
6.9 |
|
7.1 |
List Item |
7.2 (beta) |
Icon block |
Errors and pitfalls
| Symptom | Cause and fix |
|---|---|
The fallback text shows instead of the meta value |
The key is not registered with |
A bound block is empty |
The source returned an empty string (an unset meta with a |
Binding ignored on a custom block |
The attribute is not in the supported list (add it with the 6.9 filter), or its |
Meta field missing from the Attributes panel |
Its type does not match the attribute ( |
Bound value is read-only in the editor |
No |
Custom source shows only its label in the editor |
Only the server half is registered; add the JavaScript |
|
|
A bound value ignores unsaved edits |
|
Security
A source runs on every render with the visitor’s permissions and decides what leaves the database: check visibility
(is_post_publicly_viewable(), read_post, post_password_required()) as the core sources do. core/post-meta adds
no exposure beyond show_in_rest, but a custom source can read anything, protected keys included. Output is filtered
(wp_kses_post() for rich text, HTML API escaping for attributes), yet URLs still go through esc_url_raw() so no
javascript: URL is bound to a link. On the write side, the meta auth_callback and sanitize_callback protect the
data, not canUserEditValue. More in Security for Developers.
What older books and tutorials get wrong
None of the three older books (WordPress All-in-One For Dummies, Pro WordPress, Ultimate WordPress Handbook) covers Block Bindings, which arrived after their WordPress versions (6.3–6.6). Their ways of showing custom fields still work but are no longer the first choice for block themes:
| Older advice | Current practice |
|---|---|
Show a custom field with a shortcode, or with |
Bind a core block to |
A block attribute with |
Deprecated; use a binding, or |
A custom block for every field to display |
One custom bindings source, used by core blocks. |
"Reusable blocks" that cannot differ between uses |
Synced patterns (6.3) with overrides (6.6). |
The full list is in Where Older Books and Tutorials Are Outdated.
References
-
Code Reference: block_bindings_supported_attributes_\{$block_type}
-
Make WordPress Core: Block Bindings: Improvements to the Editor Experience in 6.7
-
Make WordPress Core: Block Bindings improvements in WordPress 6.9
-
WordPress Developer Blog: Introducing Block Bindings, part 1: Connecting custom fields
-
WordPress Developer Blog: Introducing Block Bindings, part 2: Working with custom binding sources
-
WordPress Developer Blog: An introduction to overrides in Synced Patterns
-
WordPress 7.1.3: wp-includes/block-bindings/pattern-overrides.php