Ognjen Regoje bio photo

Ognjen Regoje
But you can call me Oggy


I make things that run on the web (mostly).
More ABOUT me and my PROJECTS.

me@ognjen.io LinkedIn

Automatically generating Github-like og:images in Jekyll

#jekyll #seo

Github generates social images for repositories automatically. This post is about how to accomplish a similar thing for blog posts written in Jekyll.

The Github version looks like this:

Front matter editor Github image

I quite like it. It’s simple. It shows the description, the languages, popularity. A nice summary. That’s what I took as a point of reference for generating previews.

This is what the Twitter card for this post looks like:

Still needs a bit of fine-tuning, but much better than not having an image at all.

The instructions here are for a blog running on Jekyll. If you want to replicate it on another platform the general steps are:

  1. Generate html containing only the post preview

    And optimize the display of that page to 600px width

  2. Use wkhtmltoimage to capture 600x315 screenshot of the preview
  3. Copy the screenshot to the generated site directory
  4. Set the meta tags

Generate html containing only the post preview

I wanted the previews to be automatically generated based on a flag I set in the front matter of the post.

First, I created the _previews folder, that’ll hold the collection. I added that folder to .gitignore.

Next, I created previews.rb in the _plugins directory. This script copies any posts with auto_image set to true in the front matter whenever files are read.

require 'fileutils'

module Filter
  def self.process(site, payload)
    site.collections['posts'].docs.select{|x| x.data['auto_image']}.each do |post|

      # Set the path where the copied content will live
      path = './_previews/' + post.data['slug'] + '.md'

      # Copy the content of the post to the preview collection
      File.write(path, File.read(post.path))

      # Create a new document in the preview collection
      preview_doc = Jekyll::Document.new(
        path,
        {site: site, collection: site.collections['previews']}
      )

      preview_doc.read

      # Set the layout to preview
      preview_doc.data['layout'] = 'preview'

      # Add document to the collection
      site.collections['previews'].docs << preview_doc

    end
  end
end

Jekyll::Hooks.register :site, :post_read do |site, payload|
  # If the site is being served locally
  # skip generating previews
  # Otherwise there'll be an endless loop of previews being
  # written and regenerated
  if !site.config['serving']
    Filter.process(site, payload)
  end
end

Because Jekyll would load the files already in the previews directory before this step, it would result in warnings where two documents in the collection contained the same file. To handle that I added a hook that clears the directory.

# At the bottom of previews.rb

module RemovePreviews
  def self.process(site, payload)
    FileUtils.rm_rf("./_previews/.", secure: true)
  end
end

In order for these files to be picked up, the collection must be added to config.yml:

collections:
  previews:
    output: true
    permalink: /previews/:slug/

To render just the preview, I created a preview.html in the layouts directory. I just took the post.html layout and removed everything but the picture, name, title and excerpt. I also adjusted the css a bit to make it fit within a 600x315 container.

That creates the preview of each post. Here’s the generated preview for this post..

Capture the screenshots and copy them to the generated site

Next, I used the imgkit gem to take screenshots of the previews.

This requires adding imgkit and wkhtmltoimage-binary to the Gemfile and then bundle install.

# top of previews.rb

require 'imgkit'

# bottom of previews.rb

module Previews
  def self.process(site, payload)
    begin
      # On first run it's necessary to create the previews
      # directory in the generated site
      FileUtils.mkdir('./_site/assets/images/previews')
    rescue
    end

    # Loop through all the previews
    site.collections['previews'].docs.each do |p|
      slug = p.data['slug']

      # If the image already exists skip,
      # in order to speed up generation
      # To regenerate the preview, delete the file
      if !File.exists?('./images/previews/' + slug + '.png')

        # Read the generated html for the preview
        # And set imgkit up for generating a
        # 600x315 image at 75 quality
        kit = IMGKit.new(
          File.read('./_site/previews/' + slug + '/index.html'),
          quality: 75,
          width: 600,
          height: 315
        )

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

        # Then save the image to the previews directory
        kit.to_file('./images/previews/' + slug + '.png')

        # This step requires pngquant
        # It removes color depth from images and reduces their
        # size to about a third
        `pngquant #{'./images/previews/' + slug + '.png'} -o #{'./images/previews/' + slug + '.png'} -f`

        # And copy it to the generated site
        FileUtils.cp(
          './images/previews/' + slug + '.png',
          './_site/assets/images/previews/' + slug + '.png'
        )
      end
    end
  end
end

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

  # Check if the site is being built or served locally
  if !site.config['serving']
    Previews.process(site, payload)
  end
end

Note that imgkit is basically a wrapper around wkhtmltoimage so this step can be done manually through sh as well.

Set the meta tags

In the post.html layout, change the head section to pick up automatically generated previews if the featured image is not set.

{% if page.image.feature %}
<meta name="twitter:card"
  content="summary_large_image">
<meta name="twitter:image"
  content="{{ site.url }}/images/{{ page.image.feature }}">
{% elsif page.auto_image %}
<meta name="twitter:card"
  content="summary_large_image">
<meta name="twitter:image"
  content="{{ site.url }}/assets/images/previews/{{ page.slug }}.png">
{%endif%}

{% if page.image.feature %}
<meta property="og:image"
  content="{{ site.url }}/images/{{ page.image.feature }}">
{% elsif page.auto_image %}
<meta property="og:image"
  content="{{ site.url }}/assets/images/previews/{{ page.slug }}.png">
{% endif %}

If the post has the image.feature attribute set prefer that image. Otherwise set the generated preview.