Skip to content

Commit a858208

Browse files
authored
feat: first pass at enforcing image dimensions (#867)
<!-- Filling out this template is required. Any PR that does not include enough information to be reviewed may be closed at a maintainers' discretion. All new code requires documentation and tests to ensure against regressions. --> ### Description of the Change <!-- We must be able to understand the design of your change from this description. The maintainer reviewing this PR may not have worked with this code recently, so please provide as much detail as possible. Where possible, please also include: - verification steps to ensure your change has the desired effects and has not introduced any regressions - any benefits that will be realized - any alternative implementations or possible drawbacks that you considered - screenshots or screencasts --> <!-- Enter any applicable Issue number(s) here that will be closed/resolved by this PR. --> This PR adds logic to ensure that internal images always have width/height when added to the block editor. This is useful because Next.js requires width/height in order to optimize images ### Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you are unsure about any of these, please ask for clarification. We are here to help! --> - [x] I agree to follow this project's [**Code of Conduct**](https://github.com/10up/.github/blob/trunk/CODE_OF_CONDUCT.md). - [x] I have updated the documentation accordingly. - [x] I have added [Critical Flows, Test Cases, and/or End-to-End Tests](https://10up.github.io/Open-Source-Best-Practices/testing/) to cover my change. - [x] All new and existing tests pass.
1 parent 338d616 commit a858208

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

.changeset/many-drinks-prove.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@headstartwp/headstartwp": minor
3+
---
4+
5+
feat: ensure all internal images added to the block editor contains width and height.

docs/documentation/06-WordPress Integration/gutenberg.md

+24
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,28 @@ $doc = apply_filters(
110110
$block,
111111
$block_instance
112112
);
113+
```
114+
115+
### tenup_headless_wp_ensure_image_dimensions
116+
117+
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:
118+
119+
1. Checking if the image already has width and height attributes
120+
2. If not, attempting to get the image ID from the URL
121+
3. Using WordPress core functions to add the dimensions and srcset attributes
122+
123+
This process is only applied to images hosted on your WordPress site and will be skipped for external images.
124+
125+
```php
126+
/**
127+
* Filter to enable image dimension processing
128+
*
129+
* @param bool $enable Whether to enable adding dimensions, defaults to false
130+
* @param string $block_content The block content
131+
* @param array $block The block schema
132+
*/
133+
add_filter( 'tenup_headless_wp_ensure_image_dimensions', function( $enable, $block_content, $block ) {
134+
// Return true to enable the feature
135+
return true;
136+
}, 10, 3 );
113137
```

wp/headless-wp/includes/classes/Integrations/Gutenberg.php

+88
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,94 @@ class Gutenberg {
2222
*/
2323
public function register() {
2424
add_filter( 'render_block', [ $this, 'render_block' ], 10, 3 );
25+
add_filter( 'render_block_core/image', [ $this, 'ensure_image_has_dimensions' ], 9999, 2 );
26+
}
27+
28+
/**
29+
* Get the image ID by URL
30+
*
31+
* @param string $url The image URL
32+
*
33+
* @return int
34+
*/
35+
protected function get_image_by_url( $url ) {
36+
if ( function_exists( '\wpcom_vip_attachment_url_to_postid' ) ) {
37+
return \wpcom_vip_attachment_url_to_postid( $url );
38+
}
39+
40+
$cache_key = sprintf( 'get_image_by_%s', md5( $url ) );
41+
$url = esc_url_raw( $url );
42+
$id = wp_cache_get( $cache_key, 'headstartwp', false );
43+
44+
if ( false === $id ) {
45+
$id = attachment_url_to_postid( $url );
46+
47+
/**
48+
* If no ID was found, maybe we're dealing with a scaled big image. So, let's try that.
49+
*
50+
* @see https://core.trac.wordpress.org/ticket/51058
51+
*/
52+
if ( empty( $id ) ) {
53+
$path_parts = pathinfo( $url );
54+
55+
if ( isset( $path_parts['dirname'], $path_parts['filename'], $path_parts['extension'] ) ) {
56+
$scaled_url = trailingslashit( $path_parts['dirname'] ) . $path_parts['filename'] . '-scaled.' . $path_parts['extension'];
57+
$id = attachment_url_to_postid( $scaled_url );
58+
}
59+
}
60+
61+
wp_cache_set( $cache_key, $id, 'headstartwp', 3 * HOUR_IN_SECONDS );
62+
}
63+
64+
return $id;
65+
}
66+
67+
/**
68+
* Ensure that images have dimensions set
69+
*
70+
* @param string $block_content the html for the block
71+
* @param array $block the block's schema
72+
*
73+
* @return string
74+
*/
75+
public function ensure_image_has_dimensions( $block_content, $block ) {
76+
/**
77+
* Filter whether to bypass adding dimensions to images
78+
*
79+
* @param bool $bypass Whether to bypass adding dimensions, defaults to false
80+
* @param string $block_content The block content
81+
* @param array $block The block schema
82+
*/
83+
if ( ! apply_filters( 'tenup_headless_wp_ensure_image_dimensions', false, $block_content, $block ) ) {
84+
return $block_content;
85+
}
86+
87+
$doc = new \WP_HTML_Tag_Processor( $block_content );
88+
89+
if ( $doc->next_tag( 'img' ) ) {
90+
$src = $doc->get_attribute( 'src' );
91+
92+
if ( $doc->get_attribute( 'width' ) && $doc->get_attribute( 'height' ) ) {
93+
return $block_content;
94+
}
95+
96+
$src_check = str_replace( 'http://', 'https://', $src );
97+
$site_url = str_replace( 'http://', 'https://', get_site_url() );
98+
99+
// check if $src is a image hosted in the current wp install and block has no ID
100+
if ( str_contains( $src_check, $site_url ) && empty( $block['attrs']['id'] ) ) {
101+
$image_id = $this->get_image_by_url( $src );
102+
103+
if ( $image_id ) {
104+
$img = wp_img_tag_add_width_and_height_attr( $block_content, 'the_content', $image_id );
105+
$img = wp_img_tag_add_srcset_and_sizes_attr( $img, 'the_content', $image_id );
106+
107+
return $img;
108+
}
109+
}
110+
}
111+
112+
return $block_content;
25113
}
26114

27115
/**

wp/headless-wp/tests/php/tests/TestGutenbergIntegration.php

+64
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,70 @@ public function test_render_classic_block_html_tag_api() {
199199
remove_filter( 'tenup_headless_wp_render_block_use_tag_processor', '__return_true' );
200200
}
201201

202+
/**
203+
* Tests that all uploaded images have width and height attributes when rendered
204+
*
205+
* @return void
206+
*/
207+
public function test_ensure_image_width_height() {
208+
$post = $this->factory()->post->create_and_get();
209+
$attachment_id = $this->factory()->attachment->create_upload_object( __DIR__ . '/assets/dummy-image.png', $post->ID );
210+
$src = wp_get_attachment_image_url( $attachment_id, 'full' );
211+
212+
// Test with filter disabled (default)
213+
$block = $this->core_render_block_from_markup( "<!-- wp:image {} --> <figure class=\"wp-block-image\"><img src=\"$src\" alt=\"\"/></figure> <!-- /wp:image -->" );
214+
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );
215+
216+
$doc = new WP_HTML_Tag_Processor( $enhanced_block );
217+
$doc->next_tag( 'img' );
218+
219+
$this->assertNull( $doc->get_attribute( 'width' ) );
220+
$this->assertNull( $doc->get_attribute( 'height' ) );
221+
222+
// Test with filter enabled
223+
add_filter( 'tenup_headless_wp_ensure_image_dimensions', '__return_true' );
224+
225+
$block = $this->core_render_block_from_markup( "<!-- wp:image {} --> <figure class=\"wp-block-image\"><img src=\"$src\" alt=\"\"/></figure> <!-- /wp:image -->" );
226+
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );
227+
228+
$doc = new WP_HTML_Tag_Processor( $enhanced_block );
229+
$doc->next_tag( 'img' );
230+
231+
$this->assertEquals( $doc->get_attribute( 'width' ), 213 );
232+
$this->assertEquals( $doc->get_attribute( 'height' ), 237 );
233+
234+
// Clean up
235+
remove_filter( 'tenup_headless_wp_ensure_image_dimensions', '__return_true' );
236+
237+
// simulate an image with dimensions
238+
$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 -->" );
239+
$doc = new WP_HTML_Tag_Processor( $block['html'] );
240+
$doc->next_tag( 'img' );
241+
242+
$this->assertEquals( $doc->get_attribute( 'width' ), 213 );
243+
$this->assertEquals( $doc->get_attribute( 'height' ), 237 );
244+
245+
// simulate an image with hardcoded width and height
246+
$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 -->" );
247+
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );
248+
249+
$doc = new WP_HTML_Tag_Processor( $enhanced_block );
250+
$doc->next_tag( 'img' );
251+
252+
$this->assertEquals( $doc->get_attribute( 'width' ), 215 );
253+
$this->assertEquals( $doc->get_attribute( 'height' ), 235 );
254+
255+
// simulate an external image
256+
$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 -->' );
257+
$enhanced_block = $this->parser->ensure_image_has_dimensions( $block['html'], $block['parsed_block'] );
258+
259+
$doc = new WP_HTML_Tag_Processor( $enhanced_block );
260+
$doc->next_tag( 'img' );
261+
262+
$this->assertNull( $doc->get_attribute( 'width' ) );
263+
$this->assertNull( $doc->get_attribute( 'height' ) );
264+
}
265+
202266
/**
203267
* Tests block's rendering with newer tag processor api
204268
* - Wrapper to run test_render with the HTML Tag API processor enabled
Loading

0 commit comments

Comments
 (0)