Skip to main content

Next.js app router SEO overview

Hey everyone! It seems that App router is the new standard for Next.js applications. And while everyone is getting familiar with its technical components, I want to talk about the SEO capabilities it provides. Because no matter how good our website is technically, without proper SEO configuration, there is a high risk that its performance will go unnoticed.

Next.js app router SEO overview

Steps

  • Common SEO facts in app router.
  • Meta tags
  • Sitemap & Robots.txt
  • Canonical & localization tags
  • Images, fonts and script optimization
  • Core Web Vitals
  • Sctuctured data
  • Redirects
  • Conclusion

Common SEO facts in app router

Next.js has always been good for SEO because of its integration with server-side rendering (SSR) from the beginning, which was its main selling point. But now, with the new app router, it has become even better. Previously, we had to choose between SSR and incremental static regeneration (ISR), which had certain limitations. However, now we can use them together, allowing us to achieve better results. The ability to work with live data while caching individual content pieces is fantastic. As a result, you can be confident that you are using the best SEO solution when working with the app router.

Meta tags

Okay, let's start with something simple - meta tags. There's actually a lot of new things here! Now there's a clear and, most importantly, standardized way to work with them. In fact, all the meta information can be easily described inside a special object, which can be placed both in the layout and inside pages. Metadata can be inherited, which is definitely convenient for different patterns. And it can also be flexibly configured and overridden.

Here if an example:

import type { Metadata } from "next";

export const metadata: Metadata = {
title: "My page title",
description: "My page description",
...
};

export default function Page() {}

In case you need more dynamic version, here is it:

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// read route params
const id = params.id

// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json())

// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || []

return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}

export default function Page({ params, searchParams }: Props) {}

We are not going to cover all the possibilities, but we have to note that, in addition to these examples, metadata can also be configured based of files. Additionally, Next.js now has built-in dynamic image generation for OG. In general, this is definitely worth a deep dive. You can find all the information here: Next.js Metadata Documentation

Sitemap & Robots.txt

Same as with the pages router, you can create a static or dynamic sitemap \ robots.txt. Both of them is about creation a file in the root of your project:

  • static (sitemap.xml || robots.txt)
  • dynamic (sitemap.ts || robots.ts)

Static

Create a file sitemap.xml in the root of your project and fill it with the content.

  • simemap
<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>
  • robots.txt
User-Agent: *
Allow: /
Disallow: /private/

Sitemap: https://acme.com/sitemap.xml

Dynamic

This way is more flexible and allows you to generate a sitemap based on your content. Simply create a file sitemap.ts \ robots.ts in the root of your project and write a function that will generate a sitemap.

  • sitemap
import { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://acme.com",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
{
url: "https://acme.com/about",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: "https://acme.com/blog",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.5,
},
];
}
  • robots.txt
import { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: "/private/",
},
sitemap: "https://acme.com/sitemap.xml",
};
}

Canonical & localization tags

The App router introduces standardization, including for these aspects. All this information can be described using the same metadata object. Therefore, all we have to do is add the necessary fields to it.

export const metadata = {
alternates: {
canonical: "https://nextjs.org",
languages: {
"en-US": "https://nextjs.org/en-US",
"de-DE": "https://nextjs.org/de-DE",
},
media: {
"only screen and (max-width: 600px)": "https://nextjs.org/mobile",
},
types: {
"application/rss+xml": "https://nextjs.org/rss",
},
},
};

result:

<link rel="canonical" href="https://nextjs.org" />
<link rel="alternate" hreflang="en-US" href="https://nextjs.org/en-US" />
<link rel="alternate" hreflang="de-DE" href="https://nextjs.org/de-DE" />
<link
rel="alternate"
media="only screen and (max-width: 600px)"
href="https://nextjs.org/mobile"
/>
<link
rel="alternate"
type="application/rss+xml"
href="https://nextjs.org/rss"
/>

This method covers all necessary cases, but for more detailed information, please refer to the section above about meta tags.

Images, fonts and script optimization

In this section, there will be no differences from the usual classics.

All we need to do is to make sure we use components that Next.js provides us. BTW, those components were developed by the Next.js team in collaboration with Google, so these elements aware of the best practices.

Images

By using Next.js image component we are able to cover several optimizations:

  • faster page loads (LCP)
  • size optimization (LCP)
  • avoid layout shift (CLS)
  • asset flexibility (LCP)

Here is an example of how to use it:

import Image from "next/image";

export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500}
height={500}
/>
);
}

Fonts

next/font provides us with a component that allows us to load fonts in a way that is optimized for performance. It's exceptionally useful for loading fonts from Google Fonts, but it can also be used to load fonts from other sources. It has built-in automatic self-hoisting, so CLS is not a problem with that solution.

Google fonts
import { Roboto } from "next/font/google";

const roboto = Roboto({
weight: "400",
subsets: ["latin"],
display: "swap",
});

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={roboto.className}>
<body>{children}</body>
</html>
);
}
Local fonts
import localFont from "next/font/local";

// Font files can be colocated inside of `app`
const myFont = localFont({
src: "./my-font.woff2",
display: "swap",
});

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
);
}

Scripts

Same sort of optimizatoions is also avaible for scripts. The most important part of this component is strategy that can be a gamechanger for your app.

Example:

import Script from "next/script";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<section>{children}</section>
<Script src="https://example.com/script.js" strategy="afterInteractive" />
</>
);
}

Core Web Vitals

Not so long ago, Google announced that they will be using Core Web Vitals as a ranking factor for websites. This means that if your website is not optimized, it will be ranked lower than websites that others.

It became a hot topic in the SEO community and nowadays everyone is talking about it.

We are going to cover the most important metrics and ways to optimize them. But please remember that Google is constantly changing and updating their algorithms, so make sure you keep an eye on the latest updates.

First Contentful Paint (FCP)

First Contentful Paint marks the time at which the first text or image is painted.

Practical advices to improve FCP:

  • Use a CDN to deliver assets close to your users
  • Reduce server response times
  • Elemiate render-blocking resources
Larget Contentful Paint (LCP)

Largest Contentful Paint marks the time at which the largest text or image is painted.

Practical advices to improve LCP:

  • Images and fonts! Check out our previoes topic about images optimization here
  • Make sure your CSS is not blocking the rendering of your page
Total Blocking Time (TBT)

Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.

Practical advices to improve LCP:

  • Scripts optimization. Check out our previoes topic about scripts optimization here
  • Codesplitting (App router do it best here)
  • remove unused code and trird-party libraries
Cumulative Layout Shift (CLS)

Cumulative Layout Shift measures the movement of visible elements within the viewport.

Practical advices to improve CLS:

  • Images and videos should have width and height attributes
  • Make sure your fonts are optimizied and, if possible, loaded before rendering the page

Sctuctured data

Structured data is crucial for enabling search engines to understand your web pages more effectively. Over time, various vocabularies have been constantly changing, but currently, the most actual one is schema.org.

We would like to say that the App router brings a ready-made solution, but that's not the case. We will have to follow the same path, namely, to add structured data to a script tag that will be inserted into the page.

Below we present an official example from the Next.js documentation. This is an example of what a product page might look like.

export default async function Page({ params }) {
const product = await getProduct(params.id);

const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
image: product.image,
description: product.description,
};

return (
<section>
{/* Add JSON-LD to your page */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* ... */}
</section>
);
}

What can really make life easier is a type library, recommended for everyone. LINK. It's much easier to create these objects with its help.

import { Product, WithContext } from "schema-dts";

const jsonLd: WithContext<Product> = {
"@context": "https://schema.org",
"@type": "Product",
name: "Next.js Sticker",
image: "https://nextjs.org/imgs/sticker.png",
description: "Dynamic at the speed of static.",
};

Redirects

In Next.js, there are many ways to implement redirects. We will list them, but we won't go into details, as it would take a lot of time. However, we will discuss where and which method can be applicable.

Next.js execution order

Take a look at the list below, it shows the order in which code is executed during a page request. Based on this, you can decide which redirect method suits you best, as the speed of its execution will obviously vary.

  1. headers from next.config.js
  2. redirects from next.config.js
  3. Middleware (rewrites, redirects, etc.)
  4. beforeFiles (rewrites) from next.config.js
  5. Filesystem routes (public/, _next/static/, pages/, app/, etc.)
  6. afterFiles (rewrites) from next.config.js
  7. Dynamic Routes (/blog/[slug])
  8. fallback (rewrites) from next.config.js
Next.js config

You can manage redirects at the project configuration level. This method is the fastest because it's executed before Next.js starts processing requests. It's excellent for static redirects that don't require dynamic management.

module.exports = {
async redirects() {
return [
{
source: "/old-blog/:slug",
destination: "/news/:slug", // Matched parameters can be used in the destination
permanent: true,
},
];
},
};
redirect function

In the new version of our router, we also have the capability to perform redirects in a unique way. Moreover, it looks very simple and straightforward.

The redirect function allows you to redirect the user to another URL. redirect can be used in Server Components, Client Components, Route Handlers, and Server Actions.

redirect(path, type);

YES! As simple as that.

Conclusion

Hey, we hope you enjoyed this article! It contains all the basic information about SEO for Next.js app router. Check out our blog for more insights on Next.js and the modern web.

WRITTEN BY

Alex Hramovich

Alex Hramovich

TechLead at FocusReactive