Skip to main content

Generate dynamic Open Graph images in Next.js

Published ago
Updated ago
10 min read
Caution

This tutorial is written using the page router, the steps to follow may be somewhat different in the app router.

If you have ever paid attention, there is a very useful functionality in most social media sites like Facebook, or Twitter X, and it is that when an address is written it automatically loads a preview of the content of that address, being in most cases a card with an image and the title of the publication, for example:

Example of Open Graph card
Example of Open Graph card

These cards are very useful because you can show a preview of the article so that your readers know easily and quickly what your link will be about, in most cases you can use custom images for each article, but this becomes tedious (besides that logically it will consume time,) so for this blog I decided that I wanted to use a simple template and that only the text shown in the image changes.

Of course, I could save the template in Photoshop and simply update it with a new text each time I publish a new article, but this brings me two problems:

  1. As mentioned earlier, it is the loss of time that results in creating a new image for each post
  2. Each image needs to be uploaded to the server, which generates loss of space

Fortunately, I found a fairly simple way to create dynamic images for Open Graph in Next.js, below I show how I achieved the result of this Blog.

Note

Open Graph?

Open Graph is a protocol originally created by Facebook to standardize the use of metadata (data from a site,) for use by other sites, in other words it allows many sites to consume some data from your site in a predictable way, so as not to have to guess where to get each thing.

I won't go into much detail about the protocol itself, since we only need a title and an image for this tutorial, but if you have any questions don't hesitate to contact me ☺️️.

STEP 1: CREATE THE ROUTE IN REST

In broad strokes, what we are going to do is create a route in rest that generates the image we need automatically based on the title we send, so we simply link the route with the necessary parameters to our open graph tag.

We start by creating our file, this should be located in pages/api/og.tsx.

To create a route, all we need to do is add the following code to our file:

1/**
2 * Next.js dependencies
3 */
4import { NextApiRequest, NextApiResponse } from 'next';
5
6function handler(
7 req: NextApiRequest,
8 res: NextApiResponse
9) {
10 res.status(200).json({ message: '¡Hola Mundo!' })
11}
12
13export default handler;
14
15

Now if we open the route at http://localhost:3000/api/og we will find a small JSON code, which is exactly what we sent to our route:

1{"message": "¡Hola Mundo!"}
2

Here is where we will place our images, so now that our route is created, what we need to do is use the magic of ImageResponse1

Note

Where are my styles?

You may have noticed from our JSON response in the browser it doesn't have any other components or any of the styles that our site should have. This happens because the /pages/api folder is special in Next.js, it tells the compiler that everything inside is in a separate section of our site (the rest routes) and that it should form its own response.

STEP 2: CONVERT A COMPONENT INTO AN IMAGE

What ImageResponse does, in a nutshell, is convert any component into an image before displaying it in the browser. This is precisely what we need since it allows us to place the title we want inside our image, here is where the magic happens.

Starting from our previous code, we will make some modifications to return an image instead of our JSON:

1/**
2 * Next.js dependencies
3 */
4import { ImageResponse } from 'next/og';
5
6/**
7 * Necessary to avoid an error
8 * when running in the Node.js runtime
9 */
10export const config = {
11 runtime: 'edge',
12};
13
14function handler() {
15 return new ImageResponse(
16 (
17 <div style={{
18 alignItems: 'center',
19 backgroundColor: 'rgb(24 24 27/1)',
20 color: 'white',
21 display: 'flex',
22 justifyContent: 'center',
23 height: '100%',
24 width: '100%',
25 }}>
26 <h1>¡Hola Mundo!</h1>
27 </div>
28 ),
29 {
30 width: 1200,
31 height: 630,
32 }
33 );
34}
35
36export default handler;
37
Tip

When we see this:

1{
2 width: 1200,
3 height: 630,
4}
5

We are referring to the size the final image will have, this can be any size you want but 1200x630 is common in Open Graph.

