The Problem
I ran into this on a client editorial site last week. We upgraded a publisher from WordPress 6.9 to 7.0 on a staging server, ran the smoke test, and the entire homepage rendered blank wherever an ACF-backed block binding had been wired up. Headings, paragraphs, and image blocks bound to ACF fields through the Block Bindings API all rendered with no content on the frontend. The editor still showed the bound values in the sidebar inspector, so on the surface nothing looked wrong.
This is what the rendered HTML looked like on the live page:
<p class="has-large-font-size has-text-align-center"></p>
<figure class="wp-block-image">
<img src="" alt="" width="0" height="0" />
</figure>
<h2 class="wp-block-heading"></h2>
No PHP fatal, no notices in debug.log, no warning in the editor. Just empty strings everywhere a custom binding source had been registered. Bindings that used ACF's own acf/field source rendered correctly on the same page, so the failure was scoped to the custom sources we had written ourselves.
Why It Happens
WordPress 7.0 changed the signature of get_value_callback for custom block binding sources. Through 6.9 the callback received three positional arguments: $source_args, $block_instance, and $attribute_name. In 7.0 the WordPress core team added a new required argument (the binding source name) and moved it into the second slot, shifting $block_instance and $attribute_name one position later.
If your callback was typed against the old signature, PHP happily coerces the new arguments into the wrong parameters. Your code receives a string where it expected a WP_Block, calls get_field($key, $block->context['postId']) on a string, hits a null chain, and returns an empty value. Because the renderer silently coerces non-string returns to empty strings, you never see a warning. The block just renders blank.
The change shipped through Gutenberg and folded into core without a deprecation cycle, which is why so many integration plugins are tripping on it. The Block Bindings API reference was updated but the migration guidance is buried under the new examples. Anything you wrote when the API was experimental needs a refactor.
The check is fast. If your custom binding source was registered with register_block_bindings_source() and the callback signature is anything other than variadic, you are exposed.
The Fix
Refactor the callback to accept variadic arguments and pick the WP_Block instance out by type. That keeps the code working on both 6.9 and 7.0 stacks so you can roll out without a synchronised deploy.
add_action('init', function () {
register_block_bindings_source('mysite/acf-field', [
'label' => __('ACF Field', 'mysite'),
'uses_context' => ['postId'],
'get_value_callback' => function (array $source_args, ...$rest) {
$block = null;
$attribute = null;
foreach ($rest as $arg) {
if ($arg instanceof WP_Block) {
$block = $arg;
} elseif (is_string($arg) && in_array($arg, ['url', 'alt', 'id', 'content', 'href'], true)) {
$attribute = $arg;
}
}
if (! $block || empty($source_args['key'])) {
return '';
}
$post_id = $block->context['postId'] ?? get_the_ID();
$value = get_field($source_args['key'], $post_id);
if ($attribute === 'url') {
return is_numeric($value) ? (string) wp_get_attachment_image_url((int) $value, 'large') : '';
}
if ($attribute === 'alt') {
return is_numeric($value) ? (string) get_post_meta((int) $value, '_wp_attachment_image_alt', true) : '';
}
return is_scalar($value) ? (string) $value : '';
},
]);
});
Three things matter here. First, the callback walks $rest and identifies arguments by type rather than position. That keeps you safe on both core versions and on any future shuffles. Second, uses_context still declares postId so bindings resolve the right field when used inside post templates, archive loops, or query loops. Third, the return must be a string. Returning null, false, or an array gets coerced to empty by the renderer with no warning.
The attribute matters for image blocks specifically. The renderer calls the binding three times for a single image, once each for url, alt, and id. If your callback ignores the attribute argument, all three calls return the same value and you end up with <img src="42" alt="42" />.
If you are using ACF's native bindings (acf/field), update ACF to 6.5.2 or later. Earlier versions ship the broken signature. From WP-CLI:
wp plugin update advanced-custom-fields-pro
wp cache flush
wp transient delete --all
Verify the fix in two places. Load any page that uses a bound block while logged out so you bypass editor caching, and confirm the rendered HTML contains the expected value. Then open the same post in the block editor and check the inspector sidebar still shows the value in the bindings panel. Both rendering paths share the registration but call the callback with different argument orders internally, and the failure modes are independent.
One more thing worth checking. If you registered your custom source inside a plugin that loads on plugins_loaded rather than init, the registration races against the block parser on AJAX requests in the editor. Move the registration into the init action at priority 10 or later. The race is harmless on 6.9 but exposes a different empty-string path on 7.0.
The Lesson
WordPress 7.0 changed the block bindings callback signature without a deprecation cycle. Custom sources registered before 7.0 receive arguments in the wrong slots, return null, and silently render empty. Refactor to variadic arguments, identify WP_Block by type, handle the attribute name for image bindings, and update ACF to a version that ships the corrected signature. The empty output is a soft failure, so do not trust visual inspection in the editor as proof the frontend works.
If your WordPress 7.0 upgrade has stripped values out of bound blocks across an editorial site, that is the kind of cleanup I get paid to handle. See my services. For a related Block Editor breakage from this release cycle, read WordPress 7.0 block apiVersion iframe fix.
Stuck on a WordPress 7.0 upgrade? Get it shipped.