Next.js SEO: The Complete Checklist to Boost Your Site Ranking

Minh Vu

By Minh Vu

Updated Mar 01, 2024

5.7K reads

Figure: Next.js SEO: The Complete Checklist to Boost Your Site Ranking

Disclaimer: All content on this website is derived directly from my own expertise and experiences. No AI-generated text or automated content creation tools are used.

SEO (Search Engine Optimization) is one of the key factors to rank your website higher on Google and receive more traffic.

In this tutorial, I will show you what I have done to optimize this blog for SEO by providing a checklist that you can follow easily.

I have spent a lot of time researching and learning about SEO, especially for Next.js, and found some optimal ways to do it. So I hope this tutorial will help you save time and effort.

I will put the items covered in this tutorial inside the Table of Contents below. You can click on any item to jump to the section.

Please note that some parts are different for Next.js Pages Router and Next.js App Router, I will mention it in the tutorial. Otherwise, it can be applied to both.

Contents

Meta Tags

Meta tags provide information about your website to search engines and social media platforms.

Your website should include the following standard meta tags:

  • title
  • description
  • keywords
  • robots
  • viewport
  • charSet

Open Graph meta tags:

  • og:site_name
  • og:locale
  • og:title
  • og:description
  • og:type
  • og:url
  • og:image
  • og:image:alt
  • og:image:type
  • og:image:width
  • og:image:height

Article meta tags (actually it's also OpenGraph):

  • article:published_time
  • article:modified_time
  • article:author

Twitter meta tags:

  • twitter:card
  • twitter:site
  • twitter:creator
  • twitter:title
  • twitter:description
  • twitter:image

Please note that for the og:image and twitter:image tags, you should use the PNG or JPG format as some social media platforms will not read the WebP format.

Meta Tags for Next.js Pages Router

For example, the current page has the following meta tags when I use Next.js Pages Router:

pages/[slug].tsx
import Head from "next/head";
 
export default function Page() {
  return (
    <Head>
      <title>
        Next.js SEO: The Complete Checklist to Boost Your Site Ranking
      </title>
      <meta
        name="description"
        content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
      />
      <meta
        name="keywords"
        content="nextjs seo complete checklist, nextjs seo tutorial"
      />
      <meta name="robots" content="index, follow" />
      <meta name="googlebot" content="index, follow" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta charSet="utf-8" />
      <meta property="og:site_name" content="Blog | Minh Vu" />
      <meta property="og:locale" content="en_US" />
      <meta
        property="og:title"
        content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
      />
      <meta
        property="og:description"
        content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
      />
      <meta property="og:type" content="website" />
      <meta property="og:url" content="https://dminhvu.com/nextjs-seo" />
      <meta
        property="og:image"
        content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
      />
      <meta property="og:image:alt" content="Next.js SEO" />
      <meta property="og:image:type" content="image/png" />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="630" />
      <meta
        property="article:published_time"
        content="2024-01-11T11:35:00+07:00"
      />
      <meta
        property="article:modified_time"
        content="2024-01-11T11:35:00+07:00"
      />
      <meta
        property="article:author"
        content="https://www.linkedin.com/in/dminhvu02"
      />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:site" content="@dminhvu02" />
      <meta name="twitter:creator" content="@dminhvu02" />
      <meta
        name="twitter:title"
        content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
      />
      <meta
        name="twitter:description"
        content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
      />
      <meta
        name="twitter:image"
        content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
      />
    </Head>
  );
}

Meta Tags for Next.js App Router

For Next.js App Router, it's even easier to define those tags by using the built-in export const metadata argument for static metadata, and the generateMetadata function for pages that have dynamic data (blog posts, products, etc.).

Let's find out how to do it. You can visit the metadata docs to learn more.

Static Metadata

You can define static metadata by adding the export const metadata argument inside page.tsx or layout.tsx:

app/layout.tsx
import type { Viewport, Metadata } from "next";
 
export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  themeColor: "#ffffff"
};
 
