Throughout the years, Open Graph images became more and more important. Most modern tools (like Slack, Facebook, or Twitter) automatically show a little preview image of your website whenever you share it. This is the Open Graph image (or OG image).
Especially on social media platforms, link preview images influence heavily whether or not the user decides to visit your site. A well-designed image shows your competence even before the first page view.
Too bad that generating those images dynamically has been quite a struggle… until now. Vercel has a new library, called Vercel OG, that helps you create preview images on the fly.
Let’s take a look how we can use it to improve our blog.
- Integrate
@vercel/og
in Next.js-based blog Generate and embed dynamic OG images for each blog post
Beginner React developers
@vercel/og
Installing First, I set up a brand new Next.js site with the Blog Starter Kit. We will use this blog as an example throughout this article.
With the new site up and running, we can install the @vercel/og
library:
npm install @vercel/og
That’s it for now! We do not need any further configuration.
@vercel/og
Works
How Before we get into implementation details, let’s first have a look at how Vercel OG image works.
Behind the scenes, Vercel OG uses Satori, a new library to convert HTML and CSS to SVGs (and other image formats).
Satori was built for the edge. Hence, it is lightweight, very fast, and runs in a Node.js environment. Sounds like a perfect use-case for API routes in Next.js? It is!
In our blog, we can utilize @vercel/og
to do the following:
- Create an API route handler like
/api/og
. For dynamically generated images, this route may accept parameters via query params (e.g./api/og?title=My First Blog Post
) - In your API route handler, define a JSX-like syntax that represents your image. Think of a classic JSX component but instead of HTML markup, you get an image as the rendered output.
And that’s it. In production, each generated image will be cached at the edge to avoid unnecessary executions. Hence, our second call to /api/og?title=My First Blog Post
will serve the generated and cached image right away.
/api/og
API Route
Creating A Let’s start implementing the OG image generation for your blog.
Inside your pages/api
directory, create a new file called og.tsx
. This will be the handler for our API route.
import { ImageResponse } from '@vercel/og';
import { NextApiHandler } from 'next';
export const config = {
runtime: 'edge',
};
const handler: NextApiHandler = async req => {
try {
return new ImageResponse(<div>My First OG Image</div>, {
width: 1200,
height: 630,
});
} catch {
return new Response(`Failed to generate the image`, {
status: 500,
});
}
};
export default handler;
With this little example, you should already see your first generated OG image. Start your development server and visit http://localhost:3000/api/og
.
In the example, we use ImageResponse
to convert our JSX code (the first parameter) to an image with a specific configuration (the second parameter).
Note: Although it looks like that we can write regular JSX-like components in our API route handler, not all HTML, CSS, and JSX features are supported due to the limitations of the edge and the Satori rendering engine.
@vercel/og
Styling OG Images With Now that we can successfully generate images with our API route, let’s work on the image content.
For this, I created two new components (BackgroundCanvas
and ProfileContent
) that we can import and use in our og.tsx
file.
const handler: NextApiHandler = async req => {
try {
return new ImageResponse(
(
<BackgroundCanvas>
<ProfileContent />
</BackgroundCanvas>
),
{
width: 1200,
height: 630,
}
);
} catch {
return new Response(`Failed to generate the image`, {
status: 500,
});
}
};
The BackgroundCanvas
component looks like this:
export const BackgroundCanvas = ({ children }: { children?: ReactNode }) => {
return (
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
background: 'linear-gradient(to right, #e8cbc0, #636fa4)',
padding: '32px',
}}
>
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
padding: '64px',
}}
>
{children}
</div>
</div>
);
};
Since Satori (and thus Vercel OG) does not support all CSS properties, styling your components seems a bit rough at first. But since OG images are usually quite simple, I did not run into any problems due to that.
There is also an experimental Tailwind CSS example which may be interesting to you.
The ProfileContent
component is a more advanced example that also embeds images and custom fonts:
export const ProfileContent = () => {
return (
<div
style={{
display: 'flex',
height: '100%',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', marginTop: '80px' }}>
<img
alt="Vercel"
height={180}
src="data:image/svg+xml,%3Csvg width='116' height='100' fill='black' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M57.5 0L115 100H0L57.5 0z' /%3E%3C/svg%3E"
width={180}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
marginLeft: '48px',
}}
>
<span
style={{
fontSize: '56px',
color: '#222',
paddingTop: '32px',
fontFamily: "'Noto Sans', sans-serif",
fontWeight: 700,
}}
>
Your Name
</span>
<span
style={{
fontSize: '28px',
color: '#222',
fontFamily: "'Noto Sans', sans-serif",
fontWeight: 500,
}}
>
Frontend Developer
</span>
</div>
</div>
<span
style={{
fontSize: '18px',
color: '#222',
fontFamily: "'Noto Sans', sans-serif",
fontWeight: 500,
}}
>
yourname.com
</span>
</div>
);
};
To resolve the specified fontFamily
properties correctly, we need to load those fonts in our og.tsx
file too:
const fetchNotoSansMedium = fetch(
new URL('../../public/fonts/NotoSans-Medium.ttf', import.meta.url).href
).then(res => res.arrayBuffer());
const fetchNotoSansBold = fetch(
new URL('../../public/fonts/NotoSans-Bold.ttf', import.meta.url).href
).then(res => res.arrayBuffer());
const handler: NextApiHandler = async () => {
const NotoSansBold = await fetchNotoSansBold;
const NotoSansMedium = await fetchNotoSansMedium;
try {
return new ImageResponse(
(
<BackgroundCanvas>
<ProfileContent />
</BackgroundCanvas>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Noto Sans',
data: NotoSansBold,
style: 'normal',
weight: 700,
},
{
name: 'Noto Sans',
data: NotoSansMedium,
style: 'normal',
weight: 500,
},
],
}
);
} catch {
return new Response(`Failed to generate the image`, {
status: 500,
});
}
};
By placing our font files inside the public
directory, we can fetch those inside our handler and pass the data along. The ImageResponse
configuration allows us to specify multiple fonts using the fonts
array.
If you hit your /api/og
route again, you get this beautiful OG image:

