Skip to content

Instantly share code, notes, and snippets.

@mukeshpanchal27
Last active October 1, 2024 09:48
Show Gist options
  • Save mukeshpanchal27/bd0457cbfef993808beffe02a9389a8e to your computer and use it in GitHub Desktop.
Save mukeshpanchal27/bd0457cbfef993808beffe02a9389a8e to your computer and use it in GitHub Desktop.
POC: image sizes calculation using column context

WPP Column Context Plugin

Description

The WPP Column Context Plugin is a proof of concept (POC) WordPress plugin that demonstrates how to extend block context for core/columns and core/column blocks and dynamically calculate responsive image sizes based on column widths. The plugin adds custom context keys for columns and column widths, allowing block types like core/image to access this information when rendering.

Installation

  1. Download the plugin and place it in your WordPress wp-content/plugins/ directory.
  2. Activate the plugin through the WordPress admin dashboard.

Usage

  • Add a columns block (core/columns) and specify the column widths 50%.
  • Add an image block (core/image) inside the columns.
  • The plugin will automatically calculate and set the sizes attribute for the image, based on the column layout.

Example

For a simple 2-column layout, where each column is 50% wide:

  • The image block inside the column will have a sizes attribute calculated to match its column's width.
  • For an image aligned wide, the sizes attribute will be dynamically set to ensure optimal image loading.

Development Notes

  • The plugin uses a hardcoded example for wide alignment and statically checks for specific column layouts in wpp_better_sizes_calc. This is intended for demo purposes and can be extended for real-world use cases.
  • Performance improvements related to image metadata fetching still need to be addressed, as wp_calculate_image_sizes() currently makes a database query.

Credits

Inspiration for this plugin is taken from the current block binding implementation in WordPress Core:

<?php
/**
* Plugin Name: WPP Column Context
* Version: 0.1
* Author: Mukesh Panchal
* License: GPL2
*/
namespace WPP_Column_Context;
use WP_HTML_Tag_Processor;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
}
add_filter(
'render_block_context',
function( $context, $block, $parent ) {
// Merge current context into the parent's context.
if ( $parent ) {
$context = array_merge( $parent->context, $context );
}
if ( 'core/columns' === $block['blockName'] ) {
$context['wpp-columns'] = count( $block['innerBlocks'] );
// Extra context for testing.
$context['wpp-columns-extra'] = 'columns-extra';
}
// For demo purposes, this assumes all unspecified columns are equal widths
if ( 'core/column' === $block['blockName'] ) {
$context['wpp-col-width'] = $block['attrs']['width'] ?? 'auto';
// Extra context for testing.
$context['wpp-col-extra'] = 'col-extra';
}
return $context;
},
10,
3
);
/*
* Inspiration taken from current block binding implementation.
* https://github.com/WordPress/wordpress-develop/blob/6.6/src/wp-includes/class-wp-block-bindings-registry.php#L193-L204
*/
add_filter(
'get_block_type_uses_context',
function ( $uses_context, $block_type ) {
if ( 'core/image' === $block_type->name ) {
// Use array_values to reset the array keys.
return array_values( array_unique( array_merge( $uses_context, array( 'wpp-columns','wpp-col-width' ) ) ) );
}
return $uses_context;
},
10,
2
);
add_filter( 'render_block_core/image', __NAMESPACE__ . '\\wpp_add_image_block_sizes', 10, 3 );
/**
* Calculate image sizes attribute for an image block during rendering.
*
* @param string $block_content The block content.
* @param array $parsed_block The parsed block data.
* @param WP_Block $wp_block The block instance.
* @return string The updated block content.
*/
function wpp_add_image_block_sizes( $block_content, $parsed_block, $wp_block ) {
// If you want to check the context for image block add `?debug` in you url.
if ( isset( $_GET['debug'] ) ) {
echo '<pre>';
print_r( $wp_block->context );
echo '</pre>';
}
/**
* Callback for calculating image sizes attribute value for an image block.
*
* This is a workaround to use block context data when calculating the img sizes attribute.
*
* @since 😎
*
* @param string $sizes The image sizes attribute value.
* @param array $size The image size data.
*/
$filter = function( $sizes, $size ) use ( $wp_block ) {
$alignment = $wp_block->attributes['align'] ?? '';
$columns = $wp_block->context['wpp-columns'] ?? ''; // Consider two columns
$column_width = $wp_block->context['wpp-col-width'] ?? ''; // Consider 50% each width
// Hypotehtical function to calculate better sizes.
$sizes = wpp_better_sizes_calc( $size, $alignment, $columns, $column_width );
return $sizes;
};
// Hook this filter early, before default fitlers are run.
add_filter( 'wp_calculate_image_sizes', $filter, 9, 2 );
/*
* A performance problem to still be solved with this approach is that
* wp_calculate_image_sizes() will make a DB query to get the image metadata.
* It does this to get the image width, which is needed to calculate
* the sizes attribute. Currently, this function is called from `wp_filter_content_tags`
* which has already primed the cache with the image metadata. That cache priming
* will need to be hanlded earlier if sizes is calculated during block rendering.
*/
$sizes = wp_calculate_image_sizes(
// If we don't have a size slug, assume the full size was used.
$parsed_block['attrs']['sizeSlug'] ?? 'full',
null,
null,
$parsed_block['attrs']['id'] ?? 0
);
remove_filter( 'wp_calculate_image_sizes', $filter, 9 );
// Bail early if sizes are not calculated.
if ( ! $sizes ) {
return $block_content;
}
// Add the sizes attribute to the image tag here.
$processor = new WP_HTML_Tag_Processor( $block_content );
$processor->next_tag( 'img' );
$processor->set_attribute( 'sizes', $sizes );
$return = $processor->get_updated_html();
return $return;
};
/**
* Hypothetical function to calculate better sizes.
*
* @param array $size The image size data.
* @param string $alignment The image alignment.
* @param int $columns The column count.
* @param string $column_width The column width.
* @return string The sizes attribute value.
*/
function wpp_better_sizes_calc( $size, $alignment, $columns, $column_width ) {
switch ( $alignment ) {
case 'full':
return '100vw';
break;
case 'wide':
// Hard coded wide size from TT4 theme for demo only.
if ( 2 === $columns && '50%' === $column_width ) { // Statically checking for column
return '(max-width: 640px) 100vw, 640px';
}
return '(max-width: 1280px) 100vw, 1280px';
break;
default:
// Hard coded wide size from TT4 theme for demo only.
if ( 2 === $columns && '50%' === $column_width ) { // Statically checking for column
return '(max-width: 310px) 100vw, 310px';
}
return '(max-width: 620px) 100vw, 620px';
}
}
@felixarntz
Copy link

