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
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) }
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 }
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 }; }
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> </>; }
/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 }; }
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.
Top comments (0)