export const metadata: Metadata = {
  metadataBase: new URL("https://dminhvu.com"),
  openGraph: {
    siteName: "Blog | Minh Vu",
    type: "website",
    locale: "en_US"
  },
  robots: {
    index: true,
    follow: true,
    "max-image-preview": "large",
    "max-snippet": -1,
    "max-video-preview": -1,
    googleBot: "index, follow"
  },
  alternates: {
    types: {
      "application/rss+xml": "https://dminhvu.com/rss.xml"
    }
  },
  applicationName: "Blog | Minh Vu",
  appleWebApp: {
    title: "Blog | Minh Vu",
    statusBarStyle: "default",
    capable: true
  },
  verification: {
    google: "YOUR_DATA",
    yandex: ["YOUR_DATA"],
    other: {
      "msvalidate.01": ["YOUR_DATA"],
      "facebook-domain-verification": ["YOUR_DATA"]
    }
  },
  icons: {
    icon: [
      {
        url: "/favicon.ico",
        type: "image/x-icon"
      },
      {
        url: "/favicon-16x16.png",
        sizes: "16x16",
        type: "image/png"
      }
      // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
    ],
    shortcut: [
      {
        url: "/favicon.ico",
        type: "image/x-icon"
      }
    ],
    apple: [
      {
        url: "/apple-icon-57x57.png",
        sizes: "57x57",
        type: "image/png"
      },
      {
        url: "/apple-icon-60x60.png",
        sizes: "60x60",
        type: "image/png"
      }
      // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
    ]
  }
};
app/page.tsx
import { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
  description:
    "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
  keywords: [
    "elastic",
    "python",
    "javascript",
    "react",
    "machine learning",
    "data science"
  ],
  openGraph: {
    url: "https://dminhvu.com",
    type: "website",
    title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
    description:
      "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
    images: [
      {
        url: "https://dminhvu.com/images/home/thumbnail.png",
        width: 1200,
        height: 630,
        alt: "dminhvu"
      }
    ]
  },
  twitter: {
    card: "summary_large_image",
    title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
    description:
      "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
    creator: "@dminhvu02",
    site: "@dminhvu02",
    images: [
      {
        url: "https://dminhvu.com/images/home/thumbnail.png",
        width: 1200,
        height: 630,
        alt: "dminhvu"
      }
    ]
  },
  alternates: {
    canonical: "https://dminhvu.com"
  }
};

Dynamic Metadata

Dynamic metadata can be defined by using the generateMetadata function, this is useful when you have dynamic pages like [slug]/page.tsx, or [id]/page.tsx:

app/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
 
type Params = {
  slug: string;
};
 
type Props = {
  params: Params;
  searchParams: { [key: string]: string | string[] | undefined };
};
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const { slug } = params;
 
  const post: Post = await fetch(`YOUR_ENDPOINT`, {
    method: "GET",
    next: {
      revalidate: 60 * 60 * 24
    }
  }).then((res) => res.json());
 
  return {
    title: `${post.title} | dminhvu`,
    authors: [
      {
        name: post.author || "Minh Vu"
      }
    ],
    description: post.description,
    keywords: post.keywords,
    openGraph: {
      title: `${post.title} | dminhvu`,
      description: post.description,
      type: "article",
      url: `https://dminhvu.com/${post.slug}`,
      publishedTime: post.created_at,
      modifiedTime: post.modified_at,
      authors: ["https://dminhvu.com/about"],
      tags: post.categories,
      images: [
        {
          url: `https://ik.imagekit.io/dminhvu/assets/${post.slug}/thumbnail.png?tr=f-png`,
          width: 1024,
          height: 576,
          alt: post.title,
          type: "image/png"
        }
      ]
    },
    twitter: {
      card: "summary_large_image",
      site: "@dminhvu02",
      creator: "@dminhvu02",
      title: `${post.title} | dminhvu`,
      description: post.description,
      images: [
        {
          url: `https://ik.imagekit.io/dminhvu/assets/${post.slug}/thumbnail.png?tr=f-png`,
          width: 1024,
          height: 576,
          alt: post.title
        }
      ]
    },
    alternates: {
      canonical: `https://dminhvu.com/${post.slug}`
    }
  };
}

Please note that the charSet and viewport are automatically added by Next.js App Router, so you don't need to define them.

JSON-LD Schema

This part can be applied to both Pages Router and App Router.

I have written an in-depth post about How to Add JSON-LD Schema to Your Next.js Website that you can read to dive deeper into this topic.

JSON-LD is a lightweight Linked Data format. It is easy for machines to parse and generate. It is currently one of the most widely used formats for Linked Data.

You can easily generate JSON-LD Schema for your website by using the Schema Markup Generator Tool.

For example, the current page has the following JSON-LD Schema:

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  mainEntityOfPage: {
    "@type": "WebPage",
    "@id": "https://dminhvu.com/nextjs-seo"
  },
  headline: "Next.js SEO: The Complete Checklist to Boost Your Site Ranking",
  description:
    "Learn how to optimize your Next.js website for SEO by following this complete checklist.",
  image:
    "https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png",
  dateCreated: "2024-01-11T11:35:00+07:00",
  datePublished: "2024-01-11T11:35:00+07:00",
  dateModified: "2024-01-11T11:35:00+07:00",
  author: {
    "@type": "Person",
    name: "Minh Vu",
    url: "https://www.linkedin.com/in/dminhvu02"
  },
  publisher: {
    "@type": "Person",
    name: "Minh Vu",
    logo: {
      "@type": "ImageObject",
      url: "https://dminhvu.com/avatar_zoom.jpg"
    }
  },
  inLanguage: "en-US",
  isFamilyFriendly: "true"
};