@mukeshpanchal27 Some notes on this POC approach:

  • I think setting specific context keys per block is problematic, as it doesn't work well for nested blocks. For example, in the render_block_context callback you merge the parent and child contexts. This means that, if you have e.g. a columns block within another column (and thus another columns block), the child columns data will overwrite the parent columns data.
  • Another (smaller) issue with block specific keys is that we will need to allowlist all of those with the relevant filters. So it would be great to come up with a more generic and block-agnostic approach.
  • Based on these two points, I think we could do something like this:
    • Have a single context key, which contains data to effectively track the width of the current block. Let's say we call it block_width_data.
    • For example, this could be an array of numbers that nested blocks can add to.
    • And then, whenever an image is rendered in a block, we can compute the width that this image should have.

Here's an example for this: Let's say the site's wideWidth is 1400px. Let's say we have a group block as the outer most block, with wideWidth. Then within it a columns block with 3 columns where the first column is 33%, then in the first column another columns block with 2 columns of 50% width. One of these columns contains an image block. Here's what would happen:

  • For the group block, the render_block_context callback would initialize the block_width_data key with a first entry 1400px.
  • For the first columns + column block combination, the callback would append another entry 33% to block_width_data.
  • For the second columns + column block combination, the callback would append another entry 50% to block_width_data.
  • When we get to the image block, we can look at block_width_data and multiply: 1400px * 33% * 50% = 231px
  • So we would use 231px in the sizes attribute.
  • Of course this example is simplified, most importantly it only considers large enough viewports like desktop. For smaller viewports, we have to consider where these blocks have their breakpoint so that we can use different data for those viewports. That said, many times on mobile this is actually simpler since blocks usually stack on top of each other anyway. We need to explore how relevant column display or limiting to a specific maximum width even is on smaller viewports. If we don't find it to be relevant, we could, for starters, simply assume all content up to a specific breakpoint is 100% wide (minus a bit of padding, but we can ignore that).

Here's a rough draft of what the render_block_context callback could look like:

function( $context, $block, $parent ) {
	$block_width_data = $parent->context['block_width_data'] ?? array();
	if ( 'core/group' === $block['blockName'] || 'core/columns' === $block['blockName'] ) {
		if ( isset( $block['attrs']['align'] ) ) {
			switch ( $block['attrs']['align'] ) {
				case 'full':
					$block_width_data[] = '100%';
					break;
				case 'wide':
					$block_width_data[] = '1400px'; // Use actual `wideWidth` here.
					break;
				default:
					$block_width_data[] = '800px'; // Use actual `contentWidth` here.
			}
		} else {
			$block_width_data[] = '800px'; // Use actual `contentWidth` here.
		}
	}
	if ( 'core/columns' === $block['blockName'] ) {
		// This is a special context key just to pass to the child 'core/column' block.
		$context['column_count'] = count( $block['innerBlocks'] );
	}
	if ( 'core/column' === $block['blockName'] ) {
		if ( isset( $block['attrs']['width'] ) ) {
			// Use specific column width.
			$block_width_data[] = $block['attrs']['width'];
		} elseif ( isset( $parent->context['column_count'] ) ) {
			// Determine the width based on equally sized columns.
			$block_width_data[] = '' . ( 100 / $parent->context['column_count'] ) . '%';
		}
	}

	if ( $block_width_data ) {
		$context['block_width_data'] = $block_width_data;
	}
	return $context;
}

Curious to get your thoughts on this.

@mukeshpanchal27
Copy link
Author

Thanks @felixarntz for reviewing.

Here the biggest concern is that we don't get the outer most block context in our case for group block. In example you have shown above it didn't get context for group block and it will start adding context for Columns block only.

Note: i have added extra key to check which block set the context, Group or Columns.

If we have Group > Columns > Column ( 66% ) > Image then the result

[block_width_data] => Array
        (
            [0] => core/columns
            [1] => 800px
            [2] => 66.66%
        )

If we have Group > Columns > Column ( 66% ) > Columns > Column ( 50% ) > Image then the result

[block_width_data] => Array
        (
            [0] => core/columns
            [1] => 800px
            [2] => 66.66%
            [3] => core/columns
            [4] => 800px
            [5] => 50%
        )

@mukeshpanchal27
Copy link
Author

@felixarntz @joemcgill

If i use the filter without changes anything in current WP Core, I get the correct block_width_data context for image block.

$block_width_data = array();
add_filter(
	'render_block_context',
	function( $context, $block, $parent ) use ( &$block_width_data ) {
		if ( ! $parent ) {
			$block_width_data = array();
		}

		$block_width_data = $parent->context['block_width_data'] ?? $block_width_data;
		if ( 'core/group' === $block['blockName'] || 'core/columns' === $block['blockName'] ) {
			if ( isset( $block['attrs']['align'] ) ) {
				switch ( $block['attrs']['align'] ) {
					case 'full':
						$block_width_data[] = '100%';
						break;
					case 'wide':
						$block_width_data[] = '1400px'; // Use actual `wideWidth` here.
						break;
					default:
						$block_width_data[] = '800px'; // Use actual `contentWidth` here.
				}
			} else {
				$block_width_data[] = '800px'; // Use actual `contentWidth` here.
			}
		}
		if ( 'core/columns' === $block['blockName'] ) {
			// This is a special context key just to pass to the child 'core/column' block.
			$context['column_count'] = count( $block['innerBlocks'] );
		}
		if ( 'core/column' === $block['blockName'] ) {
			if ( isset( $block['attrs']['width'] ) && $block['attrs']['width'] ) {
				// Use specific column width.
				$block_width_data[] = $block['attrs']['width'];
			} elseif ( isset( $parent->context['column_count'] ) && $parent->context['column_count'] ) {
				// Determine the width based on equally sized columns.
				$block_width_data[] = '' . ( 100 / $parent->context['column_count'] ) . '%';
			}
		}
	
		if ( $block_width_data ) {
			$context['block_width_data'] = $block_width_data;
		}
		return $context;
	},
	10,
	3
);

The result:

If we have Group > Columns > Column ( 66% ) > Image then the result

[block_width_data] => Array
        (
            [0] => 800px // Group block default alignment
            [1] => 800px // Columns block default alignment
            [2] => 66.66%
        )

If we have Group > Columns > Column ( 66% ) > Columns > Column ( 50% ) > Image then the result

[block_width_data] => Array
        (
            [0] => 800px // Group block default alignment
            [1] => 800px // First Columns block default alignment
            [2] => 66.66%
            [3] => 800px // Second Columns block default alignment
            [4] => 50%
        )

If we have Group > Columns > Column ( 66% ) > Group > Columns > Column ( 50% ) > Image then the result

[block_width_data] => Array
        (
            [0] => 800px // Group block default alignment
            [1] => 800px // First Columns block default alignment
            [2] => 66.66%
            [3] => 800px // Second Group block default alignment
            [4] => 800px // Second Columns block default alignment
            [5] => 50%
        )

Notes: For the nested block the editor didn't allow the alignment option from third block. Group > Group > Columns in this case only the first and second Group block have alignment option not Columns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment