Setting up Image Previews using Puppeteer, Make, and CSS transforms

A picture of a text editor viewed from an angle. The text editor shows incorrect JavaScript async code

Before anything else: I'm making this static site the hard way. I should be using a static-site generator like hugo or jekyll and adding features like this one there. Doing this from scratch means that as I write this, my site's RSS feed is broken on some readers and lacks the content it should on others, footnotes don't work correctly, and the site just doesn't look the way I want it to. Despite all that, I'm glad I'm making this site the wrong way: it's been fun to play with and learn!

One of the features that I wanted to add to this site was image-previews: I wanted a nice preview image to show up when I share a post on slack or twitter. Given that this blog is primarily about programming, I wanted to set up a picture of code from the post... and I didn't want to have to spend seconds manually taking a screenshot of my editor like some sort of savage. I wanted to instead spend hours automating it!

And isn't this all a little vain? Yes! Yes, it is. I want my site to feel pretty, and now it does!

Rendering a nice preview image

A picture of code isn't that visually interesting, so I wanted to jazz it up by putting the code into something that looks vaguely like an editor and then adding a bit of perspective.

I started by creating a file in an editor like this:

-- title: UncaughtError.js
-- codePreview: true

async function throwsError () {
  throw new Error("alas! an error");
try {
} {
  catch (err) {
    console.error("why is nothing caught here?");

I then updated the code I use to render a markdown page into HTML to look for the codePreview tag and render it into a page with custom CSS and with the title and content both wrapped with containing divs. This took care of syntax highlighting and was a good starting point for the rest of the work.

I'm not that comfortable with CSS, but I was still able to muddle my way through setting up appropriately colored boxes for the different pieces of a fake text editor. The tricky bit was adding perspective to it, but thankfully the MDN docs on transform and transform-style were easy to figure out. After playing around with different perspectives, I ended up with:

.editor {
  position: absolute;
  top: 50px;
  left: 50px;
  min-width: 800px;

  transform-style: preserve-3d;
  transform: perspective(1000px) rotateY(30deg);

Taking a screenshot

Thanks to Puppeteer, taking a screenshot of a static page proved simple:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({
    args: [
    ignoreHTTPSErrors: true,
    headless: true,
  const page = await browser.newPage();
  await page.goto('http://localhost:8080/code-preview/index.html');
  await page.setViewport({
    width: 800,
    height: 418,
  await page.screenshot({
    path: process.argv[2],
    omitBackground: true,
    darkMode: true,


With that, I was able to run a command like node ./scripts/screenshot.js where_to_store_the_screenshot.png to create an image for a single post.

Using Make to run commands and minimize work

At this point, I had a good way to render a single image by running a command, but I wanted to set things up so that I could quickly create all of the screenshots for all of my posts and skip work when they were already up to date.

Makefiles support "recipes" where you specify how to make any of a particular type of file. To set up a recipe to create my screenshots, I wrote:

images/previews/%.png: ./code-preview/previews/
	./code-preview/ $< $@

This says:

More concretely, if I make a a markdown file at ./code-preview/previews/ and then run make images/previews/MyPreview.png, make will run ./code-preview/ ./code-preview/previews/ images/previews/MyPreview.png. 1

The ./ script is simple and only depends on having a server running and hosting the .built directory:

./scripts/runTemplating.js "$1" > "./built/screenshot.html"
node ./scripts/screenshot.js "$2"

This is nice, but it still depends on me manually running commands like make ./images/previews/ImageIWant.png, so I set up another make recipe and script to handle that:

for file in $(find ./code-preview/previews -type f); do
  outputFile="images/previews/$(basename $file .md).png"
  make $outputFile

This will attempt to run the make images/previews/ImageIWant.png command for all of the files in the ./code-preview/previews directory. Make will only actually do the work to generate a png if the associated md file has been updated since the last time it generated the png.

(Don't write for loops like this btw! I'm being lazy because I know I won't add spaces to any of my filenames.)

Could this have been simpler? Absolutely! There are probably any number of much cleaner ways to accomplish the same thing, but this works well enough to let me quickly generate all of the images I need.

Using twitter:image meta tags

At this point, it's simple to add these images to posts. Because of how popular twitter is, most platforms seem to look for twitter meta card tags:

`<meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${summary}" />`
${image ? `<meta name="twitter:image" content="${image}" />` : ""}
${image ? `<meta name="twitter:card" content="summary_large_image" />` : `<meta name="twitter:card" content="summary" />`}`

The twitter card validator can help you check whether you've created valid twitter card tags.

And after all that work, sharing links for posts can look slightly fancier!

A screenshot of the slack client with a post from the blog with its accompanying preview