And I can put it anywhere, whether inside the head tag or the body tag is fine:

import Head from "next/head";
 
export default function Page() {
  return (
    <div>
      {/* other parts */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
    </div>
  );
}

Sitemap

Your website should provide a sitemap so that search engines can easily crawl and index your pages.

Generate Sitemap for Next.js Pages Router

For Next.js Pages Router, you can use next-sitemap to generate a sitemap for your Next.js website after building.

For example, running the following command will install next-sitemap and generate a sitemap for this blog:

npm install next-sitemap
npx next-sitemap

A sitemap will be generated at public/sitemap.xml:

public/sitemap.xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
  <loc>https://dminhvu.com</loc>
    <lastmod>2024-01-11T02:03:09.613Z</lastmod>
    <changefreq>daily</changefreq>
  <priority>0.7</priority>
</url>
<!-- other pages -->
</urlset>

Please visit the next-sitemap page for more information.

Generate Sitemap for Next.js App Router

For Next.js App Router, you can define the sitemap.ts file at app/sitemap.ts:

app/sitemap.ts
import {
  getAllCategories,
  getAllPostSlugsWithModifyTime
} from "@/utils/getData";
import { MetadataRoute } from "next";
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const defaultPages = [
    {
      url: "https://dminhvu.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1
    },
    {
      url: "https://dminhvu.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.9
    },
    {
      url: "https://dminhvu.com/contact",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.9
    }
    // other pages
  ];
 
  const postSlugs = await getAllPostSlugsWithModifyTime();
  const categorySlugs = await getAllCategories();
 
  const sitemap = [
    ...defaultPages,
    ...postSlugs.map((e: any) => ({
      url: `https://dminhvu.com/${e.slug}`,
      lastModified: e.modified_at,
      changeFrequency: "daily",
      priority: 0.8
    })),
    ...categorySlugs.map((e: any) => ({
      url: `https://dminhvu.com/category/${e}`,
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 0.7
    }))
  ];
 
  return sitemap;
}

With this sitemap.ts file created, you can access the sitemap at https://dminhvu.com/sitemap.xml.

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://dminhvu.com</loc>
    <lastmod>2024-01-11T02:03:09.613Z</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  <!-- other pages -->
</urlset>

Visit Next.js App Router Sitemap to learn more.

robots.txt

A robots.txt file should be added to tell search engines which pages to crawl and which pages to ignore.

robots.txt for Next.js Pages Router

For Next.js Pages Router, you can create a robots.txt file at public/robots.txt:

public/robots.txt
User-agent: *
Disallow:
Sitemap: https://dminhvu.com/sitemap.xml

You can prevent the search engine from crawling a page (usually search result pages, noindex pages, etc.) by adding the following line:

public/robots.txt
User-agent: *
Disallow: /search?q=
Disallow: /admin

robots.txt for Next.js App Router

For Next.js App Router, you don't need to manually define a robots.txt file. Instead, you can define the robots.ts file at app/robots.ts:

app/robots.ts
import { MetadataRoute } from "next";
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: ["/"],
      disallow: ["/search?q=", "/admin/"]
    },
    sitemap: ["https://dminhvu.com/sitemap.xml"]
  };
}

With this robots.ts file created, you can access the robots.txt file at https://dminhvu.com/robots.txt.

User-agent: *
Allow: /
Disallow: /search?q=
Disallow: /admin
 
Sitemap: https://dminhvu.com/sitemap.xml

There are some important link tags like the meta tags that you should include on your website:

  • canonical
  • alternate
  • icon
  • apple-touch-icon
  • manifest

For example, the current page has the following link tags if I use the Pages Router:

pages/_app.tsx
import Head from "next/head";
 
export default function Page() {
  return (
    <Head>
      {/* other parts */}
      <link
        rel="alternate"
        type="application/rss+xml"
        href="https://dminhvu.com/rss.xml"
      />
      <link rel="icon" href="/favicon.ico" type="image/x-icon" />
      <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
      <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
      {/* add apple-touch-icon-72x72.png, apple-touch-icon-76x76.png, apple-touch-icon-114x114.png, apple-touch-icon-120x120.png, apple-touch-icon-144x144.png, apple-touch-icon-152x152.png, apple-touch-icon-180x180.png */}
      <link
        rel="icon"
        type="image/png"
        href="/favicon-16x16.png"
        sizes="16x16"
      />
      {/* add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png */}
    </Head>
  );
}
pages/[slug].tsx
import Head from "next/head";
 
export default function Page() {
  return (
    <Head>
      {/* other parts */}
      <link rel="canonical" href="https://dminhvu.com/nextjs-seo" />
    </Head>
  );
}

For Next.js App Router, the link tags can be defined using the export const metadata or generateMetadata similar to the meta tags section.

The code below is exactly the same as the meta tags for Next.js App Router section above.

app/layout.tsx
export const metadata: Metadata = {
  // other parts
  alternates: {
    types: {
      "application/rss+xml": "https://dminhvu.com/rss.xml"
    }
  },
  icons: {
    icon: [
      {
        url: "/favicon.ico",
        type: "image/x-icon"
      },
      {
        url: "/favicon-16x16.png",
        sizes: "16x16",
        type: "image/png"
      }
      // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
    ],
    shortcut: [
      {
        url: "/favicon.ico",
        type: "image/x-icon"
      }
    ],
    apple: [
      {
        url: "/apple-icon-57x57.png",
        sizes: "57x57",
        type: "image/png"
      },
      {
        url: "/apple-icon-60x60.png",
        sizes: "60x60",
        type: "image/png"
      }
      // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
    ]
  }
};
app/page.tsx
export const metadata: Metadata = {
  // other parts
  alternates: {
    canonical: "https://dminhvu.com"
  }
};

Script Optimization

Script Optimization for General Scripts

Next.js provides a built-in component called <Script> to add external scripts to your website.

For example, you can add Google Analytics to your website by adding the following script tag:

pages/_app.tsx
import Head from "next/head";
import Script from "next/script";
 
export default function Page() {
  return (
    <Head>
      {/* other parts */}
      {process.env.NODE_ENV === "production" && (
        <>
          <Script async strategy="afterInteractive" id="analytics">
            {`
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
              gtag('config', 'G-XXXXXXXXXX');
            `}
          </Script>
        </>
      )}
    </Head>
  );
}

Script Optimization for Common Third-Party Integrations

Next.js App Router introduces a new library called @next/third-parties for:

To use the @next/third-parties library, you need to install it:

npm install @next/third-parties

Then, you can add the following code to your app/layout.tsx:

app/layout.tsx
import { GoogleTagManager } from "@next/third-parties/google";
import { GoogleAnalytics } from "@next/third-parties/google";
import Head from "next/head";
 
export default function Page() {
  return (
    <html lang="en" className="scroll-smooth" suppressHydrationWarning>
      {process.env.NODE_ENV === "production" && (
        <>
          <GoogleAnalytics gaId="G-XXXXXXXXXX" />
          {/* other scripts */}
        </>
      )}
      {/* other parts */}
    </html>
  );
}

Please note that you don't need to include both GoogleTagManager and GoogleAnalytics if you only use one of them.

Image Optimization

This part can be applied to both Pages Router and App Router.

Image optimization is also an important part of SEO as it helps your website load faster.

Faster image rendering speed will contribute to the Google PageSpeed score, which can improve user experience and SEO.

You can use next/image to optimize images in your Next.js website.

For example, the following code will optimize this post thumbnail:

import Image from "next/image";
 
export default function Page() {
  return (
    <Image
      src="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-webp"
      alt="Next.js SEO"
      width={1200}
      height={630}
    />
  );
}

Remember to use a CDN to serve your media (images, videos, etc.) to improve the loading speed. Here I used ImageKit to serve my images. Some people prefer Cloudinary or Vercel built-in image optimization.

You can also use DigitalOcean Spaces to store your media files. Use this link to get $200 free credit for 60 days to try it out.

For the image format, I prefer to use WebP because it has a smaller size than PNG and JPEG.

Conclusion

I hope this tutorial will help you optimize your Next.js website for SEO.

In general, there are several important points that you should pay attention to:

  • Meta tags
  • JSON-LD Schema
  • Sitemap
  • robots.txt
  • Link tags
  • Script optimization
  • Image optimization

If you have any questions, please leave a comment below.

Minh Vu

Minh Vu

Software Engineer

Hi guys 👋, I'm a developer specializing in Elastic Stack and Next.js. My blog shares practical tutorials and insights based on 3+ years of hands-on experience. Open to freelance opportunities — let's get in touch!

Comments

Minh Vu

Feb 22, 2024

I just added the robots.txt section for Next.js App Router. Cheers!

kiwi

Feb 25, 2024

Thanks for the in-depth tutorial, can you elaborate more about JSON LD?

jayden

Feb 28, 2024

thorough and detailed explanation

Rowland Ricketts

Mar 05, 2024

Thanks for sharing this,

Leave a Comment

Receive Latest Updates 📬

Get every new post, special offers, and more via email. No fee required.