Dynamic Open Graph Image Generator with Layer0, Next.js, TailwindCSS, Chrome AWS Lambda and Puppeteer-Core
Tags: edge, layer0, aws, tutorial
Published: 2022-06-08 06:22:30

## What's an Open Graph Image?

Consider sharing a link on Twitter, LinkedIn or Slack. The descriptive images you see about the article even before you open them is because of Open Graph Image Tag (i.e. `og:image` inside your html).

### An Example

Images below show how a Tweet, LinkedIn Post or Slack message will look like when the link `https://dev.to/digitalplayer1125/conditional-basic-authorization-using-the-platform-layer0-54bi` is shared.

![Open Graph Image on Twitter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyn8dxwob428diqme865.png)
<figcaption>Fig.1 - <a href="https://dev.to/digitalplayer1125/conditional-basic-authorization-using-the-platform-layer0-54bi">Article</a> when shared on Twitter</figcaption>

![Open Graph Image on LinkedIn](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3o0wi4tttlwy8qrjt7f.png)
<figcaption>Fig.2 - <a href="https://dev.to/digitalplayer1125/conditional-basic-authorization-using-the-platform-layer0-54bi">Article</a> when shared on LinkedIn</figcaption>

![Open Graph Image on Slack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tbkqrdtp3k8gz50nd7qe.png)
<figcaption>Fig.3 - <a href="https://dev.to/digitalplayer1125/conditional-basic-authorization-using-the-platform-layer0-54bi">Article</a> when shared on Slack</figcaption>

### But how?

This all happens through the `og:image` or `twitter:image:src` tag inside the `<head>` tag of your html.

```html
<meta property="og:image" content="https://res.cloudinary.com/practicaldev/image/fetch/s--bp_68opi--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4u11hj7netzt0695wzj.png" />
```
OR
```html
<meta name="twitter:image:src" content="https://res.cloudinary.com/practicaldev/image/fetch/s--bp_68opi--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4u11hj7netzt0695wzj.png">
```

## Automating Dynamic Previews
You might wanna ask: **But Rishi, do I need to design each image, upload somewhere and then add it to my blog? Oh it'd be amazing if you could do some magic and do that over the air?**

Well, yes you can do that! This is what the rest of the blog is gonna be about, using Next.js, chrome-aws-lambda and puppeteer-core to create an app deployed on Layer0 to create pages (and cache them), and then serve screenshots (one generated, cached forever) of them as the dynamic previews.

### End Product aka Output's Example

