Image format fallback support

Ended up creating a small plugin that forces Bricks image elements to expose WordPress attachment metadata (wp-image-{ID} class and srcset attributes) so Performance Lab can emit fallbacks for Bricks images. It hooks into the bricks/frontend/render_element filter and applies WordPress content image filters before Performance Lab processes the markup.

If anyone is interested you can use it freely for yourself:

<?php
/**
 * Plugin Name: MT 路 Fixes 路 Bricks image fallback
 * Plugin URI: https://mateitudor.com
 * Description: Injects WordPress attachment metadata into Bricks image elements so Performance Lab can emit <picture> fallbacks.
 * Version: 1.0.0
 * Author: Tudor Matei
 * Author URI: https://mateitudor.com
 * Text Domain: mt-fixes-bricks-image-fallback
 * License: The Unlicense
 * License URI: https://unlicense.org/
 *
 * @package MT_Fixes_Bricks_Image_Fallback
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action(
	'plugins_loaded',
	static function() {
		add_filter( 'bricks/frontend/render_element', 'mt_bif_filter_bricks_image_output', 40, 2 );
	}
);

/**
 * Ensures Bricks image markup exposes attachment metadata before Performance Lab runs.
 */
function mt_bif_filter_bricks_image_output( $html, $element ) {
	if (
		! isset( $element->name ) ||
		'image' !== $element->name ||
		(
			function_exists( 'bricks_is_builder_main' ) &&
			( bricks_is_builder_main() || bricks_is_builder_iframe() || bricks_is_builder_call() )
		)
	) {
		return $html;
	}

	$attachment_id = (int) ( $element->settings['image']['id'] ?? 0 );
	if ( $attachment_id <= 0 ) {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( '[MT Bricks Image Fallback] Missing attachment id for element ' . (string) ( $element->id ?? '' ) );
		}
		return $html;
	}

	static $processed_cache = array();
	$cache_key             = $element->id . ':' . md5( $html );

	if ( isset( $processed_cache[ $cache_key ] ) ) {
		return $processed_cache[ $cache_key ];
	}

	$has_wp_class = str_contains( $html, 'wp-image-' . $attachment_id );
	$has_srcset   = str_contains( $html, 'srcset=' );

	if ( $has_wp_class && $has_srcset ) {
		$processed_cache[ $cache_key ] = $html;
		return $html;
	}

	if ( ! $has_wp_class ) {
		$html = mt_bif_append_wp_image_class( $html, $attachment_id );
	}

	$filtered_html = mt_bif_apply_wp_content_img_tag_filters( $html, $attachment_id );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $filtered_html !== $html ) {
		error_log( '[MT Bricks Image Fallback] Updated markup for element ' . (string) ( $element->id ?? '' ) );
	}

	$processed_cache[ $cache_key ] = $filtered_html;

	return $filtered_html;
}

/**
 * Adds the wp-image-{ID} class so WordPress can resolve attachment metadata.
 */
function mt_bif_append_wp_image_class( string $html, int $attachment_id ): string {
	if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
		return $html;
	}

	$processor = new WP_HTML_Tag_Processor( $html );
	if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
		return $html;
	}

	$current_class = (string) $processor->get_attribute( 'class' );
	$wp_class      = 'wp-image-' . $attachment_id;

	if ( str_contains( $current_class, $wp_class ) ) {
		return $html;
	}

	$processor->set_attribute( 'class', trim( $current_class . ' ' . $wp_class ) );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		error_log( '[MT Bricks Image Fallback] Added class ' . $wp_class );
	}

	return $processor->get_updated_html();
}

/**
 * Passes markup through WordPress image filters to trigger Performance Lab <picture> fallbacks.
 */
function mt_bif_apply_wp_content_img_tag_filters( string $html, int $attachment_id ): string {
	if ( has_filter( 'wp_content_img_tag' ) ) {
		$filtered_html = apply_filters( 'wp_content_img_tag', $html, 'the_content', $attachment_id );
		if ( is_string( $filtered_html ) ) {
			return $filtered_html;
		}
	}

	if ( function_exists( 'wp_filter_content_tags' ) ) {
		$filtered_html = wp_filter_content_tags( $html, 'the_content' );
		if ( is_string( $filtered_html ) ) {
			return $filtered_html;
		}
	}

	return $html;
}

1 Like