Generating OpenGraph images with Netlify On-demand builders
Tags: netlify, serverless
Published: 2021-05-10 08:52:23

---
title: Generating OpenGraph images with Netlify On-demand builders
published: true
date: 2021-05-01 16:08:35 UTC
tags: netlify,serverless
canonical_url: https://michaelheap.com/og-image-netlify-on-demand-builders/
---

A little while back I built an OpenGraph image generator on AWS Lambda for my [Action Spotlight](https://michaelheap.com/category/action-spotlight) and [TIL](https://michaelheap.com/til) categories. I toyed with the idea of making them generate the images on demand, but decided that I didn’t want the potential traffic to a Lamba function and opted to pre-generate and store in S3 instead.

Then this week, I saw that [Netlify On-demand builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are available in beta which solve all my problems. I don’t need to pre-generate any images, but they’re also only generated once per deploy and distributed via the Netlify CDN.

My requirements for the project were:

- Each image should only be generated once (once per deploy is fine)
- The function should support multiple templates for my OpenGraph image
- It should be possible to pass in any data required

Overall, it was a success 🎉

> If you’re looking for the code without an explanation, it’s available [on GitHub](https://github.com/mheap/netlify-opengraph-on-demand-builders)

## Structuring your function

Netlify functions live in the `netlify/functions` directory in your project. You can create files directly in there, or create a folder named after your function. I chose to create a folder as I have some additional files to package up with my JS file:

```bash
mkdir -p netlify/functions/ogtouch netlify/functions/og/og.js
```

I used my [image generation via Netlify](https://michaelheap.com/netlify-function-lambda-return-image/) script as a starting point as I knew it worked to generate an image.

The first requirement is that each image should only be generated once. To enable this you use the `builder` decorator from `@netlify/functions`:

```js
const { builder } = require("@netlify/functions");

exports.handler = builder(async function (event, context) {
  // Code
});
```

Make sure to install it into your project with `npm install --save-dev @netlify/functions`.

At this point the image will be generated on the first execution and then cached for any future requests.

The image so far looks like this:

 ![Initial image](https://michaelheap.com/images/og-image-netlify-on-demand-builders/basic-image.png/e191dc25-640.jpeg)

## Template support

Next, I wanted to add template support so that I didn’t have to hard-code the entire template in my function.

To do this I created a folder named `templates` and a template for my #til category:

```bash
mkdir -p netlify/functions/og/templatestouch netlify/functions/og/templates/til.html
```

To load this file I imported the `fs` module and read the file contents into a variable, then passed that string to `page.setContent()`:

```js
const fs = require("fs").promises;

exports.handler = builder(async function (event, context) {
  // ...snip...

  const template = "til";
  let htmlPage = (
    await fs.readFile(require.resolve(`./templates/${template}.html`))
  ).toString();

  const page = await browser.newPage();
  await page.setContent(htmlPage);
```

The contents of `til.html` can be anything you like. For testing purposes, I set it to the following:

```html
<style>
  h1 { color: red }
</style>

<body>
  <h1>{title}</h1>
  <p>{description}</p>
</body>
```

The title and description placeholders don’t do anything yet, but we’ll use those later. For now, it’ll just render the page exactly as it is.

At this point you can deploy the function and it will render the HTML template provided as-is

![Image using a template](https://michaelheap.com/images/og-image-netlify-on-demand-builders/using-template.png/40b2bcee-640.jpeg)

## Passing in data

Usually we’d use `GET` parameters to pass in the title and description to use in the image, but on-demand builders \_ don’t provide access to HTTP headers or query parameters from incoming requests\_.

This means that we have to make the parameters a part of the URI. I thought about the best way to achieve this, and decided on a URI structure that looks like the following:

```
/.netlify/functions/og/template=til/title=Demo Title/description=This is an example description that is a little longer to see how it works out
```

Everything after `/og` will be ignored, but it allows us to map over each segment in the URI and treat it as a key/value pair.

To build this mapping, add the following code to your function:

```js
exports.handler = builder(async function (event, context) {
  const { template, ...params } = Object.fromEntries(
    event.path
      .split("/")
      .filter((p) => p.includes("="))
      .map(decodeURIComponent)
      .map((s) => s.split("=", 2))
  );

  // snip...
});
```

You’ll notice that we also read `template` from the path, so you can delete `const template = "til";` from your function too.

Finally, we need to swap out the placeholders in the template for the values in `params`:

```javascript
let htmlPage = (
  await fs.readFile(require.resolve(`./templates/${template}.html`))
).toString();

for (const k in params) {
  htmlPage = htmlPage.replace(`{${k}}`, params[k]);
}
```

After committing and deploying that, I can visit `/.netlify/functions/og/template=til/title=Demo Title/description=This is an example description that is a little longer to see how it works out` and see that it renders the image as expected

![Rendered image using passed in data](https://michaelheap.com/images/og-image-netlify-on-demand-builders/final-image.png/c7ffd533-640.jpeg)

## Conclusion

Under 45 lines of code and we have a Netlify function that generates OpenGraph images on the fly and caches them on a CDN for future requests. On-demand builders seem pretty great, and I’m looking forward to working with them more in the future.

If you're looking for the full code, it's available below or [on GitHub](https://github.com/mheap/netlify-opengraph-on-demand-builders)

```javascript
const chromium = require("chrome-aws-lambda");
const puppeteer = require("puppeteer-core");
const { builder } = require("@netlify/functions");
const fs = require("fs").promises;

exports.handler = builder(async function (event, context) {
  const { template, ...params } = Object.fromEntries(
    event.path
      .split("/")
      .filter((p) => p.includes("="))
      .map(decodeURIComponent)
      .map((s) => s.split("=", 2))
  );

  const browser = await puppeteer.launch({
    args: chromium.args,
    defaultViewport: { height: 630, width: 1200 },
    executablePath: await chromium.executablePath,
    headless: chromium.headless,
  });

  let htmlPage = (
    await fs.readFile(require.resolve(`./templates/${template}.html`))
  ).toString();

  for (const k in params) {
    htmlPage = htmlPage.replace(`{${k}}`, params[k]);
  }

  const page = await browser.newPage();
  await page.setContent(htmlPage);
  await page.waitForTimeout(1000);

  const buffer = await page.screenshot();

  return {
    statusCode: 200,
    headers: {
      "Content-Type": "image/png",
    },
    body: buffer.toString("base64"),
    isBase64Encoded: true,
  };
});
```