Chris.luChris.lu header image, displaying an 80s style landscape and sunset

Metadata (for tsx and mdx pages)

In this chapter, we will add metadata to our pages (and layout) by using the Next.js Metadata API, this will add meta tags inside of the <head> element of our HTML documents, which is essential to help the crawlers from search engines and social networks better understand the content of our pages. These efforts will result in improved SEO scores, which means we will be getting more traffic from organic sources

As we saw early in this tutorial, Next.js has created a layout.tsx file in the /app folder and already added a basic metadata object

Metadata in layouts

We start by editing the layout.tsx file to add some entries to the metadata object:

/app/layout.tsx
import './global.css'
import { Metadata } from 'next'
import HeaderNavigation from '@/components/header/Navigation'
import { Kablammo } from 'next/font/google'
 
export const metadata: Metadata = {
    title: {
        template: '%s | example.com',
        default: 'Home | example.com',
    },
    description: 'My description',
}

Lines 7 to 10: we edit the title meta tag, the default title value was previously a string, but we turned it into an object with two properties

Line 8: by using the title.template we ensure that all will titles have a similar structure on every page and it helps reduce repetition (DRY)

Line 9: the second property is the default title for the homepage (which is required, when using template)

The template will work for any pages that are in the same route segment as the layout, as this is the root layout of our project, it means the template will work on all our pages

When we visit one of pages, Next.js metadata will replace title.template %s placeholder with the title of the current page we are visiting

Line 11: we only changed the description default value to something else

Launch the dev server, then open the "home" page at http://localhost:3000/, and then right-click and select inspect

Look at what is inside your page's <head> element. There are, for example, some Next.js <script> tags, but also the following 4 tags are now present:

<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>Home | example.com</title>
<meta name="description" content="My description">

The viewport tag gets always added to every page by default, but you can customize the content value of the viewport using the generateViewport function:

custom viewport example
import type { Viewport } from 'next'
 
export const viewport: Viewport = {
	width: 'device-width',
	initialScale: 1,
	maximumScale: 1,
	userScalable: false,
}

The second charset tag gets also added by Next.js to every page

And then there are also the two metadata tags we added in the layout.tsx file, both title and description display the default values we have set in our layout

Now, let's have a look at the source code of our home page:

/app/page.tsx
export default function Home() {
 
    return (
        <>
            <h1>Hello World?</h1>
        </>
    )
 
}

As we saw when inspecting the <head> element of our page had meta tags, but our page.tsx file does not have any code related to the metadata object, this is because every page will inherit the metadata we have set in the layout, you only add a metadata object in the pages for which you want to override the default values from the layout or if you want to add a new metadata property that you didn't set in the layout

Metadata in typescript pages

Let's keep the home page as is and instead edit the second page we created earlier, which is the /app/blog/page.tsx file, and add some metadata to it:

/app/blog/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
    title: 'Blog page',
}
 
export default function Blog() {
 
    return (
        <>
            I&apos;m the blog page
        </>
    )
}

Line 1: we import the Metadata type to strictly type the metadata object

Lines 3 to 5: we create a metadata object and add a custom title for the blog page

Now launch the dev server, then open the blog page http://localhost:3000/blog in your browser, then right click and select Inspect

If you look at the meta tags you will notice that the title tag has used the template from our layout file and combined it with the custom title from the blog page. The description is unchanged, as we did NOT overwrite the default value from our layout

Metadata in MDX pages

When I first experimented with the new metadata object, I was wondering if it would also work in MDX pages, and YES, it does 🎉

Let's edit the last gfm playground page we created earlier and add some metadata on top:

/app/(tutorial_examples)/gfm_playground/page.mdx
export const metadata = {
    title: 'GFM playground page',
}
 
<div id="core">
 

Lines 1 to 3: we create a new metadata object and add a custom title for the GFM playground page (we do NOT import the Metadata type as MDX files are not typescript)

If you inspect the source of the playground page http://localhost:3000/gfm_playground, you will see the MDX page has the correct title (and also makes use of the template from the layout)

Congratulations 🎉 you just learned how to add metadata using layouts and pages

favicon (mobile icons)

Adding a favicon is important if you want to customize how the bookmarks of your project will look like or what the icon will be if a user adds a link to your website to their desktop

It is also very simple to add a favicon when using Next.js, all you need to do is create a favicon.ico file and then put it into your /app folder

Besides adding a favicon.ico file, you can also add apple icons. You can add more than one by adding a number at the end of their name. For example, you can put an apple-icon1.png, apple-icon2.png and apple-icon3.png files into your /app folder

Next.js will detect your app and fav icon files in the /app folder and automatically generate the meta tags (and set attributes like the content-type and dimensions) for your:

<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16">
<link rel="apple-touch-icon" href="/apple-icon1.png?foo" type="image/png" sizes="152x152">
<link rel="apple-touch-icon" href="/apple-icon1.png?bar" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="/apple-icon1.png?baz" type="image/png" sizes="512x512">

If you have an image and want to quickly create a favicon online then I recommend checking out realfavicongenerator.net

Open Graph metadata

The Open Graph protocol is essential if you want to control what gets displayed when your content gets shared, but it is also helpful if you want to improve your SEO score. In the past, it was mostly regarded as a Facebook thing, but today a lot of social networks, messengers, search engines and a bunch of services will use the open graph data that your website provides

