Optimizing Metadata and SEO in Next.js for Your XM Cloud Website

Discover how Next.js transforms metadata management for better search engine visibility and improved user experience.

October 6, 2023

By David Austin

There’s been some interesting advancements in Next.js and some of their offerings, specifically around metadata, and I wanted to cover it. Especially when I think it goes without saying, metadata is one of those things we tend to leave to the bitter end. In some cases, there’s good reasoning, but more often than not, it’s good to get it out of the way up front.

What’s the Importance of Metadata?

I think it goes without saying that the title and description of a website are crucial in SEO. Without them, search engines would not be able to determine what your website is about and, more than likely, would just ignore it or give it a low ranking.

But metadata isn’t just about the title and description. These days it’s also the direct way social media platforms enable sharing of your content on their platforms in an efficient and standard manner. It also isn’t just the stuff that exists in the <head></head> of a web page. Everything from ld+json, robots.txt, sitemap.xml, and where appropriate a manifest.json. The more information we provide, the better chances we have at ensuring our content can be seen by those who are looking. The web is a big scary place; let’s make sure the light shines where it needs to be.

Optimizing Metadata and SEO In Next.js

So, let’s explore how we go about using these within Next.js.

How to Use Meta Tags in Next.js

Next.js offers two ways of generating metadata tags. You can generate them statically or dynamically. Statically though, it’s not what you think. You’re not writing HTML, such as <meta name="something" ….

Now don’t fret. You still can generate HTML meta tags, if you so desire, but the preferred, supported, and recommended method is via the new Metadata types we’ll go through below.

Generating Static Metadata

Setting this up is remarkably simple. You can effectively do this on any layout.tsx

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'ABC Company',
  description: 'Welcome page for ABC Company',
}

Now, that’s the simple stuff. You can get really wild with this, and remarkably efficient. This isn’t just a one-for-one representation. You can even go as far as defining default values.

Let’s say we want ABC Company to appear at the end of every page. For example: “News Releases - ABC Company” but we want to do it in a way that if down the road we need to rebrand, we can do it simply. Well, we can.

import { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s - ABC Company',
    default: 'ABC Company', // this is a default value if no title is present
  },
}

Subsequent child pages can now simply provide a title and the - ABC Company will be appended.

import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'News Releases',
}

What about more complex metadata such as OpenGraph or Twitter / X? Well, Next.js has us covered there as well.

export const metadata: Metadata = {
    openGraph: {
    title: 'News Releases - ABC Company',
    description: 'The amazing ABC Company',
    siteName: 'ABC Company',
    url: 'https://abccompany.com',
    images: [
      {
        url: 'https://abccompany.com/socialmedia.png',
        width: 800,
        height: 600,
      },
      {
        url: 'https://abccompany.com/socialmedia-alt.png',
        width: 1800,
        height: 1600,
        alt: 'ABC Company',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
    twitter: {
    card: 'summary_large_image',
    title: 'News Releases - ABC Company',
    description: 'The amazing ABC Company',
    siteId: '1467344254880',
    creator: '@abccompanytwitter',
    creatorId: '1467344254880',
    images: ['https://abccompany.com/socialmedia.png'],
  },
}

What is Dynamic Metadata in Next.js, and How Can We Use It?

The last thing I think of being dynamic is metadata. In my brain, it’s always static information, as they say. Data of data based upon the page you’re on. While it certainly appears that Static Metadata is likely all you need, when you see what I’m about to show you, you’ll realize that the dynamic approach is the better method for Sitecore Headless as you’ll be able to take what’s part of the page’s layout data and incorporate it.

Let’s explore. First, let’s create a header component, as it would be something that would be on every page.

We’re going to use the generateMetadata server function to pull in appropriate data about the item we’re on.

import { Metadata } from "next";

type Props = {
  fields: { pageTitle: Field<string>; };
}

export const generateMetadata = ({ fields }: Props): Metadata => {
  return {
    title: `${fields.pageTitle.value} - ABC Company`,
  };
};

export default function ABCHeader() {
  // Builds Header component for ABC website
}

Now, any page that has ABCHeader component on it. It will have the following inside the <head></head> elements.

<head>
<title>A page - ABC Company</title>
</head>
...

Because the generateMetadata is computed server side, there’s really not much you can do. Ultimately the page won’t render until generateMetadata has finished. So while, YES, you can do a fetch inside of it, do you need to? Really? If you get too crazy, this is definitely a point where your application’s or website’s performance could suffer.

Let’s say you are using a wildcard setup, and you need your generateMetadata to fetch an item from outside the site tree. Supplemental information, as it were, helps improve the overall SEO score. What’s important to know is that inside this function, all fetch calls are “memoized”. What this actually means in layman’s terms is any fetch call with the same URL is made (that includes the parameters/query string), across your application or website, such that even if it ends up being called multiple times, it’s only really called once. Now you can prevent this from happening, but the whole point of memoization is such that your app doesn’t have to continually fetch the same data over and over again.

Exploring Sitemaps, Robots, and LD+Json aka JSON for Linking Data

Both Sitemap.xml and Robots.txt can be generated by using MetadataRoute. By using this method, we’re able to very quickly generate a sitemap.xml file in combination with route mapping.

Examining MetadataRoute.Sitemap

If you’re using the default XM Cloud setup, you will likely find a sitemap.js. If we replace the sitemap.js file and route to it appropriately in the next.config.js file, we can use the MetadataRoute.Sitemap type to accurately format the XML such that you don’t have to worry about formatting.

import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://abccompany.com',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1,
    },
    {
      url: 'https://abccompany.com/news',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 0.8,
    },
    {
      url: 'https://abccompany.com/careers',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.5,
    },
  ]
}

The beauty is we could very well, using a fetch or Graph QL call, generate the appropriate JSON data represented here.

Examining MetadataRoute.Robots

Similarly, for robots.txt, we can generate this file dynamically as needed. We could pull this data using a Graph QL call from an item in Sitecore if we wanted to.

import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/top-secret/',
    },
    sitemap: 'https://abccompany.com/sitemap.xml',
  }
}

By utilizing the MetadataRoute.Robots type, we can amazingly produce the necessary output in the precise formatting.

User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://acme.com/sitemap.xml

And because it supports arrays, you can build it accordingly if you say, want or need to get specific with particular crawlers.

import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
            {
          userAgent: 'Googlebot',
          allow: '/',
          disallow: '/top-secret/',
        },
            {
          userAgent: 'Googlebot-Image',
          disallow: '/marketing/',
        },
        ],
    sitemap: 'https://abccompany.com/sitemap.xml',
  }
}

Understanding MetadataRoute.Manifest

Just like robots and sitemaps, the manifest is organized the same way utilizing the MetadataRoute.Manifest type.



Image of Fishtank employee David Austin

David Austin

Development Team Lead | Sitecore Technology MVP x 3

David is a decorated Development Team Lead with Sitecore Technology MVP and Coveo MVP awards, as well as Sitecore CDP & Personalize Certified. He's worked in IT for 25 years; everything ranging from Developer to Business Analyst to Group Lead helping manage everything from Intranet and Internet sites to facility management and application support. David is a dedicated family man who loves to spend time with his girls. He's also an avid photographer and loves to explore new places.