Dynamic Open Graph Images with NextJS
Nov 14, 2022
Kacey Cleveland
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
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
1// path: /pages/api/open-graph-image.ts
2import type { NextApiRequest, NextApiResponse } from "next";
3import chromium from 'chrome-aws-lambda';
4import { chromium as playwrightChromium } from 'playwright-core';
5// getAbsoluteURL is in a snippet further down
6import { getAbsoluteURL } from 'utils/utils';
7
8export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9 // Start the browser with the AWS Lambda wrapper (chrome-aws-lambda)
10 const browser = await playwrightChromium.launch({
11 args: chromium.args,
12 executablePath: await chromium.executablePath,
13 headless: chromium.headless,
14 })
15 // Create a page with the Open Graph image size best practise
16 // 1200x630 is a good size for most social media sites
17 const page = await browser.newPage({
18 viewport: {
19 width: 1200,
20 height: 630
21 }
22 });
23 // Generate the full URL out of the given path (GET parameter)
24 const relativeUrl = (req.query["path"] as string) || "";
25 const url = getAbsoluteURL(relativeUrl)
26
27 await page.goto(url, {
28 timeout: 15 * 1000,
29 // waitUntil option will make sure everything is loaded on the page
30 waitUntil: "networkidle"
31 })
32 const data = await page.screenshot({
33 type: "png"
34 })
35 await browser.close()
36 // Set the s-maxage property which caches the images then on the Vercel edge
37 res.setHeader("Cache-Control", "s-maxage=31536000, stale-while-revalidate")
38 res.setHeader('Content-Type', 'image/png')
39 // write the image to the response with the specified Content-Type
40 res.end(data)
41}
getAbsoluteURL
1// Gets the URL for the current environment
2export const getAbsoluteURL = (path: string) => {
3 const baseURL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"
4 return baseURL + path
5}
use-open-graph-image
1import { useRouter } from "next/router";
2import { getAbsoluteURL } from "./utils";
3
4export default function useOpenGraphImage() {
5 const router = useRouter();
6 const searchParams = new URLSearchParams();
7 // The [slug] from /posts/[slug] and /posts/open-graph/[slug]
8 // should be identical.
9 searchParams.set(
10 "path",
11 router.asPath.replace("/posts/", "/posts/open-graph/")
12 );
13 // Open Graph & Twitter images need a full URL including domain
14 const fullImageURL = getAbsoluteURL(`/api/open-graph-image?${searchParams}`);
15 return { imageURL: fullImageURL };
16}
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:
1import Head from "next/head";
2
3const postComponent = (props) => {
4 const { imageURL } = useOpenGraphImage(); // <- This custom hook here!
5 return <>
6 <Head>
7 <title>Kacey Cleveland - {title}</title>
8 <meta name="description" content={props.description} />
9 <meta property="og:title" content={props.title} />
10 <meta property="og:type" content="article" />
11 <meta property="og:image" content={imageURL} />
12 </Head>
13 <div>
14 // Content here
15 </div>
16 </>;
17}
/utils/use-open-graph-image.ts
1import { useRouter } from "next/router";
2import { getAbsoluteURL } from "./utils";
3
4export default function useOpenGraphImage() {
5 const router = useRouter();
6 const searchParams = new URLSearchParams();
7 searchParams.set(
8 "path",
9 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
10 );
11 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.
12 return { imageURL: fullImageURL };
13}
Fin
TLDR the order of operations are the following:
- A user shares a link to your article/dynamic content
- The site that the article is shared on finds reads the meta tags and finds there is an open graph image tag
- 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/