Skip to content

feat: first pass at enforcing image dimensions #867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 7, 2025
5 changes: 5 additions & 0 deletions .changeset/many-drinks-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@headstartwp/headstartwp": minor
---

feat: ensure all internal images added to the block editor contains width and height.
24 changes: 24 additions & 0 deletions docs/documentation/06-WordPress Integration/gutenberg.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,28 @@ $doc = apply_filters(
$block,
$block_instance
);
```

### tenup_headless_wp_ensure_image_dimensions

HeadstartWP can automatically add width and height attributes to any internal images in Gutenberg blocks that for some reason is missing width and height attributes to prevent layout shifts. This feature is disabled by default and must be enabled using the `tenup_headless_wp_ensure_image_dimensions` filter. When enabled, it works by:

1. Checking if the image already has width and height attributes
2. If not, attempting to get the image ID from the URL
3. Using WordPress core functions to add the dimensions and srcset attributes

This process is only applied to images hosted on your WordPress site and will be skipped for external images.

```php
/**
* Filter to enable image dimension processing
*
* @param bool $enable Whether to enable adding dimensions, defaults to false
* @param string $block_content The block content
* @param array $block The block schema
*/
add_filter( 'tenup_headless_wp_ensure_image_dimensions', function( $enable, $block_content, $block ) {
// Return true to enable the feature
return true;
}, 10, 3 );
```
88 changes: 88 additions & 0 deletions wp/headless-wp/includes/classes/Integrations/Gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,94 @@ class Gutenberg {
*/
public function register() {
add_filter( 'render_block', [ $this, 'render_block' ], 10, 3 );
add_filter( 'render_block_core/image', [ $this, 'ensure_image_has_dimensions' ], 9999, 2 );
}

/**
* Get the image ID by URL
*
* @param string $url The image URL
*
* @return int
*/
protected function get_image_by_url( $url ) {
if ( function_exists( '\wpcom_vip_attachment_url_to_postid' ) ) {
return \wpcom_vip_attachment_url_to_postid( $url );
}

$cache_key = sprintf( 'get_image_by_%s', md5( $url ) );
$url = esc_url_raw( $url );
$id = wp_cache_get( $cache_key, 'headstartwp', false );

if ( false === $id ) {
$id = attachment_url_to_postid( $url );

/**
* If no ID was found, maybe we're dealing with a scaled big image. So, let's try that.
*
* @see https://core.trac.wordpress.org/ticket/51058
*/
if ( empty( $id ) ) {
$path_parts = pathinfo( $url );

if ( isset( $path_parts['dirname'], $path_parts['filename'], $path_parts['extension'] ) ) {
$scaled_url = trailingslashit( $path_parts['dirname'] ) . $path_parts['filename'] . '-scaled.' . $path_parts['extension'];
$id = attachment_url_to_postid( $scaled_url );
}
}

wp_cache_set( $cache_key, $id, 'headstartwp', 3 * HOUR_IN_SECONDS );
}

return $id;
}

/**
* Ensure that images have dimensions set
*
* @param string $block_content the html for the block
* @param array $block the block's schema
*
* @return string
*/
public function ensure_image_has_dimensions( $block_content, $block ) {
/**
* Filter whether to bypass adding dimensions to images
*
* @param bool $bypass Whether to bypass adding dimensions, defaults to false
* @param string $block_content The block content
* @param array $block The block schema
*/
if ( ! apply_filters( 'tenup_headless_wp_ensure_image_dimensions', false, $block_content, $block ) ) {
return $block_content;
}

$doc = new \WP_HTML_Tag_Processor( $block_content );

if ( $doc->next_tag( 'img' ) ) {
$src = $doc->get_attribute( 'src' );

if ( $doc->get_attribute( 'width' ) && $doc->get_attribute( 'height' ) ) {
return $block_content;
}

$src_check = str_replace( 'http://', 'https://', $src );
$site_url = str_replace( 'http://', 'https://', get_site_url() );

// check if $src is a image hosted in the current wp install and block has no ID
if ( str_contains( $src_check, $site_url ) && empty( $block['attrs']['id'] ) ) {
$image_id = $this->get_image_by_url( $src );

if ( $image_id ) {
$img = wp_img_tag_add_width_and_height_attr( $block_content, 'the_content', $image_id );
$img = wp_img_tag_add_srcset_and_sizes_attr( $img, 'the_content', $image_id );

return $img;
}
}
}

return $block_content;
}

/**
Expand Down
64 changes: 64 additions & 0 deletions wp/headless-wp/tests/php/tests/TestGutenbergIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,70 @@ public function test_render_classic_block_html_tag_api() {
remove_filter( 'tenup_headless_wp_render_block_use_tag_processor', '__return_true' );
}

/**
* Tests that all uploaded images have width and height attributes when rendered
*
* @return void
*/
public function test_ensure_image_width_height() {
$post = $this->factory()->post->create_and_get();
$attachment_id = $this->factory()->attachment->create_upload_object( __DIR__ . '/assets/dummy-image.png', $post->ID );
$src = wp_get_attachment_image_url( $attachment_id, 'full' );

// Test with filter disabled (default)
$block = $this->core_render_block_from_markup( "<!-- wp:image {} --> <figure class=\"wp-block-image\"><img src=\"$src\" alt=\"\"/></figure> <!-- /wp:image -->" );
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );

$doc = new WP_HTML_Tag_Processor( $enhanced_block );
$doc->next_tag( 'img' );

$this->assertNull( $doc->get_attribute( 'width' ) );
$this->assertNull( $doc->get_attribute( 'height' ) );

// Test with filter enabled
add_filter( 'tenup_headless_wp_ensure_image_dimensions', '__return_true' );

$block = $this->core_render_block_from_markup( "<!-- wp:image {} --> <figure class=\"wp-block-image\"><img src=\"$src\" alt=\"\"/></figure> <!-- /wp:image -->" );
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );

$doc = new WP_HTML_Tag_Processor( $enhanced_block );
$doc->next_tag( 'img' );

$this->assertEquals( $doc->get_attribute( 'width' ), 213 );
$this->assertEquals( $doc->get_attribute( 'height' ), 237 );

// Clean up
remove_filter( 'tenup_headless_wp_ensure_image_dimensions', '__return_true' );

// simulate an image with dimensions
$block = $this->core_render_block_from_markup( "<!-- wp:image {\"id\":$attachment_id} --> <figure class=\"wp-block-image\"><img class=\"wp-image-$attachment_id\" src=\"$src\" alt=\"\"/></figure> <!-- /wp:image -->" );
$doc = new WP_HTML_Tag_Processor( $block['html'] );
$doc->next_tag( 'img' );

$this->assertEquals( $doc->get_attribute( 'width' ), 213 );
$this->assertEquals( $doc->get_attribute( 'height' ), 237 );

// simulate an image with hardcoded width and height
$block = $this->core_render_block_from_markup( "<!-- wp:image {} --> <figure class=\"wp-block-image\"><img src=\"$src\" alt=\"\" width=\"215\" height=\"235\"/></figure> <!-- /wp:image -->" );
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );

$doc = new WP_HTML_Tag_Processor( $enhanced_block );
$doc->next_tag( 'img' );

$this->assertEquals( $doc->get_attribute( 'width' ), 215 );
$this->assertEquals( $doc->get_attribute( 'height' ), 235 );

// simulate an external image
$block = $this->core_render_block_from_markup( '<!-- wp:image {} --> <figure class="wp-block-image"><img src="https://example.com/image.png" alt=""/></figure> <!-- /wp:image -->' );
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );

$doc = new WP_HTML_Tag_Processor( $enhanced_block );
$doc->next_tag( 'img' );

$this->assertNull( $doc->get_attribute( 'width' ) );
$this->assertNull( $doc->get_attribute( 'height' ) );
}

/**
* Tests block's rendering with newer tag processor api
* - Wrapper to run test_render with the HTML Tag API processor enabled
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading