Cruip
Posted on September 5, 2023
Live Demo / Download
In this tutorial, we’ll see how to create dynamic Open Graph and Twitter images with Next.js. This technique allows us to generate personalized previews for our articles when shared on social media platforms, enhancing engagement with your audience.
The dynamic images will include a background that’s been prepared in advance and some text that corresponds to the title of the page. Additionally, we’ll incorporate information about the author of the page, as demonstrated in the preview image above.
What makes this approach intriguing is its ability to generate images on-the-fly whenever a page is shared. This means we can offer a tailored preview for each article without needing to create individual images for each one.
This is made possible thanks to the Dynamic Open Graph Image Generation feature introduced with Next.js version 13.3, and the new Metadata API. In summary, it involves generating images using code (in our case, TSX, HTML, and CSS) with the help of the libraries @vercel/og (already integrated in the App router) and Satori. Satori converts HTML and CSS to SVG, and then resvg-js converts the SVG to a PNG image. All of this in just a few milliseconds!
Let’s get started!
Creating a new page in your Next.js app
Since we’re using the App router, you only need to create a new folder within the app
directory and give it a name that corresponds to your desired page path. For instance, let’s consider app/social-preview
. Within this folder, create a new file named page.tsx
and include the following code:
export const metadata = {
title: 'Social Metadata - Cruip Tutorials',
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
}
import Banner from '@/components/banner'
export default function SocialPreviewPage() {
return (
<>
<main className="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
<div className="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
<div className="text-center">
<div className="font-extrabold text-3xl md:text-4xl [text-wrap:balance] bg-clip-text text-transparent bg-gradient-to-r from-slate-200/60 to-50% to-slate-200">Generate Dynamic Open Graph and Twitter Images in Next.js</div>
<p className="text-lg text-slate-500 mt-4">Share this page on Facebook and Twitter to see the preview image</p>
</div>
</div>
</main>
</>
)
}
For now, we’re focusing on setting the title; page content isn’t vital at this stage. The objective is to understand the concept of generating images dynamically.
Dynamic text generation
Before extending the metadata
object, we’ll create an API route that will serve as an endpoint for dynamically generating the text. Create a folder called og
inside the api
directory. Inside that, create a file named route.tsx
and add this code:
import { ImageResponse } from 'next/server'
export const runtime = 'edge'
export async function GET(request: Request) {
const interExtrabold = fetch(
new URL('../../../public/Inter-ExtraBold.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
try {
const { searchParams } = new URL(request.url)
const hasTitle = searchParams.has('title')
const title = hasTitle
? searchParams.get('title')?.slice(0, 100)
: 'Default title'
return new ImageResponse(
(
<div
style={{
backgroundImage: 'url(https://cruip-tutorials-next.vercel.app/social-card-bg.jpg)',
backgroundSize: '100% 100%',
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
fontFamily: 'Inter',
padding: '40px 80px',
}}
>
<div
style={{
fontSize: 60,
fontWeight: 800,
letterSpacing: '-0.025em',
lineHeight: 1,
color: 'white',
marginBottom: 24,
whiteSpace: 'pre-wrap',
}}
>
{title}
</div>
<img
width="203"
height="44"
src={`https://cruip-tutorials-next.vercel.app/author.png`}
/>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await interExtrabold,
style: 'normal',
weight: 800,
},
],
},
)
} catch (e: any) {
console.log(`${e.message}`)
return new Response(`Failed to generate the image`, {
status: 500,
})
}
}
This code creates a GET
function which returns an ImageResponse
object. The ImageResponse
constructor generates a dynamic image from JSX and CSS. The image will consist of a background image, a title, and an image containing author information.
The steps are as follows:
- Incorporating a custom font (Inter Extrabold) from the
public
directory. - Defining the
title
constant to hold the page’s title. If atitle
parameter exists in the URL, it is used; otherwise, the default is “Default title.” - Designing the structure using CSS in accordance with Satori’s guidelines.
- Specifying the image size as 1200x630px.
- Adding Inter Extrabold to the fonts array.
Calling the route handler in page.tsx
will trigger the dynamic image generation.
Defining OpenGraph and Twitter fields
We can now expand the metadata object by adding the openGraph
and twitter
fields. These fields hold metadata specifically intended for social media platforms.
First, set up metadata for Open Graph:
openGraph: {
title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
type: "article",
url: "https://cruip-tutorials-next.vercel.app/social-preview",
images: [
{
url: "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
},
],
}
Next, we’ll proceed with the metadata for the Twitter Card:
twitter: {
card: "summary_large_image",
title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
images: [
"https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
],
}
Note how we’re invoking the /api/og
endpoint to trigger the dynamic image generation. In this case, we’re passing the page’s title as a parameter, but we could pass any parameter we want.
The metadata object will look like this:
export const metadata = {
title: 'Social Metadata - Cruip Tutorials',
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
openGraph: {
title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
type: "article",
url: "https://cruip-tutorials-next.vercel.app/social-preview",
images: [
{
url: "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
},
],
},
twitter: {
card: "summary_large_image",
title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
description:
"A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
images: [
"https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
],
},
}
And here’s the end result:
Simply share the page’s link on Twitter or Facebook to observe the preview in action!
Styling the image content with Tailwind CSS
Although still an experimental feature of Satori, it’s possible to use Tailwind CSS classes to style the image’s content. We’re big fans of the Css frameworks, as shown by our Tailwind templates, so we had to give it a try!
Satori allows us to define Tailwind CSS classes within dedicated tw
attributes. Therefore, we can achieve the same result using the following code:
import { ImageResponse } from 'next/server'
export const runtime = 'edge'
export async function GET(request: Request) {
const interExtrabold = fetch(
new URL('../../../public/Inter-ExtraBold.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
try {
const { searchParams } = new URL(request.url)
const hasTitle = searchParams.has('title')
const title = hasTitle
? searchParams.get('title')?.slice(0, 100)
: 'Default title'
return new ImageResponse(
(
<div
tw="h-full w-full flex flex-col align-start justify-center py-10 px-20"
style={{
backgroundImage: 'url(https://cruip-tutorials-next.vercel.app/social-card-bg.jpg)',
backgroundSize: '100% 100%',
fontFamily: 'Inter',
}}
>
<div
tw="text-6xl font-extrabold text-white tracking-tight leading-none mb-6 whitespace-pre-wrap"
>
{title}
</div>
<img
width="203"
height="44"
src={`https://cruip-tutorials-next.vercel.app/author.png`}
/>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await interExtrabold,
style: 'normal',
weight: 800,
},
],
},
)
} catch (e: any) {
console.log(`${e.message}`)
return new Response(`Failed to generate the image`, {
status: 500,
})
}
}
Posted on September 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.