The link [https://rishi-raj-jain-html-og-image-default.layer0-limelight.link/api?title=Case%20Study%3A%20How%20Nike.com%20can%20leverage%20Layer0%20to%20improve%20their%20First%20Page%20Loads%20and%20Navigation%20upto%20~80%25%2C%20acing%20Largest%20Contentful%20Paint.&image=https%3A%2F%2Fimages.pexels.com%2Fphotos%2F1456706%2Fpexels-photo-1456706.jpeg%3Fcs%3Dsrgb%26dl%3Dpexels-ray-piedra-1456706.jpg%26fm%3Djpg&mode=true](https://rishi-raj-jain-html-og-image-default.layer0-limelight.link/api?title=Case%20Study%3A%20How%20Nike.com%20can%20leverage%20Layer0%20to%20improve%20their%20First%20Page%20Loads%20and%20Navigation%20upto%20~80%25%2C%20acing%20Largest%20Contentful%20Paint.&image=https%3A%2F%2Fimages.pexels.com%2Fphotos%2F1456706%2Fpexels-photo-1456706.jpeg%3Fcs%3Dsrgb%26dl%3Dpexels-ray-piedra-1456706.jpg%26fm%3Djpg&mode=true)

generates the image below:

![OG Image Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfg1m2ntqioecp4e8mo0.png)

### Why such a big link?

The link can be broken into following parts:

- `https://rishi-raj-jain-html-og-image-default.layer0-limelight.link/api`: An express endpoint created using [API Routes with Next.js](https://nextjs.org/docs/api-routes/introduction)
- Query Parameters:
   - `title`: The descriptive text, visible majorly in the generated image
   - `image`: Link to an image that'll be embedded inside the generated image 
   - `mode`: A string either in `true` or `false` to toggle the dark mode in the generated image

> **NOTE: While using such links inside HTML, it's important to know that while dynamically creating this link, one has to make use of encodeURIComponent() to ensure that link to the image, spaces in title, etc. get properly encoded to be received as is by the express endpoint.**

For example,

```js
const title= 'Something'
const image= 'https://images.unsplash.com/photo-1644982647869-e1337f992828?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1035&q=80'
const mode= 'true'

// The correct link to the generated image shall be:
const previewImageLink= `https://rishi-raj-jain-html-og-image-default.layer0-limelight.link/api?title=encodeURIComponent{title}&image=encodeURIComponent{image}&mode=encodeURIComponent{mode}`
```

## Creating Dynamic Previews App

**Step 1: Initialise a Next.js App**

```bash
npx create-next-app@latest dynamic-image-generator
```

**Step 2: Install TailwindCSS with Next.js**

Follow the update guide on [https://tailwindcss.com/docs/guides/nextjs](https://tailwindcss.com/docs/guides/nextjs)

**Step 3: Create a page that'll be dynamic to query parameters**

We'll hit /dynamic_blogs with the same query parameters as received by /api to take screenshot of. Let's create the page inside your next app.

```js
// File: pages/dynamic_blogs.js

// Destructuring title, image and mode from the query (sent as props to this component)
const Blogs = ({ title, image, mode }) => {
  return (
    <div className={`flex flex-row px-10 items-center justify-center h-screen w-screen ${mode === 'true' ? 'bg-gray-900' : 'bg-gray-100'}`}>
      <div className="px-10 py-0 m-0 w-4/5 h-4/5 flex flex-col">
        <h5 className="text-2xl text-gray-500">Checkout this article</h5>
        <h1 className={`mt-2 text-4xl sm:text-6xl leading-none font-extrabold tracking-tight ${mode === 'true' ? 'text-white' : 'text-gray-900'}`}>{title}</h1>
        <div className="flex flex-row items-start mt-auto">
          <img src="https://rishi.app/static/favicon-image.jpg" className="rounded-full" style={{ width: '120px', height: '120px' }} />
          <div className="ml-5 flex flex-col">
            <h6 className={`font-bold text-4xl ${mode === 'true' ? 'text-gray-300' : 'text-gray-500'}`}>Rishi Raj Jain</h6>
            <p className="mt-3 text-2xl text-gray-500">Wanna take everyone along in this web development journey by learning and giving back async</p>
          </div>
        </div>
      </div>
      <div className="px-10 py-0 m-0 w-2/5 h-4/5">
        <img src={image} className="object-cover h-full" />
      </div>
    </div>
  )
}

export default Blogs

/// Receive the query parameters on the server-side
// Read more on queries with getServerSideProps at:
// https://github.com/vercel/next.js/discussions/13309
export async function getServerSideProps({ query }) {
  return {
    props: { ...query },
  }
}
```

**Step 4: Install puppeteer-core and chrome-aws-lambda**

We'll be using these packages to open the link /dynamic_blogs with query parameters and then return screenshot from the API endpoint created in the next step.

```bash
npm i puppeteer-core chrome-aws-lambda
```

**Step 5: Create an API Route with Next.js**

Do read the comments inside the file.

```js
// File: pages/api/index.js
// This is accessible from the deployed link (say, Y.com) as y.com/api?queryparametershere

import core from 'puppeteer-core'
import chromium from 'chrome-aws-lambda'

export default async function handler(req, res) {
  // Only allow POST to the given route
  if (req.method === 'GET') {
    const { title, mode, image, width = 1400, height = 720 } = req.query
    // Launching chrome with puppeteer-core
    // https://github.com/puppeteer/puppeteer/issues/3543#issuecomment-438835878
    const browser = await core.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
      ignoreHTTPSErrors: true,
    })
    // Create a page
    const page = await browser.newPage()
    // Define the dimensions of the page
    await page.setViewport({ width: parseInt(width), height: parseInt(height) })
    // Load the /dynamic_blogs with the given query paramters
    // Don't forget to encode them!
    // req.headers.host allows to obtain the deployed link as is, hence this app can be deployed anywhere
    // This allows us to take advantage of Layer0 caching to serve the /dynamic_blogs pages faster to this .goto() call
    await page.goto(`https://${req.headers.host}/dynamic_blogs?title=${encodeURIComponent(title)}&image=${encodeURIComponent(image)}&mode=${encodeURIComponent(mode)}`)
    // On average, place an image that is fast to load.
    // Falling back to 5 seconds timeout where image might take longer to load.
    await page.waitForTimeout(5000)
    // Take screenshot of the body of the page, that is the content
    const content = await page.$('body')
    const imageBuffer = await content.screenshot({ omitBackground: true })
    await page.close()
    await browser.close()
    res.setHeader('Cache-Control', 'public, immutable, no-transform, s-maxage=31536000, max-age=31536000')
    res.setHeader('Content-Type', 'image/png')
    res.send(imageBuffer)
    res.status(200)
    return
  }
  // Any other method than GET results in a ERROR 400.
  res.status(400).json({ message: 'Invalid method.' })
  return
}
```

**Step 6: Install Layer0 CLI**

```bash
npm i -g @layer0/cli
```

**Step 7: Integrate Layer0 with Next.js**

To wrap Layer0 over your Next.js app, run:

```bash
layer0 init # 0 init
```

Modify `next.config.js` to opt-in `target:'server'` with the latest Next.js version. This is how the config will look like:
```js
// File: next.config.js

