Note: This post is very long, and it gets into some arcane-looking code stuff. If that’s not your thing, feel free to skip this one!
Since leaving 18F, I’ve spent much of the last month thinking about what’s next for me work-wise. And of course, I’ve been doing no small amount of doomscrolling. But amid all of that, I’ve been remembering how to treat my website like a worry stone. When things get too stressful I’ll spend some time sanding down a few rough edges in the redesign. A tightened-up header layout here, a little more spacing there — doing a bit of design doesn’t fix a single thing about the world, but it gives me a little space to breathe, and to get my feet back under me.
In one recent bout of site work, I spent some time trying to get better Open Graph images working on my site. If you’ve ever shared a link in Slack or on social media, and the post expands out into a lovely preview image, that’s an Open Graph image.1 If a page has an <meta property="og:image" content="image.jpg" />
tag somewhere in its <head>
, then image.jpg
will be shown on link previews on social media sites, and on services like Discord and Slack.
Previously, every single preview image on my site was, well, a big weird photo of my face. Which, my feelings about my face aside, was a little unhelpful! There’s no indication of where you’d land if you followed the link. What site even is this? What’s the page title? These are both valid questions! But instead of answering them, my site was basically just HEY HERE’S A GENERIC PHOTO OF THE AUTHOR.
Ahem. Anyway.
After a bit of sketching in Sketch (shh; shhhhhhh) I came up with a new design for my link previews:
Nothing especially fancy, mind. I thought it’d be nice to pull in some colors and textures from my site’s new design, while showing information about the linked page: namely, its title and, if it’s a blog post, the date it was posted. I also wanted to be able to replace the photo of my face if I needed to: if a page or post specified a custom image for its social media previews, I’d use that instead of my ugly mug.
That was the brief, anyway. After spending about a week on this problem, I eventually settled on a solution using an open source software package called ImageMagick. I’m going to write up what I landed on, mainly as a reference for myself. But if the post is useful to others, well, awesome.
Some initial disclaimers
Before I explain what’s happening here, I want to underline three things.
First and foremost, I’m a novice with both ImageMagick and Eleventy. I’m confident there are much, much better ways to do what I’m doing; in fact, I bet I’m doing some things incorrectly! What follows is simply an explanation of what I’ve gotten working — I am not even remotely suggesting there are any “best practices” at play here. (This website is, as you may know, held together with naught else but twine and anxiety, and I wouldn’t have it any other way.)
Second! I’m afraid to say I can’t provide any tech support for this post. This is partly related to the first point: some of this is still opaque to me, so I’m not confident I can be helpful to you. But I also need to be clear about my limits here. If something isn’t working, I hope some of the resources I’ve linked are useful to you.
And finally, I want to acknowledge there are other — and arguably, much, much better — options for tackling this with Eleventy. There’s an excellent-looking plugin named eleventy-plugin-og-image
that will (you guessed it) generate Open Graph images. I started playing around with it, helped in large part by Robb Knight’s excellent blog post about the plugin. But after experimenting a bit, it didn’t feel like the right option for me. For one, running the plugin on my wizened laptop took quite a long time; but even more importantly, I found myself wanting more visual flexibility than the plugin seemed to offer. That’s why I started investigating other ways to generate the images.
In other words, dear reader: everything that follows, I did to myself.
The ImageMagick basicks
Okay! Let’s dive in.
I’m going to start with where I landed. This is the ImageMagick2 command I’m using to generate one social media image:
magick background.png \
\( foreground.jpg -gravity center -crop 600x630+0+0 -geometry +0+0 +gravity \) -composite \
\( -size 500x216 -background transparent -gravity SouthWest -font YWFTVermont-Light -pointsize 48 -interline-spacing 5 -fill white caption:'The World-Wide Work.' -geometry +652+336 \) -composite \
\( -background transparent -gravity SouthWest -font Untitled-Sans-Bold -pointsize 20 -fill white label:'10 MARCH 2024' -geometry +652+570 \) -composite \
-layers flatten merged.png
A quick editorial sidebar, if you’ll permit me.
ImageMagick is powerful, and it’s fast as hell. It can edit, convert, and even generate images, all from the command line, and all in a matter of seconds. With all of that said, ImageMagick’s been around for well over three decades. In that time it has accumulated a dizzying number of features, many of which feel like entire products in their own right. And to top it all off, I personally found the documentation nigh on incomprehensible, which I assume is because of the software’s age and complexity. It felt like it took me ages to get to a working solution, trawling through forum posts and documentation for code samples I could start editing.
This is all to say that I absolutely see what the code looks like. I realize it looks like a robot’s cry for help.
But! At the end of the day, that behemoth magick
command only does four things:
- First, it’s pulling in that
background.png
image, which will act as the [checks notes; shuffles papers; coughs] background for our social media images. Everything created by the following lines? It gets layered on top of it. - The next line crops
foreground.jpg
down to a specific size before positioning it over the left half of the background image, thereby covering up the photograph of my face. - The next two lines generate boxes filled with type — one for the page’s title and, if we’re on a blog post, another for the date — and renders them with the specified fonts. Those boxes are then positioned over our background image.
- Finally, the last line (
-layers flatten merged.png
) takes all the layers we generated, and smooshes them down into a singlemerged.png
file.
Now, full disclosure: I wrote another thousand words about how this all works, reread it, and realized absolutely nobody needed to read a slightly more in-depth review of all those little flags and switches. Can anyone ever truly know what -crop 600x630+0+0 -geometry +0+0
means?3 Does anyone care about my opinions on using +gravity
as a reset to an earlier -gravity
statement? The answer to both of those questions is “no,” but you already knew that.
That’s why I decided to slam the entire section into a details
element. For any sickos who want to dive into a line-by-line breakdown, here you go.
Here’s the slightly more detailed breakdown that nobody asked for
I know it’s only been a few paragraphs, but let’s take one more look at the whole command:
magick background.png \
\( foreground.jpg -gravity center -crop 600x630+0+0 -geometry +0+0 +gravity \) -composite \
\( -size 500x216 -background transparent -gravity SouthWest -font YWFTVermont-Light -pointsize 48 -interline-spacing 5 -fill white caption:'The World-Wide Work.' -geometry +652+336 \) -composite \
\( -background transparent -gravity SouthWest -font Untitled-Sans-Bold -pointsize 20 -fill white label:'10 MARCH 2024' -geometry +652+570 \) -composite \
-layers flatten merged.png
Phew. Still a lot! But let’s look at what each of those five lines are doing, one by one.
Here’s the first line:
magick background.png
Simple enough. I’m invoking the magick
command, and feeding it the an image as an input. Eventually, this background.png
file will be, well, the background for my social media images. The results of the next few lines are going to be layered on top of it.
With that said, let’s create our first layer! And it has an important job to do: namely, some of my posts have custom social media images. When and if they do, I want to load in that custom image, and drop it into place over the left-hand side of the background image — effectively covering up my photo in the background.
Here’s what that looks like in practice:
\( foreground.jpg -gravity center -crop 600x630+0+0 -geometry +0+0 +gravity \) -composite \
Oh my goodness, sorry, I know: it’s a lot. But let’s see if we can decipher this a bit.
Within those parentheses, we’re grabbing foreground.jpg
as another input image, and then performing a series of ImageMagick operations on it. In each step, we’re using those operators — gravity
, crop
, geometry
, and composite
to modify the image as we go. And as Greg Dunlap kindly helped me understand when I was first researching this, those commands are run in the sequence they’re specified: do this, then this, and then this.
Again, I admit that this one line looks like a lot: ImageMagick’s syntax is, frankly, arcane. But amid all the wild punctuation, this line does four things:
- First, it crops
foreground.png
down to 600px by 630px, relative to the center of the image. (-gravity center -crop 600x630+0+0
) - Next, it positions that cropped image at the top- and leftmost corner of our
background.png
image. (-geometry +0+0
) And because we’ve already sized it to 600×630 dimensions, it perfectly covers up my ugly mug. +gravity
doesn’t modify the image. Rather, it’s a kind of reset, one that prevents some of our edits from spilling over into some of the lines below it.- Finally, the
-composite
operator signals that the results of everything inside the parentheses should be treated as one single image layer.
Here’s what it looks like so far:
So! With our first line deciphered, let’s move onto the next:
\( -size 500x216 -background transparent -gravity SouthWest -font YWFTVermont-Light -pointsize 48 -interline-spacing 5 -fill white caption:'The World-Wide Work.' -geometry +652+336 \) -composite \
As you might’ve guessed, this generates the big serif’d headline displayed on our images. We’re using a similar structure as we did in the previous line: ImageMagick will perform all of the operations inside the parentheses and then — thanks to our closing -composite
 — treat the result as a single layer to be stacked onto our background.
