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.
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: <span class="hljs-subst">${fields.pageTitle.value}</span> - 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.