First, let's add a default Open Graph object to our root layout file:

/app/layout.tsx
import './global.css'
import { Metadata } from 'next'
import HeaderNavigation from '@/components/header/Navigation'
import { Kablammo } from 'next/font/google'
 
export const metadata: Metadata = {
    title: {
        template: '%s | example.com',
        default: 'Home | example.com',
    },
    description: 'My description',
    openGraph: {
        url: 'https://example.com/',
        siteName: 'My website name',
        locale: 'en_US',
        type: 'website',
    },
}

Lines 12 to 17: we add an Open Graph object and set a few properties, like the permalink url of the page, the projects (blog, wesite, ...) site name, the locale of the content and the type of content that it is

Tip

As we already have a title and description in the regular metadata, we do NOT need to repeat those in the open graph data, Next.js will handle this automatically for us

Next, let's also add a metadata example to our GFM playground:

/app/(tutorial_examples)/gfm_playground/page.mdx
export const metadata = {
    title: 'GFM playground page',
    openGraph: {
        url: 'https://example.com/tutorial_examples/gfm_playground',
    },
}

Lines 2 to 5: we overwrite the URL property of the Open Graph layout metadata with the permalink of the current page

Now inspect the source of the playground page http://localhost:3000/gfm_playground, you will see that in the <head> element of our page there are the following tags:

<meta property="og:title" content="GFM playground page | example.com">
<meta property="og:description" content="My description">
<meta property="og:url" content="https://example.com/tutorial_examples/gfm_playground">

I expected to see 6 meta tags, but there are only 3, which means the openGraph object from the page did not get merged with the openGraph object of the layout. At first, I thought this was a bug, so I did a search in the Next.js issues in the Next.js GitHub repository and found an Issue Next.js issue "openGraph metadata not merging across layouts" #47540 and then 2 more Next.js issue "All properties of nested metadata objects like openGraph get overwritten even if only one was a duplicate" #46899, Next.js issue "Nested Open Graph metadata overwrites root metadata" #46434 about problems when merging openGraph of a page with the one of the layout. The issues all have comments which seem to indicate that this by design and it is not something they plan to change, as explained in this comment by a member of the Nextj.js team.

3 ways to merge Open Graph metadata

There are 3 different workarounds to merge Open Graph meta data, I will start with the most obvious one and end with one that is a bit more complex:

Solution 1: we can repeat the open graph values in every page, in which we use the static metadata export, this means manually copying the open graph we already set in the layout and adding it again to the page itself:

/app/(tutorial_examples)/gfm_playground/page.mdx
export const metadata = {
    title: 'GFM playground page',
    openGraph: {
        url: 'https://example.com/tutorial_examples/gfm_playground',
        siteName: 'My website name',
        locale: 'en_US',
        type: 'website',
    },
}

Lines 5 to 7: we manually re-add the Open Graph properties we already added in the layout

Solution 2: is to use the generateMetadata function to get the parent open graph metadata object and extend it manually:

/app/(tutorial_examples)/gfm_playground/page.mdx
export async function generateMetadata(props, parent) {
    const parentOpenGraph = (await parent).openGraph
    return {
        title: 'GFM playground page',
        openGraph: {
            ...parentOpenGraph,
            url: 'https://example.com/tutorial_examples/gfm_playground'
        }
    }
}

When I saw the await in the above code, I was afraid that using this might opt the page out of static rendering, so I used the npm run build command to make a local test build and saw that it is NOT the case:

Route (app)                                          Size     First Load JS
 /tutorial_examples/gfm_playground                1.04 kB         191 kB
  (Static)  prerendered as static content

The same code we added to the MDX page can be strictly typed if we add it to a typescript page, like so:

/app/blog/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
 
export async function generateMetadata(props: unknown, parent?: ResolvingMetadata):Promise<Metadata> {
    const parentOpenGraph = (await parent)?.openGraph
    return {
        title: 'Blog page',
        openGraph: {
            ...parentOpenGraph,
            url: 'https://example.com/blog'
        }
    }
}

Solution 3: as suggested in the metadata merging documentation, create a file with default open graph values and then always import that file:

First, we create a new shared folder in the root of the project and then inside of it a metadata.ts file, into which we add the metadata including our open graph we want to use in every page:

/shared/metadata.ts
import type { Metadata } from 'next'
 
export const sharedMetadata: Metadata = {
    openGraph: {
        url: 'https://example.com/',
        siteName: 'My website name',
        locale: 'en_US',
        type: 'website',
    }
}

Then we import the shared open graph file inside of our page(s):

/app/blog/page.tsx
import type { Metadata } from 'next'
import { sharedMetadata } from '@/shared/metadata'
 
export const metadata: Metadata = {
    title: 'Blog page',
    openGraph: {
        ...sharedMetadata.openGraph,
        url: 'https://example.com/blog'
    }
}
Tip

Make sure the sharedMetadata variable is first in the openGraph so that what comes after overwrites the default values

Congratulations 🎉 you now also have support for open graph metadata in your project

If you liked this post, please consider making a donation ❤️ as it will help me create more content and keep it free for everyone