When sharing links from a website to social networks, meta tags are checked for summary data and these are used to build the little preview we see. This includes the image that will be displayed, and I’d like to automatically generate that image using data I already have.

The generated image

Metatags

These are the tags in your <head /> that social networks will scrape when building a preview of your link. I like to use metatags.io to check how my page will appear, as that site displays previews for a number of social networks including Twitter and LinkedIn. The specific tag we care about is og:image, and in full it looks something like this:

<meta property="og:image" content="https://path/to/some/image.png" />

I can use Liquid to generate the tag, but generating the image will be a bit more work.

Approach

Aaron Powell has a great blog post on generating images with Azure Functions. I have a desire for much less moving-parts, and a lot less posts than Aaron, so I’d prefer to generate the images at the same time I have Jekyll generate my static site. To do this, I’ll write a .NET Core Console Application that will:

  1. Extract post metadata from the posts on disk (prior to jekyll)
  2. Build an image for each post with this metadata
  3. Change jekyll generation so it embeds the new og:image content

I want to be able to do this both locally and in a CI/CD pipeline, so I’ll package this image generation into a container image.

Image Generation

The source is here and static void Main takes four arguments:

  1. The directory where my unprocessed posts are
  2. The path to the background image I’ll use
  3. The path to a font to use
  4. The directory to write the rendered cards to

Twitter notes that images must be

an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096 pixels. Images must be less than 5MB in size

and as my wife pointed out, the images will not render on Twitter unless they fit this requirement.

I created a background image in Inkscape and I load this image into SixLabors/ImageSharp and stamp the post title, tags and date on top of it. ImageSharp’s examples repo was a great way to get a quick understanding (copy-paste) of what I needed and its font library means I can pass in any ttf/woff, should I want to change the font in the future.

I wrote a quick-and-dirty post parser which extracts the data I need, and then draw that text onto the background image. The result is a card, that looks like the one at the top of this post, rendered for every post.

Liquid templating

After the images are ready, I need Jekyll to embed them in the html it generates. In _includes/head.html I have the following:

{% assign card = page.path | replace: '.html' '' | replace: '.md' '' | replace: '_posts/', 'assets/cards/' | append: '.png' %}

{% if page.image or page.layout == 'post' %}
<meta name="og:image" content="{{ site.url }}{% if page.image %}{{ page.image }}{% else %}/{{ card }}{% endif %}">
{% else %}
<meta name="og:image" content="{{ site.url }}{{ site.title_image }}">
{% endif %}

The first line determines the name of the image and assigns it to the variable card. I have posts in both HTML and Markdown, so we’ll replace those extensions with .png and prefix the card output directory.

Then, if the page has an image defined, or, is of type post then render a page-specific image path, otherwise use the site’s main image. I retain the ability to override the post’s generated image with a custom image by using {% if page.image %}{{ page.image }}{% else %}/{{ card }}{% endif %}, which means posts like this one display a custom image instead of the generated one.

Stick it in Docker

I don’t like installing dependencies on my local, my pipeline, or any other location I want to run my code, so I’ll put the image generation in a docker container. I already generate the site with Jekyll inside a docker container - no ruby or jekyll installs for me.

I used Tye to build and push the image, the latter of which it doesn’t support yet but I’ll add it soon. The result is an ImageGenerator.ConsoleApp container image on docker hub which I’ll call in my pipeline prior to Jekyll generate:

steps:
- script: docker run -v $(Build.SourcesDirectory):/o staff0rd/image-generator:1.0.1 /o/_posts /o/_assets/post-background.png /o/_assets/Roboto-Regular.ttf /o/assets/cards
  displayName: Generate cards 

Because the code will be running inside a container, we need to mount the current directory as a volume with -v $(Build.SourcesDirectory):/o. Our arguments then use that volume path /o instead of the local path.

Conclusion

Now every post on this site has a default preview image. You can see the one on this page with Right-click > View page source and search for og:image. The code is written in such a way that I can easily change the design of the cards should the site design change. Running the code in a container means the only dependency it has, regardless of which machine it runs on, is Docker. Having the code run with site generation means even new posts will have an image generated for them.

Overall the code required to do this was pretty simple, you can check out the Image Generator Source here.