Automating Jekyll card generation with ruby's Ferrum gem
Tags: jekyll, ruby, ferrum, cdp
Published: 2022-05-12 07:04:45

---
title: Automating Jekyll card generation with ruby's Ferrum gem
published: true
tags: jekyll, ruby, ferrum, CDP
image: /images/cards/2022-05-11-automating-jekyll-card-generation-with-ruby-ferrum.png
---

I've now got a simple, ruby-only, automated way of making [Twitter summary cards](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image) and [OpenGraph image previews](https://ogp.me) for the posts on this blog. Now I just need to iterate on the [currently horrible](/images/cards/2022-05-11-automating-jekyll-card-generation-with-ruby-ferrum.png) version-zero look of them!

Historically you would have done this using javascript calling [Puppeteer](https://developers.google.com/web/tools/puppeteer) driving a headless Chrome instance to generate the screenshot. [Mikkel Hartmann](https://mikkelhartmann.dk/2021/01/17/socal-media-images.html) has a great write up of doing it this way.

Ruby now has the awesome [Ferrum gem](https://github.com/rubycdp/ferrum) which uses the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) to control Chrome, so you can drop the need for JS.

## How I did it
In my [Jekyll](https://jekyllrb.com) 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:

```yaml
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](https://jekyllrb.com/docs/front-matter/). 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](https://jekyllrb.com/docs/collections/) 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`](https://jay.gooby.org/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:

```ruby
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`](https://makefiletutorial.com). Here's a simplified version:

```Makefile
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.