Posted by Will Stocks in Guides, PHP, Snippets on Dec 17, 2018

A topic of contention - should you use PHP or jQuery (or any other of the plethora of JS framework offerings!) to manipulate elements in the DOM? This is something I had to do recently and with the help of some great developers, I managed to utilize PHP's DOMDocument() to achieve the relatively "simple" task of adding extra classes to images!

Why do this?

"Why were you even looking at this" I hear you ask! Well my good friend, I'm a MASSIVE fan of my theme and with that I'm an even bigger fan of the devs that created it!

My current theme makes use of the amazing lazysizes library by aFarkas. However, the images didn't have placeholders so at times there was some "jank". Generally, things were quick enough that this would go unnoticed, but on slower (mobile) connections it was VERY noticeable.

I started reading into lazy loading more and more and found that lazysizes actually has a "blur-up" plugin, which essentially uses a low-resolution placeholder image, with a CSS filter: blur; applied. Once the full resolution image has finished loading in the background, lazysizes then swaps the images with a smooth transition.

So, I reached out to the theme devs. They said it's something they had considered, but ultimately didn't implement due to time constraints. So I took it upon myself to investigate whether it was possible to do!

The starting point

I started with the relatively "simple" regex that was included as part of the theme, which was all well and good, but it felt a bit... old school. Side note: complex regex is WAY beyond my understanding - even simple regex is very confusing to an untrained eye!!

function willstocks_content_images( $content ) {
    global $post;
    $pattern ="/<img(.*?)class=\"(.*?)\"(.*?)src=(.*?)>/i";
    $replacement = '<img$1class="$2 lazyload blur-up"$3data-src=$4>';
    $content = preg_replace( $pattern, $replacement, $content );
    return $content;
}
add_filter( 'the_content', 'willstocks_content_images' );*/

OK, so that works fine... why bother going any further? Well, sometimes I just can't help myself. I fiddle until I can fiddle no more - I love the bleeding edge of tech!

At this point, the guys that develop the theme had a little spare time! We threw a bunch of ideas back and forth. The first idea was to handle the DOM manipulation via jQuery. However I personally wasn't a fan of this idea, simply because it pushes the responsibility client-side. Due to various different devices, processes, browser engines and network restrictions, it was possible that jQuery may not load on all clients and therefore the classes wouldn't be applied! I also wanted full control over the content you guys are seeing - I don't want weird and different experiences, just because you can't load jQuery or have a strange browser or for any other reason.

The next step: Manipulating the DOM via PHP

So I turned back to PHP. The guys supporting the theme were also in agreeance - it is much better to handle this server-side where I knew all possible variables. I can also ensure that all visitors see the same thing, no matter what device or browser!

Before I go much further, let me be honest here - I'm not hugely skilled in PHP or Wordpress functions. I dabble, but I'm no developer. 

On with the show... ?

The actual code!

So the below is the function that between me and my friendly neighbourhood theme devs came up with:

function willstocks_blurup_content_images( $content ) {
    if ( !is_admin() ) {
        global $post;
        $content = preg_replace( '/-([^-]*(\d+)x(\d+)\.((?:png|jpeg|jpg|gif|bmp)))"/', '.${4}"', $content );
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        $dom->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
        libxml_clear_errors();
        $imgs = $dom->getElementsByTagName( 'img' );
        foreach ( $imgs as $img ) {
            $image_url = $img->getAttribute( 'src' );
            $image_id = attachment_url_to_postid( $image_url );
            $new_src = wp_get_attachment_image_src( $image_id, 'willstocks_full_100' );
            $img->setAttribute( 'src', $new_src[0] );

            if ( ! wp_get_attachment_image_srcset( $image_id, 'full' ) ) {
                $img->setAttribute( 'data-src', $image_url );
            }

            $image_classes = $img->getAttribute( 'class' );
            $img->setAttribute( 'class', 'lazyload blur-up ' . $image_classes );
        }
        $content = $dom->saveHTML();
        $content = str_replace( 'srcset', 'data-srcset', $content );
        return $content;    
    }
}
add_filter( 'the_content', 'willstocks_blurup_content_images' );

Explanation of what's happening

We had to make use of libxml_use_internal_errors(true); and libxml_clear_errors(); as for some reason, this function was throwing some error messages on the frontend... I haven't gotten around to clearing that up yet! This little function basically suppresses the errors from the front end. We later clear that set of errors to ensure we're not wasting memory.

One thing I need to do after clearing the errors is include libxml_use_internal_errors(false); to ensure I don't have any knock on effects elsewhere!

