As I recently migrated this blog from Middleman to Bridgetown, I thought I could document one of my Bridgetown customizations: on-demand or build-time image resizing.

Notes

  • In this approach, the originals are stored in the frontend/ directory and should therefore be also accessible via the Bridgetown built-in asset_path helper or through esbuild imports.
  • The resized images are stored in src/images/resized so that they don’t need to be processed by esbuild
  • The resized images contain a digest of the original (CRC32 by default) ensuring that they get reprocessed if the original changes.
    • The CRC hash should be sufficient for detecting changes in the original file in a non-adversarial environment, and it is very fast to compute.
    • The CRC code can be easily replaced with a more robust, though slower, cryptographic hash function if desired
  • On the other hand, I’ve chosen not to add a hash of the generated image to the filename in order to avoid having to keep track of the originals and generated files in a manifest file.
  • I’ve chosen not to commit the generated files to Git, but you might make a different choice based on your needs. If you do push the resized images to version control, make sure to also clean up any versions that are no longer needed.

Prerequisites and assumptions

For this tutorial, I’m assuming the following:

  • Bridgetown v1.3
  • Ruby 3.3
  • ERB templates instead of Liquid
  • You’re somewhat familiar with Ruby and Bridgetown already
  • You should have libvips installed
    • macOS: brew install vips
    • Debian/Ubuntu: sudo apt install libvips

Steps

  1. If you don’t have a Bridgetown site yet, create one:

    bridgetown new --templates=erb image-resizing-demo
    cd image-resizing-demo
    
  2. Add the ImageProcessing gem to the Gemfile:

    gem 'image_processing', '~> 1.12', '>= 1.12.2'
    
  3. Add a test image to frontend/images:

    mkdir -p frontend/images
    # You can, of course, use any image you like as your test subject…
    curl --output frontend/images/blue-marble.jpg \
      https://www.randomerrata.com/_bridgetown/static/blue-marble-T6POMCUA.jpg
    
  4. Create plugins/builders/asset_helpers.rb:

    require "image_processing/vips"
    require "zlib"
    
    # If you want to use a cryptographic digest, you can uncomment following:
    #
    # require "digest/sha2"
    
    class Builders::AssetHelpers < SiteBuilder
      def build
        helper :resized_image_path do |src, width: nil, height: nil|
          basename = File.basename(src, ".*")
          ext = File.extname(src)
    
          # Find images from the frontend directory
          frontend_path = Pathname.new(site.root_dir).join("frontend")
    
          image_path = frontend_path.join(src)
    
          # Calculate a digest based on the original image file
          #
          digest = Zlib.crc32(File.read(image_path)).to_s(16)
          #
          # It's also possible to use a cryptographically secure digest but
          # calculating it will be slower and it's not necessarily necessary for
          # this use case.
          #
          # To use SHA256, uncomment the following and remove the CRC digest line:
          #
          # digest = Digest::SHA256.file(image_path).hexdigest
          #
    
          # Put resized images inside the src/images/resized directory
          static_images_dir = Pathname.new(site.root_dir).join("src", "images")
          destination_dir = Pathname.new(static_images_dir).join("resized")
    
          # Generate the resized filename based on the original filename, digest, and dimensions
          resized_filename = "#{basename}-#{digest}-#{width}x#{height}#{ext}"
          destination_path = destination_dir.join(resized_filename)
    
          # Don't do any processing if the file already exists
          if !destination_path.exist?
            Bridgetown.logger.info "Resizing image: #{src} to #{width}×#{height}"
    
            FileUtils.mkdir_p(destination_dir)
            pipeline = ImageProcessing::Vips.source(image_path).resize_to_limit(width, height)
            pipeline.call(destination: destination_path.to_s)
          else
            Bridgetown.logger.debug "Resized image exists: #{destination_path}"
          end
    
          # Calculate the relative path to the resized image in the src/images folder
          resized_path = destination_path.relative_path_from(static_images_dir)
    
          # Make sure that Bridgetown knows about the resized image
          site.static_files << ::Bridgetown::StaticFile.new(
            site,
            site.source,
            "images",
            resized_path
          )
    
          # Return the path to the resized image
          "/images/#{resized_path}"
        end
      end
    end
    
  5. Add the test image to src/index.md:

    ---
    layout: default
    ---
    
    # Welcome to your new Bridgetown website.
    
    ![Blue Marble](<%= resized_image_path("images/blue-marble.jpg", width: 1024) %>)
    
    ... the rest of the file continues
    
  6. Ignore the generated images in Git by adding resized directory to .gitignore (optional):

    # Ignore generated images
    /src/images/resized
    
  7. Start the Bridgetown server: bin/bridgetown start

  8. You should see the resized image when you visit http://localhost:4000

Screenshot

On-demand image resizing demo