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:
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:
-
Generate html containing only the post preview
And optimize the display of that page to 600px width
- Use
wkhtmltoimage
to capture600x315
screenshot of the preview - Copy the screenshot to the generated site directory
- 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.