wp-block-bindings-api
Create or audit WordPress Block Bindings sources that connect block attributes to post meta, post/term data, pattern overrides, custom tables, or remote data. Covers PHP source registration, editor registration, metadata.bindings markup, supported-attribute filters, context, edit
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-block-bindings-api
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WordPress Block Bindings API
Block Bindings replace selected block attributes at render time from a registered data source. Use them when content should stay dynamic while remaining a normal block. They do not persist data by themselves and do not make an unsupported attribute bindable automatically.
Decide before implementing
| Need | Use |
|---|---|
| Bind a supported block attribute to dynamic data | Block Bindings |
| Change the complete rendered structure | Dynamic block render.php |
| Store and validate a custom field | Metadata/REST registration plus a binding |
| Merely transform final HTML | Render filter or HTML API |
Register the server source
Register on init. The name must be lowercase namespace/name and the callback must be callable.
add_action( 'init', static function (): void {
register_block_bindings_source(
'acme/catalog-field',
array(
'label' => __( 'Catalog field', 'acme' ),
'uses_context' => array( 'postId' ),
'get_value_callback' => static function ( array $args, WP_Block $block, string $attribute ) {
$allowed = array( 'sku', 'subtitle' );
$key = isset( $args['key'] ) ? sanitize_key( $args['key'] ) : '';
if ( ! in_array( $key, $allowed, true ) ) {
return null;
}
$post_id = isset( $block->context['postId'] ) ? (int) $block->context['postId'] : 0;
return $post_id ? get_post_meta( $post_id, '_acme_' . $key, true ) : null;
},
)
);
} );
The callback receives binding args, the current WP_Block, and the target attribute name. The args originate in block content and are untrusted. Allowlist keys, validate IDs, and return null when no safe value is available.
Bind the attribute
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"acme/catalog-field","args":{"key":"subtitle"}}}}} -->
<p>Fallback subtitle</p>
<!-- /wp:paragraph -->
Do not delete useful fallback markup. If the source is missing, disabled, or returns no usable value, a stable fallback makes the content more portable.
Supported attributes in WordPress 7.1
Core's default list is:
| Block | Attributes |
|---|---|
core/paragraph |
content |
core/heading |
content |
core/list-item |
content (added in 7.1) |
core/image |
id, url, title, alt, caption |
core/button |
url, text, linkTarget, rel |
core/post-date |
datetime |
core/navigation-link, core/navigation-submenu |
url |
Query the truth with get_block_bindings_supported_attributes( $block_type ); do not freeze this table into plugin logic.
For a custom block or deliberately supported attribute, extend the list narrowly:
add_filter( 'block_bindings_supported_attributes_acme/card', static function ( array $attributes ): array {
$attributes[] = 'title';
return array_values( array_unique( $attributes ) );
} );
Only expose attributes whose render pipeline correctly consumes the bound value. A filter entry alone cannot repair custom rendering that ignores parsed attributes.
Editor registration is a separate concern
PHP registration makes frontend/server rendering work. Register the same source in the editor when users need previews, editing, or the binding picker. The editor contract can provide getValues, setValues, canUserEditValue, and, since 6.9, getFieldsList.
Use useBlockBindingsUtils() from @wordpress/block-editor to update or remove metadata.bindings; do not hand-mutate nested block attributes. Keep the source name and argument schema identical in PHP and JavaScript.
Read references/editor-and-security.md before implementing editable bindings.
Core sources
Prefer an existing Core source when it fits, notably core/post-meta, core/post-data, core/term-data, or core/pattern-overrides. For post meta, register the field correctly and expose it to REST/editor contexts as required. Protected meta is not made public merely because a block references it, but your custom callback can accidentally leak it if it skips authorization and context checks.
Performance rules
Binding callbacks can run once per bound attribute for every rendered block.
- Never issue an uncached remote request per callback.
- Cache repeated lookups within the request by source arguments and object ID.
- Prime metadata/object caches for repeated objects where appropriate.
- Keep callbacks deterministic for the same render context; randomness breaks page caches and editor/frontend parity.
- Avoid writes, analytics events, or other side effects in
get_value_callback.
Security checklist
- Treat
argsand block markup as attacker-controlled input. - Check the current object/site context; do not trust a
postIdsupplied in args. - Never return secrets or private metadata to public frontend output.
- Enforce capabilities in editor write paths and REST endpoints, not only in UI predicates.
- Sanitize for storage in the write API; escape at the final block output context.
- Do not return raw HTML for a plain string attribute unless that block's rendering contract safely handles it.
Verification
- Assert source registration after
initwithget_block_bindings_source(). - Render bound markup through
do_blocks()and check the exact output. - Test unknown args, missing object context, deleted source, and
nullreturn. - Test editor preview separately from frontend rendering.
- Measure query/request counts across a post with many bound blocks.
- On 7.1, verify
core/list-itemcontent bindings and backward compatibility for older supported Core versions.
Related skills
wordpress/wp-metadata-apifor registered post, user, term, and comment meta.wordpress/wp-rest-apifor authorized editor write endpoints.wordpress/wp-html-apifor safe rendered-markup changes.wordpress/wp-interactivity-apiwhen the bound output also needs reactive frontend behavior.
References
- Read
references/editor-and-security.mdfor editor registration, editing callbacks, permission and caching detail. - Miscellaneous block editor changes in WordPress 7.1 (List Item binding): https://make.wordpress.org/core/2026/08/04/miscellaneous-block-editor-changes-in-wordpress-7-1/
- WordPress 7.1 Field Guide: https://make.wordpress.org/core/2026/08/05/wordpress-7-1-field-guide/
Files (wp-agent-skills)
-
agents
-
openai.yaml 115 B
interface: display_name: "WP Block Bindings API" short_description: "Connect block attributes to dynamic data"
-
-
references
-
editor-and-security.md 2.3 KB
# Block Bindings editor and security reference ## Two registrations, two responsibilities | Layer | Responsibility | |---|---| | PHP `register_block_bindings_source()` | Authoritative server/frontend value resolution | | Editor `registerBlockBindingsSource()` | Editor preview, binding UI, and optional edit behavior | A JavaScript-only source cannot make PHP render dynamic frontend values. A PHP-only source can render correctly but may provide a poor editing experience. ## Editor callbacks - `getValues( { bindings, clientId, context, select } )` returns an object keyed by block attribute. - `setValues( { bindings, clientId, context, dispatch, select } )` persists edited values. Treat `newValue` as untrusted and use an authorized data/REST layer. - `canUserEditValue()` controls the editor experience, not server authorization. - `getFieldsList()` returns picker entries with a `label`, compatible `type`, and source `args`. - `usesContext` should match the server source. Do not redundantly redefine it in JavaScript when server registration already supplies it to the editor contract. Use `useBlockBindingsUtils( clientId )` for binding metadata: ```js import { useBlockBindingsUtils } from '@wordpress/block-editor'; const { updateBlockBindings, removeAllBlockBindings } = useBlockBindingsUtils( clientId ); updateBlockBindings( { content: { source: 'acme/catalog-field', args: { key: 'subtitle' }, }, } ); ``` ## Threat model Post content can be imported, edited through REST, copied from another site, or authored by a user with fewer privileges than the data source owner. Consequently: - binding source names and args are not trusted configuration; - object IDs in args must not override the actual rendering context without authorization; - `canUserEditValue()` cannot replace a server capability check; - the frontend callback must not reveal data merely because the editor previously allowed the binding; - remote sources require URL allowlists, timeouts, caching, and SSRF-safe HTTP APIs. ## Portable fallback behavior Keep static block content meaningful. Test these states: 1. plugin active and data exists; 2. plugin active but data is absent; 3. source rejects malformed args; 4. plugin/source disabled; 5. block copied to another site. The content should remain valid HTML in every state.
-
-
SKILL.md 7.1 KB
--- name: wp-block-bindings-api description: "Create or audit WordPress Block Bindings sources that connect block attributes to post meta, post/term data, pattern overrides, custom tables, or remote data. Covers PHP source registration, editor registration, metadata.bindings markup, supported-attribute filters, context, editing callbacks, permissions, caching, and the WordPress 7.1 List Item addition." license: GPLv2-or-later metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "wordpress" wp-skills-plugin-version-tested: "6.5 - 7.1" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # WordPress Block Bindings API Block Bindings replace selected block attributes at render time from a registered data source. Use them when content should stay dynamic while remaining a normal block. They do not persist data by themselves and do not make an unsupported attribute bindable automatically. ## Decide before implementing | Need | Use | |---|---| | Bind a supported block attribute to dynamic data | Block Bindings | | Change the complete rendered structure | Dynamic block `render.php` | | Store and validate a custom field | Metadata/REST registration plus a binding | | Merely transform final HTML | Render filter or HTML API | ## Register the server source Register on `init`. The name must be lowercase `namespace/name` and the callback must be callable. ```php add_action( 'init', static function (): void { register_block_bindings_source( 'acme/catalog-field', array( 'label' => __( 'Catalog field', 'acme' ), 'uses_context' => array( 'postId' ), 'get_value_callback' => static function ( array $args, WP_Block $block, string $attribute ) { $allowed = array( 'sku', 'subtitle' ); $key = isset( $args['key'] ) ? sanitize_key( $args['key'] ) : ''; if ( ! in_array( $key, $allowed, true ) ) { return null; } $post_id = isset( $block->context['postId'] ) ? (int) $block->context['postId'] : 0; return $post_id ? get_post_meta( $post_id, '_acme_' . $key, true ) : null; }, ) ); } ); ``` The callback receives binding `args`, the current `WP_Block`, and the target attribute name. The args originate in block content and are untrusted. Allowlist keys, validate IDs, and return `null` when no safe value is available. ## Bind the attribute ```html <!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"acme/catalog-field","args":{"key":"subtitle"}}}}} --> <p>Fallback subtitle</p> <!-- /wp:paragraph --> ``` Do not delete useful fallback markup. If the source is missing, disabled, or returns no usable value, a stable fallback makes the content more portable. ## Supported attributes in WordPress 7.1 Core's default list is: | Block | Attributes | |---|---| | `core/paragraph` | `content` | | `core/heading` | `content` | | `core/list-item` | `content` (added in 7.1) | | `core/image` | `id`, `url`, `title`, `alt`, `caption` | | `core/button` | `url`, `text`, `linkTarget`, `rel` | | `core/post-date` | `datetime` | | `core/navigation-link`, `core/navigation-submenu` | `url` | Query the truth with `get_block_bindings_supported_attributes( $block_type )`; do not freeze this table into plugin logic. For a custom block or deliberately supported attribute, extend the list narrowly: ```php add_filter( 'block_bindings_supported_attributes_acme/card', static function ( array $attributes ): array { $attributes[] = 'title'; return array_values( array_unique( $attributes ) ); } ); ``` Only expose attributes whose render pipeline correctly consumes the bound value. A filter entry alone cannot repair custom rendering that ignores parsed attributes. ## Editor registration is a separate concern PHP registration makes frontend/server rendering work. Register the same source in the editor when users need previews, editing, or the binding picker. The editor contract can provide `getValues`, `setValues`, `canUserEditValue`, and, since 6.9, `getFieldsList`. Use `useBlockBindingsUtils()` from `@wordpress/block-editor` to update or remove `metadata.bindings`; do not hand-mutate nested block attributes. Keep the source name and argument schema identical in PHP and JavaScript. Read [references/editor-and-security.md](references/editor-and-security.md) before implementing editable bindings. ## Core sources Prefer an existing Core source when it fits, notably `core/post-meta`, `core/post-data`, `core/term-data`, or `core/pattern-overrides`. For post meta, register the field correctly and expose it to REST/editor contexts as required. Protected meta is not made public merely because a block references it, but your custom callback can accidentally leak it if it skips authorization and context checks. ## Performance rules Binding callbacks can run once per bound attribute for every rendered block. - Never issue an uncached remote request per callback. - Cache repeated lookups within the request by source arguments and object ID. - Prime metadata/object caches for repeated objects where appropriate. - Keep callbacks deterministic for the same render context; randomness breaks page caches and editor/frontend parity. - Avoid writes, analytics events, or other side effects in `get_value_callback`. ## Security checklist - Treat `args` and block markup as attacker-controlled input. - Check the current object/site context; do not trust a `postId` supplied in args. - Never return secrets or private metadata to public frontend output. - Enforce capabilities in editor write paths and REST endpoints, not only in UI predicates. - Sanitize for storage in the write API; escape at the final block output context. - Do not return raw HTML for a plain string attribute unless that block's rendering contract safely handles it. ## Verification 1. Assert source registration after `init` with `get_block_bindings_source()`. 2. Render bound markup through `do_blocks()` and check the exact output. 3. Test unknown args, missing object context, deleted source, and `null` return. 4. Test editor preview separately from frontend rendering. 5. Measure query/request counts across a post with many bound blocks. 6. On 7.1, verify `core/list-item` content bindings and backward compatibility for older supported Core versions. ## Related skills - `wordpress/wp-metadata-api` for registered post, user, term, and comment meta. - `wordpress/wp-rest-api` for authorized editor write endpoints. - `wordpress/wp-html-api` for safe rendered-markup changes. - `wordpress/wp-interactivity-api` when the bound output also needs reactive frontend behavior. ## References - Read `references/editor-and-security.md` for editor registration, editing callbacks, permission and caching detail. - Miscellaneous block editor changes in WordPress 7.1 (List Item binding): <https://make.wordpress.org/core/2026/08/04/miscellaneous-block-editor-changes-in-wordpress-7-1/> - WordPress 7.1 Field Guide: <https://make.wordpress.org/core/2026/08/05/wordpress-7-1-field-guide/>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.