Guide

How to add Open Graph and Twitter metadata to Next.js app router

Next.js App Router simplifies adding Open Graph and Twitter metadata. We'll walk through an easy implementation and testing in your dev setup. Let's level up your website's social game!

Published: 7 minutes read
open graph and twitter card

What is Open Graph

In a nutshell: Open Graph is an internet protocol developed by Meta that allows websites to provide additional information specifically for social media sharing. This information controls how your webpage is displayed in social media previews, including the title, description, and thumbnail image (can also have playable videos). When your website is shared on social media, it will make it look like this:

open graph example

Open Graph with the App router

The old approach

The app router differentiate massively from the pages router. One of the removed feature is the Head component, something that used to look like this:

import Head from 'next/head'

function IndexPage() {
return (
<div>
<Head>
<title>My page title</title>
</Head>
<p>Hello world!</p>
</div>
)
}

export default IndexPage
import Head from 'next/head'

function IndexPage() {
return (
<div>
<Head>
<title>My page title</title>
</Head>
<p>Hello world!</p>
</div>
)
}

export default IndexPage

There you would add manually every single metadata, from height and weight of images, to every other property.

The new approach

Now things are way easier! For example, if you want to add Open Graph and Twitter images on your homepage, you don't need to code anything. Instead, you just need to put an image (.png, .jpeg or .gif) named opengraph-image and/or twitter-image in the same directory of the page that you want the images to be (in the example I want the images to show in the home page, so I placed them in the same level as the layout.tsx).

open graph image placement

You basically just added Open Graph and Twitter images to your page! But let's also add some metadata to it.

Add more metadata

As said previously, the Head component has been removed, instead we will use the metadata object from Next.js. You just need to create a const called metadata and export it. Eventually, you can also import the types.

export const metadata: Metadata = {
title: '...',
}
export const metadata: Metadata = {
title: '...',
}

Take in mind that this approach is only available for Server Components and for statically generated pages, it won't work if the page is dynamically generated. In that case you need to call the generateMetadata function, i will explain down below.

Below there is the code that i use for my homepage, look so minimal and clean!

const title: string = "Ivan Smiths, Frontend UI/UX Developer from Wiesbaden";
const description: string =
"Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen";

export const metadata: Metadata = {
title: title,
description: description,
openGraph: {
title,
description,
type: "website",
siteName: "Ivan Smiths",
url: `https://ivansmiths.com`,
},
twitter: {
title,
description,
card: "summary_large_image",
creator: "@Ivansmiths",
creatorId: "1303746727594405894",
},
};
const title: string = "Ivan Smiths, Frontend UI/UX Developer from Wiesbaden";
const description: string =
"Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen";

export const metadata: Metadata = {
title: title,
description: description,
openGraph: {
title,
description,
type: "website",
siteName: "Ivan Smiths",
url: `https://ivansmiths.com`,
},
twitter: {
title,
description,
card: "summary_large_image",
creator: "@Ivansmiths",
creatorId: "1303746727594405894",
},
};

If you see the page source, you will see that this code rendered out a bunch of html tags:

<title>Ivan Smiths, Frontend UI/UX Developer from Wiesbaden</title>
<meta name="description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta property="og:title" content="Ivan Smiths, Frontend UI/UX Developer from Wiesbaden"/>
<meta property="og:description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta property="og:url" content="https://ivansmiths.com/"/>
<meta property="og:site_name" content="Ivan Smiths"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="2880"/>
<meta property="og:image:height" content="1558"/>
<meta property="og:image" content="http://localhost:3000/opengraph-image.png?66d44f37b83759d8"/>
<meta property="og:type" content="website"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:creator" content="@Ivansmiths"/>
<meta name="twitter:creator:id" content="1303746727594405894"/>
<meta name="twitter:title" content="Ivan Smiths, Frontend UI/UX Developer from Wiesbaden"/>
<meta name="twitter:description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta name="twitter:image:type" content="image/png"/>
<meta name="twitter:image:width" content="2880"/>
<meta name="twitter:image:height" content="1558"/>
<meta name="twitter:image" content="http://localhost:3000/twitter-image.png?66d44f37b83759d8"/>
<title>Ivan Smiths, Frontend UI/UX Developer from Wiesbaden</title>
<meta name="description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta property="og:title" content="Ivan Smiths, Frontend UI/UX Developer from Wiesbaden"/>
<meta property="og:description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta property="og:url" content="https://ivansmiths.com/"/>
<meta property="og:site_name" content="Ivan Smiths"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="2880"/>
<meta property="og:image:height" content="1558"/>
<meta property="og:image" content="http://localhost:3000/opengraph-image.png?66d44f37b83759d8"/>
<meta property="og:type" content="website"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:creator" content="@Ivansmiths"/>
<meta name="twitter:creator:id" content="1303746727594405894"/>
<meta name="twitter:title" content="Ivan Smiths, Frontend UI/UX Developer from Wiesbaden"/>
<meta name="twitter:description" content="Ivan Smiths - Frontend UI/UX Developer - 3 years of experience. Seeking the limit. Currently at TD Cowen"/>
<meta name="twitter:image:type" content="image/png"/>
<meta name="twitter:image:width" content="2880"/>
<meta name="twitter:image:height" content="1558"/>
<meta name="twitter:image" content="http://localhost:3000/twitter-image.png?66d44f37b83759d8"/>

