Good way to handle images on websites?

I would love to get some advice on how to handle images in a good way.

I have several websites built with jekyll. Each website utilizes various images. Often times, as I work with a website, I may want to quickly change an image from one resolution to another resolution to see how different it would look, etc. In order to not have to open each image and resave it, I just generate multiple versions of each image and then encode the resolution, quality, etc in the filename. So that I can easily change src=picture-300x300.png to src=picture-900x900.png…

This is how I currently do this:

I have a _images folder with my “source” images in.
Here, I store the actual photographs or other graphical assets that I want to put on the site. This could include the actual RAW images, or PSD files from Photoshop, etc… I then also store a web-format version of the image which would have the same file name but with .jpg, .webp, .png, etc. as extension.

I then have a script to “build” the images. It basically goes through _images/*.{jpg,png,webp,gif,heic} and then uses ImageMagick to generate a lot of versions of each image, including different resolutions (32x32, 64x64, 128x128 for various icons; 100x100, 150x150, 200x200, 300x300, 600x600, 900x900, 1200x1200, 1800x1800, 2400x2400, …) and also different compression quality targets, like 40, 50, 60, 70, 80, 85, 90, 95, which are mostly relevant for jpeg.

Each of the images produced are stored in assets/images/$filename-$resolution-$quality.$extension

This also means that each source images in _images will produce dusins and dusins of output files in assets/images. And only few will be used…

This is of course hugely inefficient. The assets folder explodes in size and number of files and most will never be accessed…

Also, I will have to run my image build script manually every time I add new images…

I am sure there are others who have similar needs and who have solved this in a much better way…

I had the same issue… it was the main reason I converted to Hugo.

Next.js also has its own image component that does it on the fly sort of.

Cloudinary offers a service that does this as an external thing - and I am sure there are others. Jekyll doesn’t have anything built in to do anything with images other than serve them up.

I think that is totally fair that it does not have anything built in. However, I would still like to know how other Jekyll users solve this. How their workflow is, etc.

I think much of this can be accomplished by using Makefiles and ImageMagick.

I found this “Asset pipeline” for Jekyll: GitHub - envygeeks/jekyll-assets: 🎨 Asset pipelines for Jekyll.

It seems like this is an attempt to solve this for Jekyll. However, the project seems to no longer be active; and I could not make it actually work when I tried it. I think the install went fine, but when I tried having it actually handle assets I got all kinds of weird errors that I did not understand.

As I understand it, I should be able to put something like this in my templates:

{% asset img.png vips:resize='100x50' %}

Then jekyll-asset will handle producing img.png in whatever requested size/quality, etc. and replace the above code with the path of whereever the resulting image is put.

This looks really neat. However, when “img.png” needs to be {{ page.image }} it seems to fail.

Neither {% asset page.image vips:resize='100x50' %} nor {% asset {{ page.image }} vips:resize='100x50' %} seems to be accepted.
But maybe that is just me not knowing how to use {{ variables }} inside {% %} blocks?

maybe try page.image without the curlies - not sure but I think I saw something about that once.

I’ve not used that plugin but it was popular several years ago.

For my sites there are not many images and I make sure to optimize them as much as I can at the size I want and then don’t worry about it much. I’ve used place holder images to try different sizes before making my own - like placekitten.

I think the reason I cannot make the jekyll-assets plugin work seems to be related to something with bundle…

I normally invoke jekyll by running jekyll server --port 4006 --watch --livereload --livereload-port 14006 --trace --profile

However, after installing this jekyll-assets, I can no longer start jekyll. I get this error:


jekyll build
/usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:309:in `check_for_activated_spec!': You have already activated eventmachine 1.3.0.dev.1, but your Gemfile requires eventmachine 1.2.7. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:25:in `block in setup'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/spec_set.rb:138:in `each'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/spec_set.rb:138:in `each'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:24:in `map'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:24:in `setup'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler.rb:162:in `setup'
        from /var/lib/gems/3.1.0/gems/jekyll-4.3.2/lib/jekyll/plugin_manager.rb:52:in `require_from_bundler'
        from /var/lib/gems/3.1.0/gems/jekyll-4.3.2/exe/jekyll:11:in `<top (required)>'
        from /bin/jekyll:25:in `load'
        from /bin/jekyll:25:in `<main>'
make: *** [Makefile:16: site] Error 1

jekyll-assets README says to invoke using bundle. I have tried that but it also fails:

$ bundle exec jekyll serve --trace 
/usr/lib/ruby/vendor_ruby/forwardable/extended.rb:29:in `rb_delegate': wrong number of arguments (given 2, expected 1) (ArgumentError)
        from /var/lib/gems/3.1.0/gems/liquid-tag-parser-1.9.0/lib/liquid/tag/parser.rb:34:in `<class:Parser>'
        from /var/lib/gems/3.1.0/gems/liquid-tag-parser-1.9.0/lib/liquid/tag/parser.rb:12:in `<class:Tag>'
        from /var/lib/gems/3.1.0/gems/liquid-tag-parser-1.9.0/lib/liquid/tag/parser.rb:11:in `<module:Liquid>'
        from /var/lib/gems/3.1.0/gems/liquid-tag-parser-1.9.0/lib/liquid/tag/parser.rb:10:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/tag.rb:7:in `require'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/tag.rb:7:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/context.rb:6:in `require_relative'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/context.rb:6:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/filters.rb:5:in `require_relative'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/filters.rb:5:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/env.rb:15:in `require_relative'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets/env.rb:15:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets.rb:21:in `require_relative'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll/assets.rb:21:in `<top (required)>'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll-assets.rb:5:in `require_relative'
        from /var/lib/gems/3.1.0/gems/jekyll-assets-3.0.12/lib/jekyll-assets.rb:5:in `<top (required)>'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:60:in `require'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:60:in `block (2 levels) in require'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:55:in `each'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:55:in `block in require'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:44:in `each'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler/runtime.rb:44:in `require'
        from /usr/share/rubygems-integration/all/gems/bundler-2.3.15/lib/bundler.rb:187:in `require'
        from /var/lib/gems/3.1.0/gems/jekyll-3.9.3/lib/jekyll/plugin_manager.rb:51:in `require_from_bundler'
        from /var/lib/gems/3.1.0/gems/jekyll-3.9.3/exe/jekyll:11:in `<top (required)>'
        from /bin/jekyll:25:in `load'
        from /bin/jekyll:25:in `<main>'

This is how I installed it:

Added these lines to Gemfile:

$ cat Gemfile
source "https://rubygems.org"

gem "jekyll"

gem "jekyll-assets", group: :jekyll_plugins
#gem "jekyll-assets", git: "https://github.com/envygeeks/jekyll-assets", group: :jekyll_plugins
#gem "jekyll-assets", "~> x.x.alpha", group: :jekyll_plugins

Then I ran “bundle install”, and it produces this output if I re run it:

$ bundle install                   
Using concurrent-ruby 1.2.2
Following files may not be writable, so sudo is needed:
  /usr/local/bin
  /var/lib/gems/3.1.0
  /var/lib/gems/3.1.0/build_info
  /var/lib/gems/3.1.0/cache
  /var/lib/gems/3.1.0/doc
  /var/lib/gems/3.1.0/extensions
  /var/lib/gems/3.1.0/gems
  /var/lib/gems/3.1.0/plugins
  /var/lib/gems/3.1.0/specifications
Using i18n 1.14.1
Using minitest 5.20.0
Using thread_safe 0.3.6
Using tzinfo 1.2.11
Using activesupport 5.2.8.1
Using public_suffix 5.0.3
Using addressable 2.8.5
Using bundler 2.3.15
Using colorator 1.1.0
Using eventmachine 1.2.7
Using http_parser.rb 0.8.0
Using em-websocket 0.5.3
Using execjs 2.9.1
Using forwardable-extended 2.6.0
Using extras 0.3.0
Using fastimage 2.2.7
Using ffi 1.16.3
Using rb-fsevent 0.11.2
Using rb-inotify 0.10.1
Using sass-listen 4.0.0
Using sass 3.7.4
Using jekyll-sass-converter 1.5.2
Using listen 3.8.0
Using jekyll-watch 2.2.1
Using rexml 3.2.6
Using kramdown 2.4.0
Using liquid 4.0.4
Using mercenary 0.3.6
Using pathutil 0.16.2
Using rouge 3.30.0
Using safe_yaml 1.0.5
Using jekyll 3.9.3
Using jekyll-sanity 1.6.0
Using liquid-tag-parser 1.9.0
Using racc 1.7.3
Using nokogiri 1.15.4 (x86_64-linux)
Using rack 2.2.8
Using sprockets 4.0.3
Using jekyll-assets 3.0.12
Bundle complete! 2 Gemfile dependencies, 40 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

I have no idea what much of this actually means or how to fix this… I fear I may have broken my ruby/jekyll installation on my machine as it seems it ran some stuff as sudo which was quite unexpected…

I can’t help much as I don’t know anything about jekyll-assets but here are a few comments:

  • in the quote above there are 2 different paths to ruby things, this usually means there is more than 1 ruby installation and it has things is different places that don’t work together. I could be wrong and maybe this is fine for you but usually this leads to failures, uninstalling ruby and re-installing it may work
  • since the jekyll-assets plugin seems to be out of date I think you will cause more issues trying to work with it that it may be worth.
  • if you are really obsessed with the image stuff (not at all a bad thing to be obsessed with) I’d look more into an external service like cloudinary or try a different platform like hugo or next.

just curious, how many images do you have? are you sure you are going to get enough bang for the buck worrying about it? in your original post with the list of sizes I would say you are way overoptimizing them - is there really much difference between a 100x100 and a 200x200 file? even 300x300? I would think you could weed that down to 3 or maybe 4 sizes.

I did use a similar workflow for a while using Gulp initially and then NPM scripts (node) and was processing all assets - css, js and images and that took the load off jekyll and made a decent difference in build times but then jekyll tweaked a few things and got faster so it wasn’t worth the effort.

Thanks for you tips.
Yeah, likely something went wrong with the ruby versions. Likely because I use Debian which ships their own Ruby and Bundle might install stuff from a newer Ruby which likely causes things to be messed up…
By uninstalling and reinstalling ruby, I am not sure exactly how to do that now… Can I somehow ask bundle to cleanup whatever did has done? And then do an “apt purge ruby” to uninstall the debian provided ruby.
Then I can reinstall ruby in Debian only so that things will be (hopefully) working as before?

I am a bit stuck though as I have no idea how to make bundle uninstall whatever it installed. And I am a bit hesitant to experiment with bundle since last time I did that it resulted in this situation…

just curious, how many images do you have? are you sure you are going to get enough bang for the buck worrying about it? in your original post with the list of sizes I would say you are way overoptimizing them - is there really much difference between a 100x100 and a 200x200 file? even 300x300? I would think you could weed that down to 3 or maybe 4 sizes.

Yeah, you are definitely touching a key topic here!

As for actual sizes. This is for the biggest site:

  • Source files in _images: 1.5k files / 2.7 GB
  • generated files in _site/assets: 15k files / 1.6 GB

What I am doing now is NOT a good approach as I generate way too many assets. For example, let say I need some icons, I will then generate a copy of all source images in 16x16, 24x24, 32x32, 64x64. I typically also need some blog images that are typically in 3:2 aspect, so this will cause me to generate copies of all source images in 600x400, 900x600, etc. Then I will have various other design assets such as logos, etc. that are typically 150x100, 200x300, etc. Then I may also have some background images or “hero”/“leaderboard” backgrounds that are large, like 1000x1000, 2000x2000, 3000x3000, etc. but typically very low jpeg quality (like 40 or 50).

On top of that, I may generate both PNG and jpeg/webp versions of each file. And jpegs/webp images even with different quality specifications.

Now most of the generated images are never actually used. It is just that I have a script that runs through all image files in _images and then runs a function call like thumbnail "$image" "$res" $quality, where $image is the source filename, $res is desired resolution like 400x400, $quality is the quality spec like 75.

The thumbnail function is a zsh function that may look like this:

function thumbnail() {   
    src="$1"   
    res="$2" 
    q=$3  
    ext="${filename##*.}"   
    dst="$DST_FOLDER/$(basename "$filename" .$ext)-$res.$ext"   
 
    # Only generate thumbnail if source is newer than dest   
    [ "$src" -nt "$dst" ] && convert -alpha on -background none  -verbose -quality "$q" -resize "$res" "$src" "$dst"  
}

There are some other speciality functions as well. But this is the gist of it.

Now, there are some obvious optimizations possible here, including:

  1. Move different types of images like icons into a specific subfolder in _images and only generate desired resolutions that makes sense for each type of image.
  2. Have a script that tests if an images is actually referenced anywhere inside files in _site, and if not, then it is not used so skip it.

Probably lots of other stuff… But before I start doing all that, I was hoping to learn how others have solved the general problem.

Anyway, I would still very much like to hear how others solved the problem of processing images and other assets for web content.

Hello everyone. I solved this with methods applied from design.
First, I do my best to design pages with the image format in the 4:5 ratio at 320px.
When the page design requires me more horizontal formats, I use images up to 992px and minimum 400px wide. Only the 2 size variations. My layouts do not exceed 1128px wide.
Then, I use the HTML image tag with SRCSET and I only use these 2 variations. And finally, with software like GIMP I can export the 2 sizes quickly, and even export them at once in webp or avif format. This website is an example: aprendamosingles.us. The hero images are avif. And the content images are 4:5 and avif format.
I learned that from reading about mobile first. I design with the mobile experience in mind. I adapt the desktop experience to the main design. Anyway, the avif format is so light, that I don’t worry about Google asking me for the exact size of the image according to the screen. And in my opinion, we are heading that way. Between lighter images, you need fewer versions or sizes for a single image.
I would like to know if you have other methods, besides the programming ones already mentioned.

I think what you are suggesting are indeed good principles to follow regarding design. However, I am not sure I understand how this solves my problem. I already have several websites. Some with hundreds or even one with more than a thousand source image files.

Also, I took at quick look at your site you also seem to have several images that are not using those principles that you suggest. For example:
https://aprendamosingles.us/logo-mini.webp which is used several places and even in several sizes.

But I definitely see your point about limiting the number of resolutions used on a site. And I will try to limit that going forward.

One technical thing that you seem to be doing that I particularly like (but am not sure how it works) is that you might have an image such as this one: https://aprendamosingles.us/img/arlington.avif or this one: https://aprendamosingles.us/img/west-palm.avif

Those are not the same aspect ratio. However, when you display these images on the website, they are seemingly cropped to a fixed aspect ratio. Probably using CSS.
This is what I mean: https://i.imgur.com/ZPjydnn.png

THIS I think would help me a great deal. I would love to learn how you do this. Because with this trick, I might be able to reuse many of my current assets and just cut down on the published resolutions.

So I looked a bit more deeply into your CSS and realized that what you are doing is actually just skewing the image into a specific aspect ratio. I will not be able to use this as skewing the aspect of design assets will not be tolerated by my users.

I think a better approach is something based on these techniques: