Pragmatism in the real world

Images and WordPress

My new WordPress project has multiple photographs per post and as I wanted them to work in an efficient manner for multiple screen resolutions. The secret to this is the srcset and sizes attributes on the img tag.

It turns out that WordPress will create multiple sized thumbnails when you upload an image. It will also add the srcset and sizes attributes into your img tags for you if your image tag has a class of wp‑image‑{id} where {id} is the id of the image in the database.

Image sizes

The set of sizes of images that WordPress creates by default is rather small (you get widths of 150px, 300px, 768px, & 1024px). As I’m uploading 6000px wide photos, having some intermediate sizes greater than 1024 is useful to reduce the amount of data for desktop screens.

You can add new ones using the add_image_size() method which is best done after the theme is set up:

add_action('after_setup_theme', static function () {
    add_image_size('xl', 1500);
    add_image_size('2xl', 2000);
    add_image_size('3xl', 2500);
    add_image_size('4xl', 3000);
    add_image_size('5xl', 4000);
    add_image_size('6xl', 5000);
});

The third parameter to add_image_size() is the height constraint, which I don’t set as I don’t want it to be taken into account for portrait orientation pictures.

Note that even though you’ve added additional sizes, WordPress will ignore any bigger than 1600 pixels unless you tell it otherwise by adding a filter to max_srcset_image_width:

add_filter('max_srcset_image_width', static function($max_width){
    return $max_width < 6000 ? 6000 : $max_srcset_image_width;
});

If you change the set of image sizes after you’ve uploaded some images then you can create the new images using the wp‑cli media regenerate command or install the Regenerate Thumbnails plugin and run it from the Tools menu in your admin.

Adding wp‑image‑{id} class to posts

If you use the WordPress admin site to create your posts, then it automatically set the wp‑image‑{id} class for you on our img tags. I use MarsEdit, a desktop client that uses the XML-RPC API, for creating my articles and it doesn’t set this class yet, so I needed to write a plug in for this.

To do this, we put a filter on the xmlrpc_wp_insert_post_data hook to inspect the content of the post and update if needed.

add_filter('xmlrpc_wp_insert_post_data', function ($data) {
  if ($data['post_type'] ?? '' === 'post') {
    $content = wp_unslash($data['post_content'] ?? '');

    // Find all <img> tags to add the "wp-image-{id}" class
    if (preg_match_all('/<img.*>/im', $content, $images)) {
      foreach ($images[0] as $img) {
        // Find the url in the src attribute
        if (preg_match('/src="([^"]+)/i', $img, $srcList)) {
          // Retrieve the id for this image.
          $postId = attachment_url_to_postid($srcList[1]);
          if (!$postId) {
            // This image isn't in the database, so don't touch it
            continue;
          }

          // Add the wp-image-{id} class if it doesn't exist for this image
          $list = [];
          if (stripos($img, 'class') === false
            || preg_match('/class="([^"]+)/i', $img, $list)) {
            $classes = $list[1] ?? '';
            $hasClassAttribute = (bool)count($list);

            if (!preg_match('/wp-image-([0-9]{1,10})/i', $classes)) {
              // wp-image-{id} class does not exist on this img
              $classes .= ' wp-image-' . $postId;

              if ($hasClassAttribute) {
                // Update the img tag with the new class attribute
                $newImg = preg_replace('/class="[^"]*/', 'class="'.$classes, $img);
              } else {
                // Insert class attribute into the img tag
                $newImg = str_replace('<img ', '<img class="'.$classes.'" ', $img);
              }
              // Replace the original <img> with our updated one
              $content = str_replace($match, $newImg, $content);
            }

          }

        }
      }
    }

    $data['post_content'] = wp_slash($content);
  }

  return $data;
});

Note that the post’s content is slashed as if the old magic_quotes_gpc setting is enabled. This is one example of how long-lived WordPress is, so we need to use wp_unslash() to remove them and then wp_slash() to put them back when we’re done.

As this is for me, I know that I write fairly compliant and consistent HTML, so I’m happy to use regular expressions. If the HTML is really bad, this may not work for you :)

The basic process is:

  • Find all the <img> tags in the content using preg_match_all()
  • Iterate over each one and look for the url within the src attribute and get the id in the database for this image. If the image isn’t in the database, then there’s nothing to do.
  • Find the list of classes in the class attribute. If there isn’t a class attribute then we definitely need to add one. Note that we have to set $list to an empty string before the if statement as otherwise it remains set to the last value for the next time around the foreach loop.
  • Look for wp-image-{id} class within the $classes string and if it’s not there, add it.
  • Update the entire image tag with the updated class attribute
  • Finally we replace the old img tag with the our new one in the $content string.

Once we’re done, we return the $data array with our updated content.

The Sizes attribute

By default, WordPress sets the img sizes attribute to:

sizes="(max-width: 3000px) 100vw, 3000px"

This is for a 3000px image and says that if the window is wider than 3000px, then display the image at 3000px, otherwise, assume that the image takes up 100% of the width of the browser window. (vw means % for reasons.)

For my site, there’s a sidebar that takes up 25% of the width of the window, so a better size attribute in this case would specify 75vw instead. We add a filter to the wp_get_attachment_image_attributes hook to do this:

add_filter('wp_get_attachment_image_attributes', function ($attr, $attachment) {
  $width = wp_get_attachment_metadata(1234)['width'] ?? null;
  if (!$width) {
     return $attr;
  }

  $attr['sizes'] = '(max-width: '.$width.'px) 75vw, '.$width.'px';
  return $attr;
}, 10, 2);

The $attr array contains all the attributes of the image tag and we replace the sizes string with our own version. To get the width of the image, we can use wp_get_attachment_metadata() which contains the width parameter for this image.

All done

We’re all done. Each image in a post uploaded by the XML-RPC API now has the correct wp-image-{id}class on it so that WordPress can add the srcset and sizes attributes. We also have plenty of differently sized image files, so the browser will pick the appropriate one for that will fit within 75% of the size of the window.

With all this work that WordPress does on each render of a page with images, it makes sense to add caching. Personally, I use WP Super Cache, but there’s plenty of choices.

3 thoughts on “Images and WordPress

Comments are closed.