DEV Community

Cover image for Generating Sharable Content Images with Open Graph and NextJS
Kacey Cleveland
Kacey Cleveland

Posted on

Generating Sharable Content Images with Open Graph and NextJS

Introduction

The Open Graph Protocol (https://ogp.me/) allows for parsing of specific metadata that many social networks utilize to create dynamic sharable content. An example of this could be when you share a post on Facebook with a link but when you actually share it, the link is joined with a description, an author, an even a cover photo/picture. We can take it a step further and generate the photo/picture and also populate the other metadata fields. This article will focus on creating dynamic images based on your dynamic pages. I utilize this method deploying to Vercel for this blog on my website (https://kleveland.dev).

Tech used

  • NextJS
  • Serverless functions (via Vercel/AWS)

Example

https://www.kleveland.dev/posts/create-notion-blog

image

When I try and share one of my blog posts on Linkedin, you can see it gets populated with a preview image and text. We will go over how that image is generated and how we can customize it.

How It Works

As a starting point, I am going to assume you have some dynamic content/pages in a NextJS application. In my case, I utilize the following files for this blog:

Pages:

  • /pages/posts/[slug].tsx
  • /pages/posts/open-graph/[slug].tsx
  • /pages/api/open-graph-image.ts

Utils:

  • /utils/use-open-graph-image.ts
  • /utils/utils.ts

The code is actually borrowed heavily from here with a set of adjustments to make it more customizable:
https://playwright.tech/blog/generate-opengraph-images-using-playwright


api/open-graph-image

// path: /pages/api/open-graph-image.ts
import type { NextApiRequest, NextApiResponse } from "next";
import chromium from 'chrome-aws-lambda';
import { chromium as playwrightChromium } from 'playwright-core';
// getAbsoluteURL is in a snippet further down
import { getAbsoluteURL } from 'utils/utils';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // Start the browser with the AWS Lambda wrapper (chrome-aws-lambda)
  const browser = await playwrightChromium.launch({
    args: chromium.args,
    executablePath: await chromium.executablePath,
    headless: chromium.headless,
  })
  // Create a page with the Open Graph image size best practise
  // 1200x630 is a good size for most social media sites
  const page = await browser.newPage({
    viewport: {
      width: 1200,
      height: 630
    }
  });
  // Generate the full URL out of the given path (GET parameter)
  const relativeUrl = (req.query["path"] as string) || "";
  const url = getAbsoluteURL(relativeUrl)

  await page.goto(url, {
    timeout: 15 * 1000,
    // waitUntil option will make sure everything is loaded on the page
    waitUntil: "networkidle"
  })
  const data = await page.screenshot({
    type: "png"
  })
  await browser.close()
  // Set the s-maxage property which caches the images then on the Vercel edge
  res.setHeader("Cache-Control", "s-maxage=31536000, stale-while-revalidate")
  res.setHeader('Content-Type', 'image/png')
  // write the image to the response with the specified Content-Type
  res.end(data)
}
Enter fullscreen mode Exit fullscreen mode

getAbsoluteURL

// Gets the URL for the current environment
export const getAbsoluteURL = (path: string) => {
    const baseURL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"
    return baseURL + path
}
Enter fullscreen mode Exit fullscreen mode

use-open-graph-image

import { useRouter } from "next/router";
import { getAbsoluteURL } from "./utils";

export default function useOpenGraphImage() {
  const router = useRouter();
  const searchParams = new URLSearchParams();
  // The [slug] from /posts/[slug] and /posts/open-graph/[slug]
  // should be identical.
  searchParams.set(
    "path",
    router.asPath.replace("/posts/", "/posts/open-graph/")
  );
  // Open Graph & Twitter images need a full URL including domain
  const fullImageURL = getAbsoluteURL(`/api/open-graph-image?${searchParams}`);
  return { imageURL: fullImageURL };
}
Enter fullscreen mode Exit fullscreen mode

pages/posts/[slug]

Both of these files should generate the same slugs; the open-graph route slug will correspond to the image for the corresponding article from /pages/posts/[slug].tsx. For example, this article on my website has this route:
https://www.kleveland.dev/posts/create-notion-blog

and if I want the open graph image for that route, I can go to:

https://www.kleveland.dev/posts/open-graph/create-notion-blog

The part that matters is the usage of the custom hook in /pages/posts/[slug].tsx that will get us the imageURL to pass to the meta tags:

import Head from "next/head";

const postComponent = (props) => {
    const { imageURL } = useOpenGraphImage(); // <- This custom hook here!
    return <>
      <Head>
        <title>Kacey Cleveland - {title}</title>
        <meta name="description" content={props.description} />
        <meta property="og:title" content={props.title} />
        <meta property="og:type" content="article" />
        <meta property="og:image" content={imageURL} />
      </Head>
      <div>
        // Content here
      </div>
  </>;
}
Enter fullscreen mode Exit fullscreen mode

/utils/use-open-graph-image.ts

import { useRouter } from "next/router";
import { getAbsoluteURL } from "./utils";

export default function useOpenGraphImage() {
  const router = useRouter();
  const searchParams = new URLSearchParams();
  searchParams.set(
    "path",
    router.asPath.replace("/posts/", "/posts/open-graph/") // This will take the current URL of the post and give us the open-graph one. Modify as needed for how you have your routing setup
  );
  const fullImageURL = getAbsoluteURL(`/api/open-graph-image?${searchParams}`); // This will then pass along the route for the open-graph image to our api request which will run the serverless function which runs headless chrome and goes to the /posts-open-graph/[slug].tsx route and takes a screenshot to serve as the 'fullImageURL' return.
  return { imageURL: fullImageURL };
}
Enter fullscreen mode Exit fullscreen mode

Fin

TLDR the order of operations are the following:

  1. A user shares a link to your article/dynamic content
  2. The site that the article is shared on finds reads the meta tags and finds there is an open graph image tag
  3. The image URL is a GET request to a serverless function that will take a screenshot of the passed route (/posts/open-graph/[slug].tsx) and return the image to be served on the social media site the link was shared on.

Additional Resources

https://ogp.me/

Discussion (1)