Now if we open our API page, we will see this in the browser:

Example of ImageResponse
Real image generated by `ImageResponse`
Note

As you have probably noticed, we are using the style attribute directly in the component. This happens because as we cannot load any styles in the image, we must include them directly in the component.

This works very well in most cases, but it has limitations, make sure to check the documentation to have a list of functional properties.

Important

In some cases, when you return more than one component, if you have an error and the image does not load, try adding display: flex explicitly to the parent element (as I did in the previous example). Errors can be quite difficult to find but you will find a log in the console if you are in development mode.

Example of error buried in log
Example of error buried in the log

STEP 3: ADDING STYLE TO OUR IMAGE

We are almost ready, we just need to add a little more style to our image, and since we will simply convert a component into an image, there is nothing stopping us from adding another image as well:

1/**
2 * Next.js dependencies
3 */
4import { ImageResponse } from 'next/og';
5
6/**
7 * The img tag needs an absolute URL
8 */
9const SITE_URL = 'https://marioaguiar.net';
10
11/**
12 * Necessary to avoid an error
13 * when running in the Node.js runtime
14 */
15export const config = {
16 runtime: 'edge',
17};
18
19function handler() {
20 return new ImageResponse(
21 (
22 <div
23 style={{
24 backgroundColor: 'rgb(24 24 27/1)',
25 display: 'flex',
26 fontFamily: 'Raleway, sans-serif',
27 gap: 16,
28 }}
29 >
30 <div
31 style={{
32 alignItems: 'center',
33 color: 'white',
34 display: 'flex',
35 flexDirection: 'column',
36 fontSize: 36,
37 height: 630,
38 justifyContent: 'center',
39 padding: 16,
40 textAlign: 'center',
41 width: 800,
42 }}
43 >
44 <h1
45 style={{
46 fontWeight: 600,
47 }}
48 >
49 ¡Hola Mundo!
50 </h1>
51
52 <p>
53 marioaguiar.net
54 </p>
55 </div>
56
57 <div
58 style={{
59 display: 'flex',
60 alignItems: 'center',
61 justifyContent: 'center',
62 }}
63 >
64 <img
65 width={400}
66 height={400}
67 src={`${SITE_URL}/mariobw-og.jpg`}
68 alt='Mario Aguiar'
69 />
70 </div>
71 </div>
72 ),
73 {
74 width: 1200,
75 height: 630,
76 }
77 );
78}
79
80export default handler;
81
Example of styled image

STEP 4: ADDING POST DATA

Finally (and the point of the whole tutorial really,) we must add the data of our post to the image, and for this we simply need to modify the code a bit to receive parameters and use them in the image:

1/**
2 * Next.js dependencies
3 */
4import { ImageResponse } from 'next/og';
5import { NextApiRequest } from 'next';
6
7/**
8 * The img tag needs an absolute URL
9 */
10const SITE_URL = 'https://marioaguiar.net';
11
12/**
13 * Necessary to avoid an error
14 * when running in the Node.js runtime
15 */
16export const config = {
17 runtime: 'edge',
18};
19
20async function handler(req: NextApiRequest): Promise<ImageResponse> {
21 // Receive the parameters from the URL.
22 const { searchParams } = new URL(req.url || '');
23 const title = searchParams.get('title') || 'Mario Aguiar';
24
25 return new ImageResponse(
26 (
27 <div
28 style={{
29 backgroundColor: 'rgb(24 24 27/1)',
30 display: 'flex',
31 gap: 16,
32 fontFamily: 'Raleway, sans-serif',
33 }}
34 >
35 <div
36 style={{
37 display: 'flex',
38 flexDirection: 'column',
39 fontSize: 36,
40 alignItems: 'center',
41 justifyContent: 'center',
42 width: 800,
43 height: 630,
44 color: 'white',
45 padding: 16,
46 textAlign: 'center',
47 }}
48 >
49 <h1
50 style={{
51 fontWeight: 600,
52 }}
53 >
54 {
55 <span style={{
56 textTransform: 'uppercase',
57 }}>
58 {title}
59 </span>
60 }
61 </h1>
62
63 <p>
64 marioaguiar.net
65 </p>
66 </div>
67
68 <div
69 style={{
70 display: 'flex',
71 alignItems: 'center',
72 justifyContent: 'center',
73 }}
74 >
75 <img
76 width={400}
77 height={400}
78 src={`${SITE_URL}/mariobw-og.jpg`}
79 alt='Mario Aguiar'
80 />
81 </div>
82 </div>
83 ),
84 {
85 width: 1200,
86 height: 630,
87 }
88 );
89}
90
91export default handler;
92

