Top 5 SEO Optimization Tips in Next.js 14

Since the original release of Next.js there have been some major version changes and improvements to the framework in the area of SEO optimization.
After the release of Next.js 13 there has been a lot of focus to support React Server Components (RSC). In this article we'll take a look at the top 5 SEO optimization tips for Next.js, including:
- Improved page title and templates
- Simplified SEO Metadata support
- Social Media Optimization with Open Graph
- Static image optimization
- Sitemap generation
Read on to learn about these essential common tips and best practices for SEO optimization in Next.js.
What is Next.js SEO optimization?
Next.js has updated its SEO optimization API which has been revised to be more in alignment with React Server Components (RSC)
.
As writing Server Side Rendered (SSR) code becomes much easier with RSC in Next.js, the SEO API is now also RSC.
Why is Next.js SEO optimization important?
The new framework API changes make it very simple to provide extensive coverage for page SEO optimization, including:
- Image caching
- Simplified
Static Site Generation (SSG)
- Dynamic page titles with templating
- Full metadata coverage
- Reduce
Content Layout Shifting (CLS)
- Better web presence on social platforms
- and more
The good news is that the new API changes provides these capabilities. So you can focus on development rather than building your own API or third party library to do the job.
Tip 1 - Page Titles
Page titles and descriptions are probably one of the primary ways that a website can convey the intent of the page content to the user as well as search engines.
Why use page titles?
The primary reasons to use unique page titles are:
- Provides meaningful bookmark titles for users
- Represents the primary topic of your page content for users and search engines
- If you're blogging, the title should match your opening
<h1>
tag
How to add page titles in Next.js
Next.js has changed the way it sets page titles. It used to dynamically hoist them on the client side but now they must be added on RSC components.
Consider the following project structure with a root layout, nested product layout and a product page.tsx
.
your-project
└── app
├── product
│ ├── page.tsx
│ └── layout.tsx
└── layout.tsx (root layout)
Using the new <Head>
component
The first change that should be made is to switch to the new <Head>
component from next/head
in the root layout, this way Next.js can manage the head tag output.
You can include a <title>
tag as default base title in the root layout.
/app/layout.tsx
import Head from 'next/head';
export const RootLayout = ({ children }) => {
return (
<html lang="en">
<Head>
<title>ACME Corp</title>
{}
</Head>
<body>
{children}
</body>
</html>
);
};
export default RootLayout;
There are two options for adding page titles.
- Use the
Metadata
title only
- Use the
Metadata
template with a placeholder
You can add titles by simply exporting a named metadata
object from any RSC component modules, including:
- layout.tsx
- page.tsx (RSC only)
Next.js will compute the title based on every nested child metadata
export it finds on route segments.
To simply add just a page title, you can just set the title
field to a string value.
Title only metadata
export const metadata: Metadata = {
title: 'ACME Corp'
}
To create a template, simply set the title
field to an object with the following fields.
template
: a string with a placehoder %s
for the page.
default
: a default title if nested pages don't provide a unique title.
Using a template allows you to still keep your website name in the title including the more specific page title.
Metadata with template
export const metadata: Metadata = {
title: {
template: '%s | ACME Corp',
default: 'ACME Corp'
}
}
Let's add a template export for the product layout so that all the product
pages can use the same template.
/app/product/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | ACME Corp',
default: 'ACME Corp'
}
}
Each nested product page that inherits the layout template metadata
from its parent route segment, can specify a unique page title and description.
/app/product/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: "Product Page",
description: "Some product page description"
}
If you access the /product
route, the title and description in the browser would read.
Product Page | ACME Corp
Some product page description
You can have as many nested metadata
exports as needed. Next.js will use the closest nested parent template in the route segments path to build the title.
A couple tips on RSC components:
- Export templates on
layout.tsx
and treat them like hubs for many pages the same relevent content.
- Export title and description
metadata
on page.tsx
RSC components.
If your page.tsx
uses the use client
indicator, you may need to rethink how you'll export the metadata
title and description since it can only be added on RSC components.
It's usually better to have page.tsx
as an RSC and break the client code into smaller nested client components if possible.
<meta>
tags, also known as Metadata
in the Next.js framework, are tags that are added to your page to further inform search engines about your content, including:
keywords
: A space delimited string of keywords.
category
: The category meta name property.
icons
: The icons for the document. Defaults to rel="icon"
.
robots
: The robots setting for the document.
viewport
: The viewport setting for the document.
authors
: a list of authors for a paritculuar page.
These are just a few of the basic meta tags from the Metadata
API but the API has very thorough coverage for just about all of the tags you'll need for SEO optimization.
Metadata tags provide information which is used by search engines to indicate the type of content on a web page.
Utimately, the actual content will determine how it ranks on a search engine's results page but the meta tags should also be added to support the type of content on your page.
All of the <head>
tags can be set by exporting the generateMetadata
function and returning the Metadata
object. This is the same Metadata
object used to define the title and template as we saw earlier.
The generateMetadata
function is used when you need to gather meta information that might be fetched async.
For example, let's say we add a details page that has a product id=1234
param and fetches the meta information.
sample JSON response
{
"id": "1234",
"title": "Product 1234",
"description": "Product 1234 description",
"author": {
"url": "example.com/url/to/bio/lc-bio",
"name": "Lloyd Christmas"
},
"keywords": ["nextjs, SEO"],
"category": "SEO Optimization",
"icons": [
"https://example.com/icon.png"
]
}
/app/product/detail/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({
params: { id },
}: {
params: { id: string };
}): Promise<Metadata> {
const { title, description, author, keywords, category, icons } = (await fetch(
`/api/product/detail/${id}`
)) as any;
return {
title,
description,
authors: [author],
keywords,
category,
icons: icons.map((url) => { rel: "icon", url }),
robots: { index: true, follow: true },
viewport: {
width: 'device-width',
initialScale: '1.0'
}
};
}
function Page({ params: { id } }: { params: { id: string } }) {
return (
<section>
<h1>Product Details</h1>
</section>
);
}
export default Page;
Accessing the new app route /product/detail/1234
will generate the following HTML tags in the head.
<head>
<title>Product 1234 | ACME Corp</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Product 1234 description" />
<meta name="keywords" content="nextjs, SEO" />
<meta name="category" content="SEO Optimization" />
<meta name="author" content="Lloyd Christmas" />
<link rel="author" href="example.com/url/to/bio/lc-bio"></link>
<link rel="icon" href="https://example.com/icon.png" />
<meta name="robots" content="index, follow" />
</head>
Using the Metadata
object makes adding a single or an array of tags using object notation very easy.
The Open Graph protocol was created by facebook to standardize the use of meta tags to improve the way links are shared on presented on social media.
X (Twitter) has its own set of meta tags to control the way card posts are displayed but will default to Open Graph meta tags if X meta tags are not present.
By defining some simple <meta>
tags, any website can greatly improve and optimize how their URL links are displayed.
Your web page content or landing page Ads might not display as expected on these platforms without these tags.
The Open Graph meta tags allow for a rich user experience and will draw more attention and trust with users on social media.
This is really essential for promotional advertisements or even blog articles but in general it's good practice and easy to do on Next.js using the Metadata
object.
For example, here's what a post with Open Graph meta tags would look like.
And here's an example without Open Graph.
Without these tags, social media platforms will just use the standard meta tags and probably won't display as you would expect.
The great thing about the new API is that everything is built into the Metadata
object, including Open Graph support.
So now we can build on the previous meta tags and add some Open Graph tags to the example.
Here's a revised version of generateMetadata
function we created earlier.
/app/product/detail/[id]/page.tsx (revised)
import { Metadata } from 'next';
export async function generateMetadata({
params: { id },
}: {
params: { id: string };
}): Promise<Metadata> {
const siteUrl = "https://acme.com";
const siteName = "ACME";
const { title, description, author, keywords, category, icons } = (await fetch(
`/api/product/detail/${id}`
)) as any;
return {
title,
description,
authors: [author],
keywords,
category,
icons: icons.map((url) => { rel: "icon", url }),
robots: { index: true, follow: true },
viewport: {
width: 'device-width',
initialScale: '1.0'
}
openGraph: {
type: 'website',
title,
description,
url: siteUrl,
siteName: siteName,
},
twitter: {
title,
description,
creator: author?.name,
card: 'summary_large_image',
site: siteName,
},
};
}
function Page({ params: { id } }: { params: { id: string } }) {
return (
<section>
<h1>Product Details</h1>
</section>
);
}
export default Page;
By addding these additional fields to the Metadata
object response, it will generate the following tags in the <head>
, assuming we are using the same JSON sample product data.
Generated Open Graph and Twitter meta tags
<head>
<title>Product 1234 | ACME Corp</title>
<meta name="description" content="Product 1234 description" />
<meta name="keywords" content="nextjs, SEO" />
<meta name="category" content="SEO Optimization" />
<meta name="author" content="Lloyd Christmas" />
<link rel="author" href="example.com/url/to/bio/lc-bio"></link>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://example.com/icon.png" />
<meta name="robots" content="index, follow" />
<meta property="og:title" content="Product 1234 | ACME Corp">
<meta property="og:description" content="Product 1234 description">
<meta property="og:url" content="https://acme.com">
<meta property="og:site_name" content="ACME Corp">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="ACME Corp">
<meta name="twitter:creator" content="Lloyd Christmas">
<meta name="twitter:title" content="Product 1234 | ACME Corp">
<meta name="twitter:description" content="Product 1234 description">
</head>
The Open Graph and Twitter types in Next.js are quite extensive, so I've narrowed it down to the bare minimum you'll need.
To get more information on these types, you can inspect the Metadata
module and see the metadata-interface.d.ts
file that opens in your IDE.
The Metadata
object can even generate arbitrary <meta>
tags by using the other
object key.
For example:
Adding arbitrary meta tags
export function generateMetadata(): Metadata {
return {
other: {
'some:arbitrary:key': 'SEO Optimization'
}
}
}
Generated arbitrary meta tag
<head>
<meta name="some:arbitrary:key" content="SEO Optimization">
</head>
Tip 4 - Use a Sitemap
A sitemap.xml
file is what search engines will use as an index to discover all of the available public pages that you want the search engine bots to crawl and parse.
Here's a snippet from the Next.js docs of what a sitemap.xml file looks like.
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://acme.com</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
<changefreq>yearly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://acme.com/about</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://acme.com/blog</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
Here's what these fields represent:
loc
: Url to access the page.
lastmod
: Used to indicate when the file was last modified.
changefreq
: This field represents the change frequency of modifications.
priority
: This is the imporance of the url compared to the other urls on the site and it ranges from 0.0 - 1.0.
Most of the research says lastmod
, changefreq
and priority
are most likely ignored by search engines since they might not be properly maintained by Webmasters.
As long as url
and lastmod
are added to this list is probably good enough.
Why do I need a sitemap?
As search engine bots crawl your website they can only crawl to other pages if there is a relative link from one page to the other page. However, this is not always the case and many pages will only be accessible by direct link only.
So a sitemap.xml file serves as a complete index of all the available URL's you want the bot to crawl and parse.
It can contain up to 50,000 URL entries but if you need more you can generate multiple sitemap.xml pages or have nested sitemap.xml files in subdirectories.
How to create a sitemap in Next.js
Next.js provides the sitemap.ts
file and will call the default exported function at build time and automatically generate a sitemap.xml.
You can have multiple sitemaps.ts
at any route segment but in most cases, placing one at the app
root is usually sufficient enough for most websites.
Here's a simple example of how a blog site might generate a sitmap.xml file.
/app/sitemap.ts
import { MetadataRoute } from 'next';
import { getPosts } from '@/lib/blog/posts';
import moment from 'moment';
export default async function sitemap(): MetadataRoute.Sitemap {
const siteUrl = "https://acme.com";
const posts = await getPosts().map((post) => {
const {slug, dateModified} = post;
return {
url: `${siteUrl}/blog/${slug}`,
dateModified: moment(`${dateModified}`).toDate()
};
});
return [
{
url: `${siteUrl}/dashboard`,
lastmod: moment().toDate()
},
{
url: `${siteUrl}/resources`
lastmod: moment().toDate()
},
{
url: `${siteUrl}/blog`
lastmod: moment().toDate()
},
...posts
];
}
And the output for the sitemap.xml
would be as follows.
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://acme.com/dashboard</loc>
<lastmod>2024-03-15T07:52:57.880Z</lastmod>
</url>
<url>
<loc>https://acme.com/resources</loc>
<lastmod>2024-03-15T07:52:57.880Z</lastmod>
</url>
<url>
<loc>https://acme.com/blog</loc>
<lastmod>2024-03-15T07:52:57.880Z</lastmod>
</url>
<url>
<loc>https://acme.com/blog/blog_1</loc>
<lastmod>2024-03-15T07:52:57.880Z</lastmod>
</url>
<url>
<loc>https://acme.com/blog/blog_2</loc>
<lastmod>2024-03-28T19:24:55.853Z</lastmod>
</url>
</urlset>
Tip 5 - Use Next.js Image API
In the latest version of Next.js they have provided a finalized version of their <Image>
API tag, making it very easy to optimize image resources.
Images are no doubt the primary focal point of websites but can also come with a performance impact.
If not used in an optimized way they can also have an unpleasent end user experience with content shifting around during loading.
Slow page load times can also have an impact on SEO page ranking. The longer it takes your page to load, the lower it might be ranked on a search engine results page (SERP).
Why do I need Next.js Image API?
The <Image>
component from the next/image
API provides some benefits, including:
- Image caching at build time.
- Improved Cumulative Layout Shift (CLS) (a.k.a Content Layout Shift) which greatly reduces page shift during image loading.
- Optimized image content types like
image/avif
and image/webp
.
- Remote image resource loaders.
- Accessibility tag improvements.
- and more, just to name a few.
How to create a Next.js Image
To use the <Image>
component, simply import from the next/image
and also import the local image from your public
directory.
For this example I'll just import a local hero background image.
- Dimension: 1200 x 800
- File Size: 63KB
- Content type:
image/jpg
.
I'm passing the following props to the <Image>
component:
src
: A local import
image.
alt
: Requires an alt attribute for Accessibility.
width
: Requires a width of the image in pixels, unless the image is local static import.
height
: Requires a height of the image in pixels, unless the image is a local static import.
The width
and height
should be used to set the optimal dimensions of the image you want. Next.js will generate an optimized image based on these settings.
You can still use classes and styles for styling to fit the web page layout.
/app/demo/page.tsx
import Image from 'next/image';
import HeroImage from '@/public/demo/home-decor-bg.jpg';
function Page() {
return (
<div>
<Image
src={HeroImage}
width={1200}
height={800}
alt="Image of SEO optimize image using Next.js image API"
className="w-full"
/>
</div>
);
}
export default Page;
The image will now be cached automatically and optimized and served through an API endpoint.
If you open a Chrome inspector network tab, you'll see that the image is now being loaded from a reserved internal Next.js endpoint for images.
localhost:3000/_next/image
After using the <Image>
component, we can now see the following details:
- Dimension: 1200 x 800
- File Size: 36KB
- Content type:
image/webp
.
So the file size has been reduced to half of its original size and the image content type is now image/webp
and fully cached.
The file size is optimized and will greatly reduce layout CLS shifting when loading.
You can imagine how larger background or banner images will benefit from this new feature.
Conclusion
Now that we've examined some of the new features in Next.js for SEO optimization and performance, I hope you can apply them to your project for some huge benefits.
Just remember that performance is an essential part of SEO optimization among other factors.
As React web application make their migration to being mostly server side rendered (SSR) or even statically render (SSG), we'll definitely see more great features from the Next.js team to accelerate web application development.