Generate Open Graph images with Jekyll

I never cared about social media sharing on my website until a few month, that can be one of the reasons why I never get any share but anyway. A few months back I added some Open Graph metas but it still felt uncomplete because when I was sharing a post in a toot there was no image.

A mastodon toot sharing a blog post without Open Graph image

I decided that I wanted to have an image for every posts, so the only solution was to create an automatic generator for those images. Why ? Because I don’t want to spend any time searching for illustration images for every posts like I did in the beginning of the blog (images that I removed because I didn’t like them years after choosing them).

What I wanted

The generation of images MUST be run locally on my Mac, so that generated Open Graph images can be stored in my git repository. The main reason is that I want to keep my CI build time fast, thus I want to put any required dependency as dev dependency in my GemFile to keep build time low.

I want the images to be simple and to match the colors of my light theme and I want a simple design (yeah I already said simple), so I came up with this :

A mastodon toot sharing a new post on my site without Open Graph image

This was the very first draft, with that I was able to start the images generation and finally I thought that the second line was not that necessary. My name is already displayed below and it’s not what I want to be seen, I want the title to be visible. The final reason to drop it was because in some cases I have titles that are so long that there was not enough space to display both the title and my name. So to keep things simple I dropped the line with my name.

Another thing I wanted was to have lightweight images as I don’t want to bloat my git repository with MB of images, hopefully generating simple images will only add a few MB to my repository and will not overweight my docker images.

How do I generate my Open Graph images

To generate the images I heavily inspired myself from a post «Automatically generating Github-like og:images in Jekyll». I took his code but I simplified it a lot as I directly generate the HTML to create the image in /tmp before optimizing it and finally copying it in my images folder. Copying the file in /tmp allows to optimize the image first then Jekyll will copy the image to _site only once (which is not the case in the original plugin).

Here is the code that is generating my Open Graph images.

require 'fileutils'
require 'imgkit'

module Previews
  def self.process(site, payload)
    begin
      # Ensure the destination directory exists
      FileUtils.mkdir('./images/previews')
    rescue
    end

    # Loop through all the posts
    site.collections['posts'].docs.each do |p|
      slug = p.data['slug']
      tmp_img = "/tmp/#{slug}.png"
      src_img = "./images/previews/#{slug}.png"

      # Skip if there is already an existing image.
      # To regenerate a preview image, simply delete the image in the destination folder
      if !File.exist?(src_img)
        puts "\n  Creating preview: #{slug}.png"

        # HTML for generating a @2x image of 1200x530 image at 100 quality
        # Setting the charset is helpful when you have accents in your posts title
        kit = IMGKit.new(
          "<!DOCTYPE HTML>
          <html>
            <head>
              <meta charset='utf-8' />
            </head>
            <body>
              <h2>#{p.data['title']}</h2>
            </body>
          </html>",
          zoom: 2,
          quality: 100,
          width: 1200,
          height: 630
        )

        # Attach the local stylesheet for wkhtmltoimage to pick up
        kit.stylesheets << './assets/css/preview.css'

        # Save the image to the previews directory
        kit.to_file(src_img)

        # Optimize to reduce the size to about a third
        `pngquant #{src_img} -o #{src_img} -f`
      end
    end
  end
end

# Add a hook that's run after html is written
Jekyll::Hooks.register :site, :post_write do |site, payload|
  Previews.process(site, payload)
end

Do not forget to create a CSS file where you put you style, or you’ll end up with a white image. For my images, I used a very simple CSS File :

html {
  font-size: 16px;
}

body {
  background: #0275d7;
  width: 600px;
  height: 330px;
  position: relative;
  margin: 0;
}

h2 {
  color: #fff;
  font-size: 2.5rem;
  font-family: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
  margin: 0 50px;
  padding: 0;
  position: absolute;
  bottom: 90px;
  right: 50px;
  text-align: right;
}

The plugin that generate the image is run every time a post is written on the disk into the _site folder.

The first run after adding the plugin can be quite long depending by how many posts you have and how complicated the HTML you use for the generation is. But once every images is generated, your build time should come back close to its previous duration. The only difference is that now, when you’ll create a new blog post an image will be generated automatically. And you’ll just need to add that image with the post when commiting a new post.

Optimizations

Before deploying it live I did some tests. I wanted to know if generating @2x images with various qualities would have an impact on the total images weight, I wanted to know if pngquant was necessary, is PNG the right format… So I run the generation of all images multiples times and here is what I got :

JPG Image generated in 600x315 at 75 quality without optimization :

JPG Image generated in 600x315 at 75 quality optimized manually with ImageOptim :

PNG Image generated in 600x315 at 75 quality without PngQuant :

PNG Image generated in 600x315 at 75 quality optimized by PngQuant :

PNG Image generated in 1200x630 at 100 quality optimized by PngQuant :

Those values apply for my HTML and my settings, but it’s easy to see that generating PNG images and optimize them with pngquant is really worth it even if it adds another external dependency that you can easily install on Mac with brew install pgnquant. After generating @1x or @2x, I notice that it didn’t have a huge difference so I picked @2x size.

Every generated image weight between 10kB and 20kB, the longer the title of the post is the heavier is the image.

Final notes

In the end, this is what is displayed when I share a post on mastodon.

A mastodon preview with generated Open Graph image

I think I reached my goal as every post as now an image and the total weight of all images is around 3.2MB. I manually processed images throug ImageOptim to reduce a little the quality and optimize the size of images a little more.

I’m pretty with the result, if I want to change the design or something else I just have to change my HTML/CSS and regenerate all images, it’s only a matter of a few minutes.

And finally a huge thanks to Ognjen Regoje for the intial post.