Responsive images inside of post content

#1

Problem

When running performance tests on post pages, the images are a major problem. Even on mobile devices a full size image is being loaded.
This quickly becomes a lot of data that does not need to be loaded.

Current status

Inside the post context only {{content}} is available, which returns all HTML for the content. There is no way to use responsive images here.
Also no image resizing happens when uploading the pictures via the editor, so no custom solution is possible, such as injecting srcset attributes into the content via JavaScript.

Solution

Include responsive images, such as described here, for post content. This will need automatic conversion of images as soon as they are included into the post via the editor.

Some blogs/websites might not want to have this feature enabled, so it should be possible to toggle it via the settings.

TODO

  • Add setting to enable post image optimization.
  • When a picture is added to a post, automatically resize according to the sizes set in the themes package.json.
  • Outputting post content should include the responsive img HTML

Id be more than happy to help out with this, Id only need some guidance on where I can find the right places in the code.

This is one the major roadblocks for me to achieve a highly performant blog, which is a personal project in order to have a proof of concept for a ressource friendly blog with Ghost (similar to https://sustywp.com/)

Responsive images inside of posts (or how to score 100 on Lighthouse performance)
Reusing images and avoiding multiple uploads of the same file
#2

This isn’t strictly true, Ghost has a separate service for resizing images that is accessed by specifying the image size in the URL (limited to sizes configured in the theme):

The same principle exists for all images uploaded in Ghost, whether it’s feature images or content images. You can build up your own srcset using those urls in a HTML or markdown card, eg:

<img class="post-image"
    srcset="/content/images/size/w300/2019/04/17/image.jpg 300w,
            /content/images/size/w600/2019/04/17/image.jpg 600w,
            /content/images/size/w1000/2019/04/17/image.jpg 1000w,
            /content/images/size/w2000/2019/04/17/image.jpg 2000w"
    sizes="(max-width: 1000px) 400px, 700px"
    src="/content/images/size/w600/2019/04/17/image.jpg"
    alt="{{title}}"
/>

Note the /size/{size}/ part of the URL, that directs the request to the image resizing service which will generate the resized imaged on the fly and store it locally for faster subsequent requests.

The reason srcset output hasn’t been implemented for content images yet is due to problems with the current design where only images sizes specified in the theme can be generated to avoid opening up DoS attacks. Content is static so if you change theme or adjust the allowed image sizes to cater for a new design then you may end up with image sizes referenced in your content that can no longer be dynamically generated.

We need to come up with an alternative method for specifying generally allowed image sizes, then a plan for how old content could be migrated to use dynamic image sizes (if that’s possible). There also need to be considerations made for how the new feature would work inside markdown cards, again if that’s possible, it may need to be limited to image cards.

It’s something the core team is thinking about but we want to make sure the implementation is correct this time :wink:

2 Likes
#3

Thx Kevin!

I now have a solution like this:
post.hbs

{{#post}} 
  <section class="post__content">
    <noscript>
      {{content}}
    </noscript>
  </section>
 
  <script src="{{asset "built/post-content.js"}}"></script>
  {{/post}}

This will load all content normally when JavaScript is disabled and performs the manipulation to use responsive images when JavaScript is enabled with the following code:

post-content.js

function generateResponsiveImage(url) {
  const urlSplit = url.split("content/images");
  const start = "/content/images/size/";
  const end = urlSplit[1];
  return `srcset="${start}w100${end} 100w,
            ${start}w300${end} 300w,
            ${start}w600${end} 600w,
            ${start}w1000${end} 1000w"
    sizes="(max-width: 1000px) 400px, 700px"
    src="${start}600w${end}"
    `;
}

function improvePostPerformance() {
  const contentSection = document.querySelector(".post__content");
  let content = contentSection.querySelector("noscript").innerHTML;

  const images = content.match(/<figure.+?figure>/g);
  const updatedImages = images.map(image => {
    const src = /src="([^"]*)/.exec(image)[1];
    if (src.split("/")[1] === "content") {
      return image.replace(/src="[^"]*"/, generateResponsiveImage(src));
    } else {
      return image;
    }
  });

  images.forEach((image, index) => {
    content = content.replace(image, updatedImages[index]);
  });

  contentSection.innerHTML = content;
}

improvePostPerformance();

It works well, even though it is probably not the most elegant solution.
I will follow this up with a blog post soon :slight_smile:

Thanks for the help on this.

Regarding your problem: Would the image resizer not automatically create the new image sizes when changing the theme as long as the original image is present?
Might be a feature that could slowly transition from opt-in to opt-out on the post page?

Good luck with this.

2 Likes
Responsive images inside of posts (or how to score 100 on Lighthouse performance)