Static, automated social images with NextJS
Tags: nextjs, opengraph, tutorial, resoc
Published: 2021-09-10 06:24:18

The "social images" are linked to pages via [Open Graph metadata](https://ogp.me/) and used by social networks to display rich content. For example, if I share https://demo-project-nextjs-devto.vercel.app/ on Slack I get:

![Share a link on Slack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xd97clul8gqwj9lvwn3.png)

The white-text-blue-background image above is probably the first thing that catches the eye.

As great opportunities to grab attention, social images should be carefully crafted. Often they are not. No wonder. Creating such images for every single page is plain boring. A classic strategy is to create a one-size-fits-all image with a generic title and company's logo, and use it everywhere...

![David good enough](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03ezv16jqtzl9domt8ky.png)

However, we can do way better. This tutorial explains how to do it right with NextJS, by automating the boring part while leaving us only the fun bits.

In this first part, we focus on static sites, ie. `getStaticProps`. We will explore server side rendering later.

By the end of your reading, you will have a NextJS app, ready to be deployed to Vercel, with two pages. Each page will have its own social image.

Get ready, create your app!

```bash
yarn create next-app social-demo
```

## ~~Multiple images~~ A single image template

What we are afraid of is the idea of creating dozens of similar images manually. We don't want to do this.

However, there *is* something fun to do: define how these images should look like, once for all. Plus, we are going to make it a web design task, with HTML and CSS.

Enter your app and create a new image template using the [Resoc image template dev kit](https://www.npmjs.com/package/itdk):

```bash
cd social-demo
npx itdk init resoc-template
```

This command creates a new image template and opens it in your browser.

![Default image template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ck6z7g57p2hwuxulctjn.png)

What you see here is not an image but a template. Some elements are fixed, like the logo in the bottom left corner. Some others are designed to change from an image to another, like the title or the photo. Edit the values in the right panel to check for yourself.

This template is a good start but we won't use it as is. First, we need to decide of the fields. For this demo, let's rely on something pages already have: a title and a description.

Open `social-demo/resoc-template/resoc.manifest.json`, which describes the template. Replace its content with:

```json
{
  "partials": {
    "content": "./content.html.mustache",
    "styles": "./styles.css.mustache"
  },
  "parameters": [
    {
      "name": "title",
      "type": "text",
      "demoValue": "A picture is worth a thousand words"
    },
    {
      "name": "description",
      "type": "text",
      "demoValue": "And so much to say!"
    }
  ]
}
```

Go back to your browser. The form in the right panel now has the two fields we put in the manifest:

![New parameters](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wn450idk9e8yntgq4xyl.png)

Oh, and we broke the existing template in the process, but that's okay. We want to write our own anyway.

Edit `social-demo/resoc-template/content.html.mustache`. Here you find regular HTML, with a little bit of Mustache, a templating system. For example, `<h1>{{ title }}</h1>` will actually become `<h1>Whatever</h1>` when generating an image where the title is, well, "Whatever". Our template is fairly simple:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">

<div class="wrapper">
  <h1 id="title">
    {{ title }}
  </h1>
  <div id="footer">
    <h2 id="description">
      {{ description }}
    </h2>
    <img id="logo" src="logo.png" />
  </div>
</div>

<script src="textFit.js"></script>
<script>
  window.onload = function() {
    textFit(document.getElementById('title'), { maxFontSize: 500, multiLine: true });
  };
</script>
```

We use [textFit](https://github.com/STRML/textFit), a handy JS utility to make text fits its container.

We need CSS in `social-demo/resoc-template/styles.css.mustache` to make the template attractive:

```css
.wrapper {
  font-family: Roboto;

  padding: 4vw;

  background: rgb(7,68,144);
  background: linear-gradient(50deg, rgba(7,68,144,1) 0%, rgba(0,123,255,1) 100%);

  color: white;

  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10vh;
}

#title {
  flex: 1.62;
  font-size: 0.01rem; /* Rely on textFit to find the right size */
  height: 100%;
}

#footer {
  display: flex;
  align-items: center;
  gap: 10vh;

  font-size: 6vh;
}

#description {
  flex: 1 1 auto;

  margin: 0;

  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

