The NextJS app router SEO features: Metadata, sitemaps, robots.txt, structured data…
SEO improvements were one of NextJS’s main promises. These were reiterated with version 13 and the app
router.
The improvements include greater speed and reduced javascript thanks to React Server Components. However, some features of the app router aim to address SEO needs directly.
So let’s dive in, and explore:
- first, how to generate Metadata
- second, creating the Robots and Sitemap content
- third, creating Structured Data
Let’s dive right in!
Metadata
NextJS generates metadata via additional functions within the page.tsx
file. The generateMetadata
function has the same calling signature as the component function. However, instead of returning JSX, the function returns an object with key-value pairs. And these pairs define the metadata to be injected into the document head.
So here, for example, we have the title and the description:
import { Metadata } from 'next';
/*....*/
export async function generateMetadata( {params: {lang}}: PageProps ):Promise<Metadata> {
return {
title: 'Kodaps Homepage',
description: 'Learn all about software engineering'
};
}
Robots and Sitemaps
Defining the Robots.txt file with NextJS
The Robots and SiteMap content is generated somewhat like a Page.
For the Robots content (which tells bots from the search engines where to look), you create a robots.ts
file at the root of your app
folder. This file exports a default function. The function returns an object that defines the content of the robots file:
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://kodaps.dev/sitemap.xml',
};
}
This results in the following robots file being generated :
User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://kodaps.dev/sitemap.xml
Creating Dynamic Sitemaps with NextJS
The sitemap.ts
file follows a similar principle of returning data that defines the sitemap content. Allow me to walk you through my code:
First I import the route metadata type, called MetadataRoute
, from next:
import { MetadataRoute } from 'next';
Then I define a default exported function, which returns a promise of Sitemap data :
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
Within this function, I define a constant called urlList
. This constant is array of strings. I initialise this array with known paths.
const urlList:string[] = ['/en', '/fr'];
Then I fetch all the dynamic content, I get the corresponding URLs, and I add them to the list of urls:
const _posts = await findAllPosts('blogpost');
for(const post of _posts) {
urlList.push(getPermalink(post.slug, 'post', post.lang));
}
Finally, I use the map
array function to transform my list of strings into a list of objects. These objects each have url
and lastModified
members. This list of objects is what the function returns.
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
/* create the URL list in a const called urlList */
//Format the list
return urlList.map((url) => ({
url: BASE_URL + url,
lastModified: new Date(),
}));
}
All this allows me to have a dynamically updated Sitemap.
Structured Data
Another important part of SEO is presenting data in a format engines can easily digest. The ideal way to do so is via “structured data”, a standardised format which tells Search Engine bots exactly what the page contains.
This helps search engines better understand and index your content. However, it can also enhance how your page is shown in search results. This improves the click-through rate and overall website visibility.
So, how can we add Structured Data to our website?
Well, let’s take the example of a BlogPost schema.
Using Schema-DTS
The fun part of using Structured Data is that the types are complex. Thankfully, a library called schema-dts
covers exactly this use case. This means we can use the provided TypeScript types to help us ensure we provide data with the right “shape”.
In our case, let’s import BlogPosting
and WithContext
from this library
import { BlogPosting, WithContext } from 'schema-dts';
Now let’s generate an object. I pass in the content data, which follows a Post type that I’ve defined for my data. And I use that post data to generate an object that follows the WithContext<BlogPosting>
schema :
export const generateContentStructuredData = (post:Post ) => {
const schema: WithContext<BlogPosting> = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.description,
author: [{
'@type': 'Person',
name: post.author || "David Hockley",
},],
image: post.image,
datePublished: post.date.toISOString(),
};
return schema;
}
I have a <StructredData>
component that takes the structured data object and renders it as a “LD+JSON” script, which is the format Google prefers.
const StructuredData:React.FC<DataProps> = ({ data }) => {
return (
<script
key="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
Then within the page, I simply add the component:
<StructuredData data={generateBlogPostStructuredData(post)} />
I’ve used the schema on my website for various other things like providing FAQs. You can find plenty of other types explained on the Schema.org website.
We help you better understand software development. Receive the latest blog posts, videos, insights and news from the frontlines of web development
Conclusion
As you can see, the app
router in NextJS makes these SEO features easy to implement.
If you want to keep exploring Next JS, you might want to understand how the app router does its thing, or how to translate a NextJS application that uses the app
router, this is for you.