10. Generating images on the fly using puppeteer

August 14, 2018

I wrote this post for my newsletter, sign up here to get emails like these every week.


With stackstickers, we tried doing a fun promotion thing when you order. We ask you to tweet out your order to share it with your friends. You know standard shopping stuff, nothing fancy about that.

What’s fancy is the preview image that comes in your tweet. This isn’t just any image, it’s the image of the stickers you order!

People really enjoyed that, I’d like to believe it made others feel like getting stickers too.

The way twitter adds images for shared links is interesting…

tweet

When you share a link, twitter looks for a meta tag with name="twitter:image" and fetches the url from the content field.

<head>
  <meta name="twitter:image" content="image_url" />
</head>

To supply this image, my first instinct was to create all possible combinations and save them on our servers.

When twitter asks our servers for the image, we just serve it statically, something like stackstickers.shop/images/react-vue-angular-javascript-preact.png

That should work, right?

We started with 18 options and sold packs of 5 stickers, that means 18^5 = 1889568 total combinations!

Each image was around 80KB so we’re looking at around 152GB worth of images.

Yeah, I’m not paying for that sort of storage.

It was clear, that the images have to be generated when someone orders a pack. There are 2 options here

  1. Generate the image when an order is placed
  2. Generate the image on the fly when twitter pings our servers

Option 2 sounds like it would be more fun to implement, doesn’t it? 😄

Okay, so here’s how I implemented it:

First, we need a HTML page that has the laptop image and can paste stickers on top of it.

<body classname="yellow">
  <div
    id="laptop"
    style="background-image: url(laptop.png)"
  />
</body>

tweet

And now some hacky javascript to read the URL

<script>
  // URL: https://stackstickers.shop/laptop?q=javascript,redux,react,vue,graphql

  const searchQuery = window.location.search
  // ?q=javascript,redux,react,vue,graphql

  const list = searchQuery.split('q=')[1]
  // javascript,redux,react,vue,graphql

  const stickers = list.split(',')
  // ['javascript', 'redux', 'react', 'vue', 'graphql']
</script>

Great, now that we have an array of stickers, let’s place them on the laptop

// plain old javascript
const laptop = document.getElementById('laptop')

for (let i = 0; i < stickers.length; i++) {
  // create a new div
  const stickerDOM = document.createElement('div')

  // attach classname
  stickerDOM.className = 'sticker'

  // add background image
  stickerDOM.setAttribute(
    'style',
    'background-image: url(images/' + stickers[i] + '.png)'
  )

  // add that to the laptop dom
  overlap.append(stickerDOM)
}

Result:

tweet

Isn’t that beautiful? The link is live if you want to play with the URL and see stickers change. Okay, but this is still a HTML page, we can’t pass this to twitter. We need an image, remember?

This is where puppeteer comes in. Puppeteer is a node library which provides a high-level API to control a headless Chrome instance.

What does that mean? It means we can run chrome on a unix server without needing an interface!

So I put up an express server to take requests and takes a screenshot of the above html page. Here’s what the code looks like:

I’ve used async await, it lets you pretend your code is synchronous even when it’s not. Whenever you see await, assume that step blocks the flow of code until it’s done. More about it here.
/* NodeJS server */

// Launch a new browser instance
const browser = await puppeteer.launch()

// Create a "page"
page = await browser.newPage()

// Here's our express server
app.get('/', async function(req, res) {
  // parse the stickers from url
  const query = qs.parse(url.parse(req.url).query)

  // navigate to the html page we had created earlier
  await page.goto(
    `https://stackstickers.shop/laptop?q=${stickers}`
  )

  // take a screenshot of the page!
  const image = await page.screenshot({})

  // response with the image!
  res.end(image)
})

Whoa, can you do that?

Yep! You can open a browser on your server, go to a URL and take it’s screenshot! Magic! ✨

screenshot with stickers

This image was returned by that express server.

And this server is live too, hit this link to get an image: https://stackstickers.shop/laptop?q=javascript,redux,react,vue,graphql

That’s the URL we pass along to twitter for fetching images. Works like a charm! You can play around with the URL to change the stickers.

Puppeteer is a really flexible tool! You can take screenshots, convert to PDF, crawl pages, conduct performance audits and of course, end to end testing. Here are bunch of code examples, if you’re curious.

Hope this was useful on your journey

Sid


Want articles like this in your inbox every Friday?
Javascript and personal growth. No spam, I promise!