Block SVG & Block Binding
Lately I bumped into an issue with our Nynaeve Theme based on Sage 11. Blocks that had svg icon images broke on rebuild . They would load as invalid blocks code on each built. Seems that Vite would load them in using a different url hash than the one WordPress had stored them in the database. So on some searching I realized WordPress had already come up with a solution for that. They call it block bindings.
The Block Bindings API lets you “bind” dynamic data to the block’s attributes, which are then reflected in the final HTML markup that is output to the browser on the front end.
So basically instead of just letting WordPress store the icon or image you add in the database as a url hash which gets rebuild on every npm run build you store a binding reference, which will always stay truthful to its final destination.
Block Binding
The action WordPress has for that is called register_block_bindings_source or Register Block Bindings Source.
The action takes a name as string imagewize/theme-icon and an array of data including label, get_value_callback. The callback in turn takes
source_args,block_instanceandattribute_name.
Label is the label of your choosing and preferably one that can be translated. The source arguments get an instance of the post in question , and then attribute name.
Attribute Name
The attribute name for image blocks are id, url, title, alt, and caption. We will be filtering for urls and if attribute is not an url or empty we return right away.
The icon path for the url uses ltrim which
Strip whitespace (or other characters) from the beginning of a string
and it uses the $source_args to get the path. That variable is an array containing source arguments. Details of Attribute name and other values we got are fed to Vite::asset (Illuminate\Support\Facades\Vite::asset).
/**
* Register block bindings source for theme SVG icons.
*
* Instead of storing hashed Vite asset URLs in post_content (which break on every
* rebuild), editor.jsx templates store a binding reference with the icon path.
* This callback resolves the current Vite asset URL at render time, so the
* frontend always serves the correct file regardless of content hash changes.
*
* Usage in block template:
* metadata: { bindings: { url: { source: 'imagewize/theme-icon', args: { path: 'icon-link.svg' } } } }
*
* Paths are relative to resources/images/icons/ (e.g. 'icon-link.svg', 'elayne/icon-fse.svg').
*/
add_action('init', function () {
register_block_bindings_source('imagewize/theme-icon', [
'label' => __('Theme Icon', 'imagewize'),
'get_value_callback' => function (array $source_args, \WP_Block $block_instance, string $attribute_name): ?string {
if ($attribute_name !== 'url' || empty($source_args['path'])) {
return null;
}
$icon_path = ltrim(str_replace('..', '', $source_args['path']), '/');
try {
return Vite::asset('resources/images/icons/'.$icon_path);
} catch (\Exception $e) {
return null;
}
},
]);
});
Editor Addition Block Binding Source
But then we are not done yet. Then we need to pass current Vite-resolved icon links to the block editor as window.imagewizeIcons to be used there. That we do with the action enqueue_block_editor_assets . We are dealing with icon assets here.
In the function call you supply, simply use
wp_enqueue_scriptandwp_enqueue_styleto add your functionality to the block editor.
And this method is combined with wp_add_inline_script to add the Icons as JSON data.
/**
* Pass current Vite-resolved icon URLs to the block editor as window.imagewizeIcons.
*
* editor.jsx templates use these as the initial url attribute on core/image blocks
* so icons display correctly in the editor. The imagewize/theme-icon binding handles
* correct resolution on the frontend — these localized values are editor-only fallbacks.
*
* Add an entry here whenever a new block imports SVG icons via core/image.
*/
add_action('enqueue_block_editor_assets', function () {
// Keys are the icon paths (matching binding args.path) → current Vite asset URL.
// Add an entry here whenever a new block uses imagewize/theme-icon bindings.
$icon_paths = [
// icon-grid block
'icon-link.svg',
'icon-copy.svg',
'icon-x-circle.svg',
'icon-list.svg',
'icon-bar-chart.svg',
'icon-code.svg',
'icon-map.svg',
'icon-chat.svg',
// trust-bar block
'icon-shield.svg',
'icon-users.svg',
'icon-clock.svg',
// service-hero block
'icon-search.svg',
// feature-cards block
'elayne/icon-fse.svg',
'elayne/icon-performance.svg',
'elayne/icon-patterns.svg',
'elayne/icon-plugin.svg',
'elayne/icon-responsive.svg',
'elayne/icon-accessible.svg',
];
$icons = [];
foreach ($icon_paths as $path) {
try {
$icons[$path] = Vite::asset('resources/images/icons/'.$path);
} catch (\Exception $e) {
$icons[$path] = '';
}
}
wp_add_inline_script(
'wp-blocks',
'window.imagewizeIcons = '.wp_json_encode($icons).';',
'before'
);
});