Create Social Image Using Puppeteer

October 10th, 2020

You need something for og:image, but you don't want to look for a random image from unsplash that is barely related to your post. Let's automate it. So my plan is this.

  1. I will make a page like https://eunjae-stuff.vercel.app/blog-image.html?title=xxx&description=yyy​ and let it render what the image will look like.
  2. I will use puppeteer to render it and take a screenshot.

Template

I've searched a bit, and it seems like 1200x628px seems to be a recommended resolution for Twitter card. To play with the template, I used CodeSandbox, and this is my sandbox:

https://codesandbox.io/s/social-image-playground-so1ml?file=/index.html

When building the sandbox,

  • I didn't want to use any bundler.
  • I just created an empty HTML file.
  • I wanted to style it easily. I used TailwindCSS.
  • I just used their CDN version not to use a bundler. Its size is quite big, but it doesn't matter here.
  • I used Google Fonts.

I uploaded this HTML file and a profile image file to Vercel. You can use Netlify, too.

To make this responsive to the query parameters, I've made a little change:

<p id="title" class="text-orange-100 font-title leading-tight"></p>
<p id="description" class="text-orange-100 font-subtitle"></p>
...
<script type="text/javascript">
const params = new URLSearchParams(window.location.search);
["title", "description"].forEach((key) => {
document.getElementById(key).innerText = params.get(key);
});
setTimeout(() => {
window.done = true;
}, 200);
</script>

I'll explain about window.done, although I guess you've already noticed what it's for.

Puppeteer

When you create an instance of puppeteer, you can do the following:

const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();

But if you're going to run this on the server-side, then it should change to:

import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'
const browser = await puppeteer.launch({
defaultViewport: null,
args: chrome.args,
executablePath: await chrome.executablePath,
headless: chrome.headless,
})

So here's the full function:

1import chrome from 'chrome-aws-lambda'
2import puppeteer from 'puppeteer-core'
3import tempfile from 'tempfile'
4
5async function createImage(title, description) {
6 const browser = await puppeteer.launch({
7 defaultViewport: null,
8 args: [...chrome.args, `--window-size=2560,1440`],
9 executablePath: await chrome.executablePath,
10 headless: chrome.headless,
11 })
12 const page = await browser.newPage()
13 page.goto(
14 `https://eunjae-stuff.vercel.app/blog-image.html?title=${encodeURIComponent(
15 title
16 )}&description=${encodeURIComponent(description)}`
17 )
18 await page.waitForFunction(`window.done === true`)
19 const filePath = tempfile('.png')
20 await page.screenshot({
21 path: filePath,
22 clip: { x: 0, y: 0, width: 1200, height: 628 },
23 })
24 await browser.close()
25 return filePath
26}

Here are some remarks:

  • At line 8, we only need 1200x628, but considering the UI part of the browser like URL bar, tabs, etc. I just put large numbers to avoid scrolling.
  • At line 18, puppeteer sometimes took a screenshot even before the script set the title and description. That's why I put a setTimeout and set window.done = true. Here puppeteer waits for it and takes a screenshot.

It's up to you how to use it. I created a serverless function that generates an image and uploads it to my CMS service (sanity.io). If you have a Gatsby blog with mdx, you can have a small script to run this function and put the generated image into your folder.

I'm not good at design, so I'd like to see how you all are doing with your templates. Show me yours. I'm interested. Leave a comment in the tweet ⬇️


I'm Eunjae -

A software engineer
focused on web development.
I'm working at Algolia, in Paris.
Feel free to connect!

© 2020 Eunjae Lee