Inside the parentheses, things look a little different from the preceding line. We’re not cropping existing images; instead, we’re using ImageMagick itself to draw something onto our layer.
-size 500x216 -background transparent
creates a 500px by 216px canvas with — you guessed it! — a transparent background.-gravity SouthWest
aligns whatever’s inside that canvas to its bottom left corner. (The “southwest” corner. Get it? đź§)- The heart of this line is the
caption
command, which draws some text inside the canvas we specified. All the operators that precede it are passing along typesetting instructions for how that text should render: choosing a typeface (-font YWFTVermont-Light
), a font size (-pointsize 48
), leading (-interline-spacing 5
), and a color. (-fill white
) - And finally, we’re using
-geometry +652+336
to define the pixel coordinates for our new layer, positioning it relative tobackground.png
’s dimensions.
And here’s how things are shaping up:
A quick note here: the caption
interface allows you to render a string of text, and to have it reflow automatically within a given box. However, it can only apply one font to a given string — so if your page title has, say, a book title that you’d like to italicize, you’re outta luck. There is an alternative to caption
called pango
, which does allow for more text formatting options. However, I couldn’t figure out how to get it to bottom-align text to its container, which caption
does do quite nicely. So I’m sticking with caption
for now, even though I lose the italicized text.
(If you were able to read that paragraph without having your eyes start to cross, you’re doing much better than I did.)
Anyway! Two layers done, one more to go! Here’s the next line in our monster command:
\( -background transparent -gravity SouthWest -font Untitled-Sans-Bold -pointsize 20 -fill white label:'10 MARCH 2024' -geometry +652+570 \) -composite \
This one might feel familiar to you. Just like the line preceding it, we’re generating some text here for our blog post dates, and giving ImageMagick some instructions for how and where it should be typeset. The main difference here is that instead of caption
to render our text, we’re using label
, which is a different (and simpler) interface for generating type.
Now that we’ve generated our label, we turn to our final line:
-layers flatten merged.png
That line tells ImageMagick to take all of the layers generated by the previous lines — the background image, the optional foreground image, the text layers for our page title and our date — and to flatten
them into a file named merged.png
. And here’s the final result:
Now that we’ve written that command, our work’s finally done.
…well, okay, no. It’s not really done.
Gotta keep it automated
I’ve created one ImageMagick command to create one single image. But I now need to transform it into a template: one that can automatically create OpenGraph images for every page on my website, and then make those images available to Eleventy.
If you’d like to see it, here’s the code for the solution I landed on. And here’s a quick rundown of what’s happening.
- First, I defined a new collection in my Eleventy configuration file, one that collects all of the public pages on my site.
- That collection’s used in an Eleventy template that generates a CSV file containing information about each of those pages, which will be passed along to ImageMagick: the title of the page; whether or not it contains a custom social media image; and whether or not the page is a blog post. (And therefore, whether or not it should show a date on its OpenGraph image.)
- I wrote a node script that gets the data for each page from that CSV file, and passes each page’s information along to a custom ImageMagick command.
- Finally, I wrote some code that’s included in my main Eleventy template. It uses a handy filter originally written by Peter deHaan to check if there’s an OpenGraph image that matches the current file’s name. If there is a corresponding image, then it’s linked in the page’s
<head>
, which causes the image to appear in social media previews.
I’ll note that this is a slightly streamlined version of the workflow. In order to guarantee that every OpenGraph image’s filename was unique, I referenced some work of Bob Monsour’s to append md5 hashes to the generated images; I’m also using sharp to compress the images a bit. I’ve stripped all that code out in order to simplify the examples — and to, well, make a long story a little less long. I’ll also say that I’m the last person who’d describe this workflow as elegant. It requires Eleventy to build the CSV file before the node script can run, which then makes the files available to Eleventy.
So full disclosure, I often think the presence of subheadings on my blog entries means I should have done a few more editing passes
But! I’m still really happy with what I managed to get working, imperfect though it is. Besides, this is just the current state; I’m sure it’ll change as I learn more about these technologies. And for now, this setup feels like it works well for my little website: I’ve got some nice customization options, it’s very fast, and I’m delighted with how the new images look.
Anyway, whew. Thanks so much for reading through all of that — and thanks, as always, for reading.
Footnotes
There used to be competing standards for these images, namely Facebook’s Open Graph specification and Twitter’s own “Cards”-style previews. But since Twitter turned into a literal Nazi bar, I don’t worry anymore about making previews for that platform. Fascists don’t deserve pretty links. ↩︎
This post assumes you’re running ImageMagick 7, and not the “legacy” ImageMagick 6. I haven’t tested my work on the older syntax. (Yes, there are two versions of ImageMagick; yes, both are still available; yes, this made searching for support…challenging.) ↩︎
I mean, after all this research, I kinda do. But I wouldn’t recommend the experience to anyone else. ↩︎
This has been “Magick images.” a post from Ethan’s journal.