I've now got a simple, ruby-only, automated way of making Twitter summary cards and OpenGraph image previews for the posts on this blog. Now I just need to iterate on the currently horrible version-zero look of them!
Historically you would have done this using javascript calling Puppeteer driving a headless Chrome instance to generate the screenshot. Mikkel Hartmann has a great write up of doing it this way.
Ruby now has the awesome Ferrum gem which uses the Chrome DevTools Protocol to control Chrome, so you can drop the need for JS.
How I did it
In my Jekyll install I've got a _cards
symbolic link to my _posts
directory, and in my _config.yml
I've got a defaults
section that looks like this:
defaults:
-
scope:
path: "_posts"
values:
layout: post
-
scope:
path: "_cards"
values:
layout: card
You need to do this so your posts don't specify layout: post
in their individual frontmatter. If they did, this would override the layout: card
that we need the cards
collection to use, and we'd just end up with a screenshot of the entire page.
So there's now a cards
collection that mirrors my posts, but that render with their own _layouts/cards.html
layout. I use this to generate the post image from.
Because of the symbolic link, every _post
entry like _posts/2022-05-11-automating-jekyll-card-generation-with-ruby-ferrum.md
will have a corresponding /cards/2022-05-11-automating-jekyll-card-generation-with-ruby-ferrum.html
page when you run jekyll serve
or jekyll build
. It's these pages we'll point Chrome at to generate the images.
After adding the Ferrum gem to my Gemfile
, I can use this ruby to make images for my posts:
require "Rubygems"
require "Ferrum"
def generate_card(browser, card, png, options={})
browser.go_to("http://localhost:4000/cards/#{card}")
# see all the options here https://github.com/rubycdp/ferrum#screenshots
browser.screenshot(path: "./images/cards/#{png}",
full: true,
# final image size is window_size x scale
scale: 2)
end
browser = Ferrum::Browser.new(window_size: [800, 418])
# Check what cards we need to make
Dir.glob("_posts/*").each do |post|
post = File.basename(post, ".md")
png = post + ".png"
card = post + ".html"
generate_card(browser, card, png) unless File.exists?("./images/cards/#{png}")
end
This will use your local version of the jekyll site to create the screenshots from. To ensure it's running, and for a bunch of other tasks, I use a Makefile
. Here's a simplified version:
default: deploy
clean-site:
# Do this step to ensure we don't accidentally publish drafts etc
rm -rf _site
# JEKYLL_ENV=production to avoid localhost urls in your jekyll-seo-tag links
serve-site:
JEKYLL_ENV=production jekyll serve & echo $$! > /tmp/jekyll.pid && sleep 15
build-cards: serve-site
ruby ./scripts/generate-cards.rb
kill -9 $(shell cat /tmp/jekyll.pid)
rm -f /tmp/jekyll.pid
deploy: clean-site build-cards
rsync --progress --delete -a -e ssh _site/ gooby.org:~/jay.gooby.org/public
The deploy
recipe cleans the site, serves it, generates the images using the ruby script, then "deploys" the site by copying it to my remote host with rsync
. Because it's the default recipe, I can just call make
in the root of my project to get the site live.
Top comments (0)