const { withLayer0, withServiceWorker } = require('@layer0/next/config')

module.exports = withLayer0(
  withServiceWorker({
    target: 'server',
    compress: true,
    layer0SourceMaps: true,
    disableLayer0DevTools: true,
  })
)
```

**Step 8: Cache both the dynamic_blogs page and the API Route**

With Layer0's caching, you can overcome the long times to generate the dynamic pages and the API response again, rather cache them as long as you want. As these are just images, that'll be cached separately with each new query parameter value, you can cache them for a good long year.

Same can be achieved by modifying routes.js as follows:
```js
const { nextRoutes } = require('@layer0/next')
const { Router } = require('@layer0/core/router')

module.exports = new Router()
  .match('/service-worker.js', ({ serviceWorker }) => {
    return serviceWorker('.next/static/service-worker.js')
  })
  // Caching will be unique to each unique query param
  // /dynamic_blogs?title=Some will be cached for a year
  // /dynamic_blogs?title=Other will be cached for a year
  // But each will serve pages that contain the respective titles, and not the same. 
  .match('/dynamic_blogs', ({ cache }) => {
    cache({
      browser: {
        maxAgeSeconds: 0,
        serviceWorkerSeconds: 31536000,
      },
      edge: {
        maxAgeSeconds: 31536000,
        forcePrivateCaching: true,
      },
    })
  })
  // Similar to dynamic_blogs, caching will be unique to each unique query param
  .match('/api', ({ cache }) => {
    cache({
      browser: {
        maxAgeSeconds: 0,
        serviceWorkerSeconds: 31536000,
      },
      edge: {
        maxAgeSeconds: 31536000,
        forcePrivateCaching: true,
      },
    })
  })
  .use(nextRoutes)
```


**Step 9: Deploy to Layer0**

```bash
layer0 deploy # 0 deploy
```

OR

[![Deploy with Layer0](https://docs.layer0.co/button.svg)](https://app.layer0.co/deploy?repo=https://github.com/rishi-raj-jain/html-og-image)

At the end, you shall see something like this:

![Deployment Successful Layer0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i98fo15jhx0ewzf5y403.png)

**Understanding the architecture**
![OG-Image Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wzahmtbwj3key8dd1d9s.png)

**Step 10: Test**

Open the deployed URL, and append `/api?title=Incremental%20Static%20Generation&image=https://images.pexels.com/photos/12079516/pexels-photo-12079516.jpeg?cs=srgb&dl=pexels-hoài-nam-12079516.jpg&fm=jpg` at the end of it to see the magic happen!

### That's it folks!
I hope this was helpful. Hit me up for any doubt on [rishi18304@iiitd.ac.in](mailto:rishi18304@iiitd.ac.in).