#logo {
  height: 10vh;
}
```

Also, why not replace `social-demo/resoc-template/logo.png` with something else?

Now our template looks great:

![Final template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fb5h70gmoavolpr43mxu.png)

We want to make sure our template does not break with large chunks of text. Let's type a lot of text to check how it behaves:

![Huge text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ndnqmlpu7s5kaqxk2api.png)

Alright! The title font size decreases as we type thanks to textFit, and the description overflow is hidden with ellipsis. Our template is ready.

## Better meta management

NextJS boilerplate puts page metadata, such as title and favicon, in `social-demo/pages/index.js`. This is okay for a first shot, but a real app won't force each page to declare these markups. Instead, they should be defined once for all.

We are going to move meta logic to `MyApp` and pass title and description via `pageProps`. Edit `social-demo/pages/_app.js`:

```jsx
import Head from 'next/head'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>{pageProps.title}</title>
        <meta name="description" content={pageProps.description} />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Component {...pageProps} />
    </>
  )
}

export default MyApp
```

We now change `social-demo/pages/index.js` accordingly. We remove the `<Head>` markup and content near the beginning of the file, along with the `next/Head` import. We also add a new `getStaticProps` at the bottom of the file. And we drop most of the demo content for clarity:

```jsx
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      No need for content...
    </div>
  )
}

export async function getStaticProps(context) {
  return {
    props: {
      title: "Automated social images!",
      description: "Look ma, no hands"
    }
  }
};
```

Start the app:

```bash
yarn run dev
```

Visit `http://localhost:3000/` and note the homepage title in your tab. It says "Automated social images!".

Time for automation!

## Automate image creation

We are now going to use our image template to create as many images as there are pages.

First, create directory `social-demo/public/open-graph`, where images will be stored. When using Git, these files don't need to be committed. So we should add this repository to `.gitignore` and commit an empty `social-demo/public/open-graph/.keep` file to make sure the directory is always here.

We need two packages to turn our template into images:

```bash
yarn add --dev @resoc/core @resoc/create-img
```