And finally, if we enter /api/og?title=lorem ipsum we will have our final result:

Example with title

BONUS: IMPROVING TYPOGRAPHY

I honestly thought about finishing the tutorial here, but just before sitting down to write today, I discovered that in addition to some few styles we used, ImageResponse also accepts a slightly more basic version of typography editing, I don't know how recommended it is since you have to take into account performance issues, but I will still show you how it is done.

For this, it is necessary to have the font file somewhere on our server (or access to a cdn could also work.) Once we choose our font, we must load the contents of the file in Javascript, and pass it to ImageResponse in the configuration:

1/**
2 * Next.js dependencies
3 */
4import { ImageResponse } from 'next/og';
5import { NextApiRequest } from 'next';
6
7/**
8 * The img tag needs an absolute URL
9 */
10const SITE_URL = 'https://marioaguiar.net';
11
12/**
13 * Necessary to avoid an error
14 * when running in the Node.js runtime
15 */
16export const config = {
17 runtime: 'edge',
18};
19
20async function handler(req: NextApiRequest): Promise<ImageResponse> {
21 const { searchParams } = new URL(req.url || '');
22 const title = searchParams.get('title') || 'Mario Aguiar';
23
24 // Load the content of the typography in buffer.
25 const ralewayBlack = await fetch( new URL('./fonts/Raleway-Black.ttf', SITE_URL) )
26 .then((res) => res.arrayBuffer());
27
28 return new ImageResponse(
29 (
30 <div
31 style={{
32 backgroundColor: 'rgb(24 24 27/1)',
33 display: 'flex',
34 gap: 16,
35 // We specify the typography to use.
36 fontFamily: 'Raleway, sans-serif',
37 }}
38 >
39 <div
40 style={{
41 display: 'flex',
42 flexDirection: 'column',
43 fontSize: 36,
44 alignItems: 'center',
45 justifyContent: 'center',
46 width: 800,
47 height: 630,
48 color: 'white',
49 padding: 16,
50 textAlign: 'center',
51 }}
52 >
53 <h1
54 style={{
55 fontWeight: 600,
56 }}
57 >
58 {
59 <span style={{
60 textTransform: 'uppercase',
61 }}>
62 {title}
63 </span>
64 }
65 </h1>
66
67 <p>
68 marioaguiar.net
69 </p>
70 </div>
71
72 <div
73 style={{
74 display: 'flex',
75 alignItems: 'center',
76 justifyContent: 'center',
77 }}
78 >
79 <img
80 width={400}
81 height={400}
82 src={`${SITE_URL}/mariobw-og.jpg`}
83 alt='Mario Aguiar'
84 />
85 </div>
86 </div>
87 ),
88 {
89 width: 1200,
90 height: 630,
91 fonts: [
92 // We add the data of our typography.
93 { data: ralewayBlack, name: 'Raleway,' weight: 900 },
94 ]
95 }
96 );
97}
98
99export default handler;
100

And with this, we will have a slightly more personalized typography:

Example with personalized typography
Example using the Raleway typography

CONCLUSION

And with this, we have finished, I hope this tutorial has been helpful to you, and if you have any questions do not hesitate to contact me. See you!