Next up, we're grabbing the actual DOM document itself with $dom->loadHTML(); function. You'll notice I have a few extra little bits in there: mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) - the reason for this is that while implementing this function, I noticed I was seeing a whole load of random and strange characters sprinkled throughout my post content. GOOGLE TO THE RESCUE! As it turns out, it was simply a case of $dom->loadHTML(); expecting a certain data format/encoding.

Get the images

Now for the fun bit - this is where we actually make the changes to the image classes! So we've got this piece of code:

$imgs = $dom->getElementsByTagName( 'img' );
        foreach ( $imgs as $img ) {
            $image_url = $img->getAttribute( 'src' );
            $image_id = attachment_url_to_postid( $image_url );

What we're doing here is grabbing <img> element that's listed in the DOM and iterating through each one, grabbing the src value (the URL). The next line was the interesting one that I was really struggling with initially via regex - here we pass that src value through to the Worpress core function attachment_url_to_postid. Per Wordpress' own documentation this function "Tries to convert an attachment URL into a post ID".

Get the low-res placeholder image

The reason we need to do this is so that we can tie that particular image back to its appropriate image sizes, via:

$new_src = wp_get_attachment_image_src( $image_id, 'willstocks_full_100' );
$img->setAttribute( 'src', $new_src[0] );

We're using the wp_get_attachment_image_src core function to pull back the relevant image size (in this case willstocks_full_100 which I've setup in my functions.php as follows: add_image_size( 'willstocks_full_100', 100 ); - so no hard cropping, images will retain their aspect ratio which means no jank!) and storing it as the $new_src variable. We then head back to the original $img that we grabbed the src (URL) from and replace that URL with our $new_src value, which will be the -100x## variant. This will now be the first image loaded when a user visits the page!

Note: I went with a 100px wide image as the placeholder as in my case. It was a low-enough resolution that file size was small (the point of diminishing returns), but also with enough quality that you could just about make out the image content.

Set the original to the lazy loaded image

if ( ! wp_get_attachment_image_srcset( $image_id, 'full' ) ) {
    $img->setAttribute( 'data-src', $image_url );
}

At this point, we're now making sure to ignore "full" sized images, as typically these images are things like GIF's. Now, we set the data-src value to the original src< value (remember, we had $image_url = $img->getAttribute( 'src' ); further up!). This means that our original image now becomes the lazy loaded image, as lazysizes will swap out src and data-src when the images come into the view-port!

Add the new class

So, we've made sure the images are correctly lined up and we know which images we want to add our new class to... so let's do it! 

$image_classes = $img->getAttribute( 'class' );
$img->setAttribute( 'class', 'lazyload blur-up ' . $image_classes );

First up, we need to get and store the existing class attributes of the img tag (which we're doing with $image_classes. Once we've done that, we can use setAttribute to add our new classes to the img tag and then re-add the original classes back on the end!

FINALLY! Return the images with the new classes

THE FINAL STEP! All we need to do now is return the images with the new classes. First up, we need to save everything we've been doing back to our $content variable our original content! For this, we'll use the saveHTML() function.

$content = $dom->saveHTML();
$content = str_replace( 'srcset', 'data-srcset', $content );
return $content;

Next up, to make sure we're only serving the appropriately sized image (because Wordpress serves responsive images as standard!), we're going to make use of PHP's str_replace function to move the img's srcset values (aka Wordpress' responsive images) into the data-srcset attribute. We're saving all of these changes in $content which means there's nothing else to do.

Now, we can return $content;! Et voila... all <img> tags will now show up as (example based on my site):

<img class="lazyload blur-up" src="https://cdn.willstocks.co.uk/image-100x45" data-srcset="https://cdn.willstocks.co.uk/image-420x236.jpg 420w, https://cdn.willstocks.co.uk/image-11x7.jpg 11w, https://cdn.willstocks.co.uk/image-300x169.jpg 300w, https://cdn.willstocks.co.uk/image-768x432.jpg 768w, https://cdn.willstocks.co.uk/image-100x56.jpg 100w, https://cdn.willstocks.co.uk/image-840x473.jpg 840w, https://cdn.willstocks.co.uk/image-21x13.jpg 21w, https://cdn.willstocks.co.uk/image.jpg 1000w" data-sizes="auto">

As you can see, this has all of the responsive image sizes I have setup for my site. The classes are also applied and you can review the source for my site and you'll see that all of the images have the lazyload class. Scroll those images into view and boom - lazyloaded.

Afterthoughts

This was honestly a fun learning experience for me! I got more insight into the intricacies of PHP and what I can do with it. I don't know whether it's the most efficient method for completing this task, but it feels much better than the original regex... at least more supportable!

Also - shoutout to the guys that develop/support my theme, they're honestly great and have helped me out a tonne!!!

If there's a better way to do this (preferably server-side), please let me know if the comments down below! Might even be a slightly more efficient tweak that could be made to the function???