That's pretty convenient! As you can see, it also added width and height to both images. If you also want to add an alt text to the images , you need to add a .txt file called respectively opengraph-image.alt and/or twitter-image.alt with the alternative text written inside.

Add Open Graph and Twitter images to dynamic pages

Often you will end up creating pages trough dynamic routes with a slug or id used as params (e.g. page.tsx file inside a directory named [slug] or [id]). In these cases, as previously mentioned, we need to use the generateMetadata function. The skeleton of the function looks something like this:

export async function generateMetadata({ params }) {
// the logic on how you fetch your data
let post = getBlogPosts().find((post) => post.slug === params.slug);
})
export async function generateMetadata({ params }) {
// the logic on how you fetch your data
let post = getBlogPosts().find((post) => post.slug === params.slug);
})

The generateMetadata accept the params, which you need to fetch inside the function. In this case I am importing the function is use for fetching the slug for each blogpost I wrote.

This is the code I use for each blogpost page:

type Params = {
params: {
slug: string;
};
};


export async function generateMetadata({
params,
}: Params): Promise<Metadata | undefined> {
let post: Posts | undefined = getBlogPosts().find(
(post) => post.slug === params.slug,
);
// if no posts are available return null
if (!post) {
return;
}
// destructure the properties
let { title, publishedAt: publishedTime, excerpt, tags } = post.metadata;
// the actual Open Graph images
const ogImage = [
{
url: `https://ivansmiths.com/blog/${post.slug}/cover.png`,
height: post.metadata.coverHeight,
width: post.metadata.coverWidth,
alt: title,
},
];

return {
title,
description: excerpt,
openGraph: {
title,
description: excerpt,
type: "article",
authors: ["Ivan Smiths"],
tags: tags,
publishedTime,
url: `https://ivansmiths.com/blog/${post.slug}`,
images: ogImage,
},
twitter: {
card: "summary_large_image",
title,
creator: "@Ivansmiths",
creatorId: "1303746727594405894",
description: excerpt,
images: ogImage,
},
};
}

type Params = {
params: {
slug: string;
};
};


export async function generateMetadata({
params,
}: Params): Promise<Metadata | undefined> {
let post: Posts | undefined = getBlogPosts().find(
(post) => post.slug === params.slug,
);
// if no posts are available return null
if (!post) {
return;
}
// destructure the properties
let { title, publishedAt: publishedTime, excerpt, tags } = post.metadata;
// the actual Open Graph images
const ogImage = [
{
url: `https://ivansmiths.com/blog/${post.slug}/cover.png`,
height: post.metadata.coverHeight,
width: post.metadata.coverWidth,
alt: title,
},
];

return {
title,
description: excerpt,
openGraph: {
title,
description: excerpt,
type: "article",
authors: ["Ivan Smiths"],
tags: tags,
publishedTime,
url: `https://ivansmiths.com/blog/${post.slug}`,
images: ogImage,
},
twitter: {
card: "summary_large_image",
title,
creator: "@Ivansmiths",
creatorId: "1303746727594405894",
description: excerpt,
images: ogImage,
},
};
}

As you can see, in this case I just create a const called ogImages, where I fetch the images and its height, width and alt text and used that constant as a value on each images key in the openGraph and twitter objects.

Testing and validation with Vercel

If you are hosting your website in Vercel, you can actually test and validate the metadata on each deployment. For doing this, select your 'project' in the 'dashboard', go to 'Deployments' and select the deployment that you want to test. Once selected, you will see in the navbar on the top, the Open Graph link.

open graph image placement

With this, you ensure that your website will look nice and clan when shared on social media.