Optimise Jekyll Front-matter Images for social media

  9 mins read  

Setting up a blog with jekyll is super easy. It’s also easy to use Azure Pipelines to build Jekyll sites in a continuous integration pipeline. Most Jekyll blog templates are designed to use a large hero image as the feature image at the top of the post. Indeed, the template used for this blog has such a design. Typical of the same templates is to also generate a home page with the posts and their thumnail images in a grid or column layout.

There’s jekyll plugins that generate images with different dimensions, including jekyll-minimagick. I found the documentation is a little lacking for jekyll-minimagick, however the examples in the project’s README gave me enough to work from, to generate images for my homepage thumbnails.

Social media cards

I want to go one step further, and design the site’s posts to be optimised for social media. To start, I want to focus on the Facebook and Twitter cards. For my test case, we have a jekyll site for a podcast I’m working on. I want to ensure whenever a post from the podcast is shared on either of the social media site, the podcast’s name, colour scheme, and logo, is present.

The jekyll-minimagick plugin uses the minimagick ruby library to perform its image manipulation. And the minimagick library in turn uses the ImageMagick command-line tool. Unfortunately, jekyll-minimagick only exposes a sub-set of minimagick’s capability, and some of the image manipulation tasks I want to perform, needs something more.

Optimised image dimensions

I specifically want my social media images to be sized to the optimal dimensions for Facebook and Twitter. While I couldn’t find a definitive guide, trial and error with social media site’s respective card validation tools show the following dimensions are suitable:

  Width Height
Facebook 1080px 570px
Twitter 800px 420px

Overlayed site branding

The social media images should also be branded with the site’s name, logo, and colour scheme. This can be acheived by a super-imposing a strap spanning the full width of the image.

The end result needs to look something like this:

Screenshot of Facebook card

Manipulating images with ImageMagick

The challenge with front-matter images, for most jekyll templates, is their source must be wide enough to span the width of most wide-screen monitors. When sourcing these images, an author typically doesn’t have an extensive image library available. If they’re anything like me, they’ll use DuckDuckGo’s or Google’s image search function, and select the option to search for images that are explicitly marked for free re-use.

Given an image of any width and any height, the simple operation of scaling the image down to the optimised size won’t be enough. The width-to-height ratio of any arbitrary image will not match the required image dimensions of either social media card.

To get to the desired dimensions, three ImageMagick operations need to be completed:

  1. Resize - to meet the minimum dimensions (1080x570px or larger for Facebook, and 800x420px or larger for Twitter)
  2. Composite - take an image with a transparency (a PNG formatted image), overlay it on top of the post’s image
  3. Crop - trim the edges, so the final image is the correct dimensions without any unnecessarily compromised quality

Unfortunately, the jekyll-minimagick plugin does not support all these operations, so instead we have to create our own plugin.

Creating a custom jekyll plugin

There are six different types of Jekyll plugins. The plugin we want to create is of generator type, so it inherits from the Generator class. Like the jekyll-minimagick plugin, it should take the source images from a configured folder, and output the generated images to a configured destination folder.

The new plugin will use functionality from the mini_magick library, so we’ll require that at the top.

To add the new plugin to the jekyll site, we’ll create it as a Ruby (.rb) file called magick-composite.rb, and place it in the jekyll site’s _plugins folder.

require 'mini_magick'

module Jekyll
  module MagickComposite

    class MagickComposite < Jekyll::StaticFile
      # Initialize a new GeneratedImage.
      #   +site+ is the Site
      #   +base+ is the String path to the <source>
      #   +dir+ is the String path between <source> and the file
      #   +name+ is the String filename of the file
      #   +preset+ is the Preset hash from the config.
      #
      # Returns <MagickComposite>
      def initialize(site, base, dir, name, preset)
        @site = site
        @base = base
        @dir  = dir
        @name = name
        @crop = preset.delete('crop')
        @composite = preset.delete('composite')
        @dst_dir = preset.delete('destination')
        @src_dir = preset.delete('source')
        @commands = preset
      end

      # Obtains source file path by substituting the preset's source directory
      # for the destination directory.
      #
      # Returns source file path.
      def path
        File.join(@base, @dir.sub(@dst_dir, @src_dir), @name)
      end

      # Obtains source file path by substituting the preset's source directory
      # for the destination directory.
      #
      # Returns source file path.
      def overlaypath
        File.join(@base, @composite)
      end

      # Use MiniMagick to create a derivative image at the destination
      # specified (if the original is modified).
      #   +dest+ is the String path to the destination dir
      #
      # Returns false if the file was not modified since last time (no-op).
      def write(dest)
        dest_path = destination(dest)

        # return false if File.exist? dest_path and !modified?

        self.class.mtimes[path] = mtime

        FileUtils.mkdir_p(File.dirname(dest_path))
        image = ::MiniMagick::Image.open(path)
        compositeimage = ::MiniMagick::Image.open(overlaypath)
        result = image.composite(compositeimage) do |c|
            @commands.each_pair do |command, arg|
                c.send command, arg
            end
        end
        @commands.each_pair do |command, arg|
            result.send command, arg
        end
        result.write dest_path

        if @crop
          cropimage = ::MiniMagick::Image.open(dest_path)
          cropimage.combine_options do |c|
            @commands.each_pair do |command, arg|
                c.send command, arg
            end
            c.crop @crop
          end
          cropimage.write dest_path
        end

        true
      end

    end

    class ImageOverlayGenerator < Generator
      safe true

      # Find all image files in the source directories of the presets specified
      # in the site config.  Add a MagickComposite to the static_files stack
      # for later processing.
      def generate(site)
        return unless site.config['magick_composite']

        site.config['magick_composite'].each_pair do |name, preset|
          Dir.glob(File.join(site.source, preset['source'], "*.{png,jpg,jpeg,gif}")) do |source|
            site.static_files << MagickComposite.new(site, site.source, preset['destination'], File.basename(source), preset.clone)
          end
        end
      end
    end

  end
end

Next we’ll create a PNG image file with a transparent background, to use as the overlay. This image file will be created at a size of 2400x2400px, and only the bottom 400px has the strap we’re looking to overlay across the bottom. It doesn’t matter where it’s located but for the purposes of this example, it is named 365daysofcloud-socials_overlay.png and located in the jekyll site’s assets/img/2400x2400/ folder.

Finally, we’ll configure the new plugin, by including the following in to the jekyll site’s _config.yml file.
This can be added to the bottom of the site’s existing _config.yml file:

magick_composite:
  social800:
    source: assets/img/source
    destination: assets/img/socials-800x420
    composite: assets/img/2400x2400/365daysofcloud-socials_overlay.png
    resize: "800x420^"
    crop: "800x420+0+0"
    gravity: south
  social1080-square:
    source: assets/img/source
    destination: assets/img/socials-1080x1080
    composite: assets/img/2400x2400/365daysofcloud-socials_overlay.png
    resize: "1080x1080^"
    crop: "1080x1080+0+0"
    gravity: south
  social1080-wide:
    source: assets/img/source
    destination: assets/img/socials-1080x570
    composite: assets/img/2400x2400/365daysofcloud-socials_overlay.png
    resize: "1080x570^"
    crop: "1080x570+0+0"
    gravity: south

Values of the _config.yml file’s resize, crop, and gravity parameters are passed all the way through to the ImageMagick command-line, so the ImageMagick help documentation covers their usage:

  • Resize and Crop - parameters conform to the Image Geometry command-line option
  • Gravity - parameters conform to the Gravity command-line option

In a later post, I’ll describe how to use the images generated by the new plugin in the site’s <meta> tags.