Adding The OG Image To Your Site
With the API route in place, we can add our generated OG image to our site. In Next.js, you can embed any meta properties with the Head
component.
Let’s assume you have a Layout
component that wraps every page. To add the OG image to your site, you could extend your component like this:
const Layout = ({ children }: Props) => {
return (
<>
<Head>
<meta property="og:image" content={`${BASE_URL}/api/og`} />
</Head>
<div className="min-h-screen">
<main>{children}</main>
</div>
<Footer />
</>
);
};
In this example, BASE_URL
is a variable that specifies the domain of our site based on the environment. If we run the site in development mode, this will be http://localhost:3000
. In production, it will be whatever domain you host your site on.
Generating Dynamic OG Images For Blog Posts
Lastly, we want to create dynamic OG images for each individual blog post. Each image should show a little preview of the blog post title and its author.
To do that, we can extend our API route so that it accepts query params. Thus, we can call our OG image generation with dynamic data.
const handler: NextApiHandler = async req => {
const NotoSansBold = await fetchNotoSansBold;
const NotoSansMedium = await fetchNotoSansMedium;
const { searchParams } = new URL(req.url);
const hasTitle = searchParams.has('title');
const title = hasTitle ? searchParams.get('title') : undefined;
const Content = hasTitle ? (
<BlogPostContent title={title} />
) : (
<ProfileContent />
);
try {
return new ImageResponse(<BackgroundCanvas>{Content}</BackgroundCanvas>, {
width: 1200,
height: 630,
fonts: [
{
name: 'Noto Sans',
data: NotoSansBold,
style: 'normal',
weight: 700,
},
{
name: 'Noto Sans',
data: NotoSansMedium,
style: 'normal',
weight: 500,
},
],
});
} catch {
return new Response(`Failed to generate the image`, {
status: 500,
});
}
};
We can access the request and retrieve the title
param from it. If there is a title specified, we simply render a different component. Pretty easy, isn’t it?
If we now call our API again with the following request: /api/og?title=Generating Dynamic OG Images For Your Blog
, we get this image in return.

Similarly to our first OG image, you can embed the dynamically generated OG image on your blog post page using the Head
component:
export default function Post({ post, preview }: Props) {
const title = `${post.title} | Next.js Blog Example with ${CMS_NAME}`;
return (
<Layout preview={preview}>
<Container>
<Header />
<article className="mb-32">
<Head>
<title>{title}</title>
<meta
property="og:image"
content={`${BASE_URL}/api/og?title=${title}`}
/>
</Head>
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
/>
<PostBody content={post.content} />
</article>
</Container>
</Layout>
);
}
Now you automatically get an individual OG image for each blog post 🎉
With @vercel/og
, OG image generation got so much easier. If you want to learn more about, have a look at the documentation.
If you want to check out the full repository of our example, here it is:
https://github.com/konstantinmuenster/next-js-blog-with-vercel-og-image