Dynamically Generated Open Graph Images in Jekyll

Creating a plugin to generate og:images

6 Oct 2018

Since moving my this site to Jekyll I’ve been thinking of ways I could leverage all the powers of Jekyll to my advantage, one of those I came up with recently was Open Graph. To find out more about Open Graph itself, please visit the website, this post won’t describe everything you need to know about OG (and its Twitter counterpart.)

I love adding large og:images to my website for sharing on Twitter. I usually craft a nice artwork that will render for each share. For example with my app Atlas Wallpaper, if you tweet the website url (https://atlaswallpaper.app) there is a nice big banner:

I wanted these for my blog posts too, but didn’t want to have to create them each tie by hand, so I set out to make a Jekyll plugin to make them for me. I’ve never used Ruby before and don’t have much experience dynamically generating images so the learning curve wasn’t gradual but I got it done and the og:image for this post is:

How it works:

Firstly, I didn’t want to mess around with ruby gems just yet so I just created the plugin in my site’s _plugins folder. This will need to be rectified in the future, but it works for now. Secondly, I know I wanted to have one on any page, but not every page. So it needed to be called from Liquid (the Jekyll templating engine.) With these two requirements set I went to build it.

module Jekyll
    class OGFilter < Liquid::Tag
    end
end
Liquid::Template.register_tag('og_filter', Jekyll::OGFilter)

I created a class called OGFilter which is a Liquid tag ({% these things %}) interpreter. Now anywhere I type {% og_filter %} in a liquid page a new image will be created and the URL inserted at that point. Pretty neat hey?

Well Jekyll plugins can come in a few flavours, each with its own required definitions/functions/methods. The Tag type must contain:

def initialize(tag_name, text, tokens)

… and …

def render(context)

The first one takes the parameters needed from the tag, and render is where the actual rendering happens. For the simple use case I have, I took no parameters and as such my initialise is just a call to super and nothing else. So render is where the magic happens.

The context parameter is from Liquid and in it you can access any of the same variables you have in your page, for example page and site (and all their related variables.) This makes it really easy to get page attributes for use.

This uses ImageMagick to create the image, and a static background (the drop shadow) created in Sketch. It also uses an sha hash for file names. As such the requirements for this module are:

require 'rmagick'
require 'rickshaw'

And of course, jekyll, which is already included. I take a hash of the page for the filename, get the background image, overlay the text of the title, output that to a folder, and return the URL. It’s pretty simple once you look at it:

def render(context)
    
    
    # This creates an image id hash from the page id in Jekyll
    id = context["page"]["id"].to_sha1

    # Check if the file already exists in the 'opengraph' foldler, return early if it does
    if(File.exist?("#{Dir.pwd}/opengraph/#{id}.png")) 
        puts "File exists #{Dir.pwd}/opengraph/#{id}.png}"       
    else
        
        # Create an image list from ImageMagic using the base image
        img = Magick::ImageList.new("#{Dir.pwd}/assets/artboard.png")

        # Create a caption of the title in a smaller area and center aligned. 
        text = Magick::Image.read("caption:#{context["page"]["title"]}") {
            self.fill = '#D85F46'
            self.font = "SF-Pro-Display-Medium"
            self.pointsize = 50
            self.size = "800x500"
            self.gravity = Magick::CenterGravity
            self.background_color = "none"
        }.first

        # Composite the two images over each other (witht the smaller text image being centred)
        a = img.composite(text, Magick::CenterGravity, 0,0, Magick::OverCompositeOp)

        # Write out the file
        a.write("#{Dir.pwd}/opengraph/#{id}.png")
    end 
    
    # Get the site variable
    site = context.registers[:site]

    # Add the file to the list of static_files needed to be copied to the _site
    site.static_files << Jekyll::StaticFile.new(site, site.source, "/opengraph/", "#{id}.png")

    "/opengraph/#{id}.png"

end

It works (with some hiccups) and now I just need to improve the image to not look terrible. Thanks to the way I can just regenerate my entire site with a button click using Jekyll, once I improve it all the og:images will be improved immediately too—no effort from me.

Let me know of you use this too on your blog!

GitHub Gist: