David Hockley

The NextJS app router SEO features: Metadata, sitemaps, robots.txt, structured data…

SEO improvements were one of Next JS’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!


NextJS generates metadata via additional functions within the page.tsx file. The generateMetadatafunction 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 lastModifiedmembers. 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 BlogPostingand WithContextfrom 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 (
        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.


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.

Made by kodaps · All rights reserved.
© 2023