Resoc uses [Puppeteer](https://github.com/puppeteer/puppeteer) and Chromium to convert HTML to images. Everything will take place at build time, thus the `--dev`. We won't hear about these packages once our app is deployed.

We create `social-demo/lib/social-image.js` to take advantage of these utilities:

```javascript
import { compileLocalTemplate } from '@resoc/create-img'
import { FacebookOpenGraph } from '@resoc/core'
import path from 'path'

const socialImage = async (title, description, baseName) => {
  const ogImage = await compileLocalTemplate(
    'resoc-template/resoc.manifest.json', {
      title,
      description
    },
    FacebookOpenGraph,
    `public/open-graph/${baseName}.jpg`
  );

  return {
    title,
    description,
    ogImage: path.basename(ogImage)
  }
};

export default socialImage;
```

This is were most of the magic happens. We declare `socialImage`, which takes the title and description as parameters, along with a name for the image. We call `compileLocalTemplate` to turn the template into an image, passing it the template itself, the title and description, the output image resolution (`FacebookOpenGraph`) and an output file name. Finally, we return values which will be used to populate `pageProps`.

Let's call `socialImage`. In `social-demo/pages/index.js`, update `getStaticProps`:

```javascript
export async function getStaticProps(context) {
  return {
    props: {
      ...(await socialImage(
        'Automated social images!',
        'Look ma, no hands',
        'homepage'
      ))
    }
  }
};
```

In addition to the title and description, `MyApp` receives a third prop named `ogImage`, returned by `socialImage`. Edit `social-demo/pages/_app.js` to take it into account:

```javascript
import Head from 'next/head'
import { FacebookOpenGraph } from '@resoc/core'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>{pageProps.title}</title>
        <meta name="description" content={pageProps.description} />
        <link rel="icon" href="/favicon.ico" />

        <meta property="og:title" content={pageProps.title} />
        <meta property="og:description" content={pageProps.description} />
        <meta property="og:image" content={`/open-graph/${pageProps.ogImage}`} />
        <meta property="og:image:width" content={FacebookOpenGraph.width} />
        <meta property="og:image:height" content={FacebookOpenGraph.height} />
      </Head>

      <Component {...pageProps} />
    </>
  )
}

export default MyApp
```

The Open Graph markups we've just added are straightforward. `og:title` and `og:description` are similar to the existing title and description tags. `og:image:width` and `og:image:height` define the image resolution. And `og:image` is the URL of the generated image.

Playtime! Restart the app:

```bash
yarn run dev
```

Visit the homepage and inspect the `social-demo/public/open-graph` directory. It contains an image named `homepage.jpg`:

![Homepage image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u48l0jk33kk1a19zio4p.jpg)

In this image, we find the title and description of the homepage.

Let's create a second page for the sake of having multiple images generated for us. Populate `social-demo/pages/another-page.js` with:

```jsx
import styles from '../styles/Home.module.css'
import socialImage from '../lib/social-image'

export default function Home() {
  return (
    <div className={styles.container}>
      Just another page...
    </div>
  )
}

export async function getStaticProps(context) {
  return {
    props: {
      ...(await socialImage(
        'Just another page',
        'Nothing fancy, really',
        'another-page'
      ))
    }
  }
};
```

Visit `http://localhost:3000/another-page` and look at `social-demo/public/open-graph`: a new image with a different content!

However, there is an issue we need to fix.

## No time to waste: use the cache!

We could ship our app as it. Images will be created at build time, as they should.

However, if you refresh your homepage repeatedly, you will notice that each access takes a couple of seconds. This is because the social image of the page is always re-generated.

Although we can wait a few moments while experimenting with social images, this is definitely not something we want to hear about while coding the rest of our app. Waiting more than a few milliseconds just breaks the development experience.

We fix this by leveraging caching. To do so, edit `socialImage` in `social-demo/lib/social-image.js`:

```javascript
import { compileLocalTemplate } from '@resoc/create-img'
import { FacebookOpenGraph } from '@resoc/core'
import path from 'path'

const socialImage = async (title, description, baseName) => {
  const ogImage = await compileLocalTemplate(
    'resoc-template/resoc.manifest.json', {
      title,
      description
    },
    FacebookOpenGraph,
    // Use the image hash as a suffix
    `public/open-graph/${baseName}-{{ hash }}.jpg`,
    // Enable cache
    { cache: true }
  );

  return {
    title,
    description,
    ogImage: path.basename(ogImage)
  }
};

export default socialImage;
```

We pass the options `{ cache: true }` to `compileLocalTemplate` to enable cache. We also change the output image name to `public/open-graph/${baseName}-{{ hash }}.jpg`. `compileLocalTemplate` now generates a hash based on the template itself and the title and description. It then injects this hash in the file name, thanks to the `{{ hash }}` in the name. If such a file already exists, it means we already generated this image and there is no need to do it again. Else, the image is created. `compileLocalTemplate` also returns the file name so we can use it in the the Open Graph `og:image` meta.

Refresh the homepage a few times. The first refresh still takes a few seconds. But the next ones are as fast as usual. Also check `social-demo/public/open-graph`: it now contains an image named `homepage-462b92c2.jpg` or something similar.

## The result

You can deploy this app to [Vercel](https://vercel.com/).

Or you can skip this step and check the app I wrote while writing this article: https://demo-project-nextjs-devto.vercel.app/.

To see the social images in action, you can visit the [Facebook debugger](https://developers.facebook.com/tools/debug/?q=https%3A%2F%2Fdemo-project-nextjs-devto.vercel.app%2F) or share the link via Facebook, LinkedIn, etc.

## Conclusion

We've just made our social images from lame to great, while turning the creation process from dull to fun!

Although this demo was simple, we can easily find ways to improve it. In particular, we could add more data to the images. Like the author's name and profile picture (when there is one), the keywords already populated by the SEO team in the corresponding `meta` markup, the featured image, etc.

We used the [Resoc Image Template Development Kit](https://www.npmjs.com/package/itdk) and other components. I'm creating this suite to make social images creation a bliss. If you followed this article, I would love to get your feedback!

We covered only static generation. Server-side rendering is another topic we will explore in a future article. Follow me to be notified when it's ready!