DEV Community

Cover image for Mastering SEO for Next.js: A Practical Optimization Checklist
Chandu Bobbili
Chandu Bobbili

Posted on

Mastering SEO for Next.js: A Practical Optimization Checklist

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

I've been on a deep dive into Next.js SEO, and I'm excited to share what I've learned. This tutorial provides a clear, actionable checklist to help you optimize your blog and see real results in search engine rankings. Let's make your content shine!

1. Meta Tags

Ensure your website's SEO and social media presence are strong by implementing this meta tag checklist:

  • Standard Meta Tags : title, description, keywords, robots, viewport, charSet
  • Open Graph Meta Tags : og:site_name, og:local, 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 (Open Graph): article:published_time, article:modified_time, article:author
  • Twitter Meta Tags: twitter:card, twitter:site, twitter:creator, twitter:title, twitter:description, twitter:image

For optimal compatibility with social media platforms, utilize PNG or JPG image formats in the og:image and twitter:image meta tags, as WebP support is inconsistent.

Meta Tags for Next.js App Router

The App Router provides export const metadata for static metadata and generateMetadata for dynamic metadata generation, simplifying meta tag implementation in Next.js.

Want to know how? The metadata docs have all the info.

Static Metadata

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

import type { Viewport, Metadata } from "next";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  themeColor: "#ffffff"
};

export const metadata: Metadata = {
  title: "This is meta title",
  description: "This is meta description.",
  metadataBase: new URL("https://example.com"),
  keywords: [
    "example",
    "seo",
    "javascript",
    "nextjs",
  ],
  robots: {
    index: true,
    follow: true,
    "max-image-preview": "large",
    "max-snippet": -1,
    "max-video-preview": -1,
    googleBot: "index, follow"
  },
  alternates: {
    canonical: "https://example.com",
    types: {
      "application/rss+xml": "https://example.com/rss.xml"
    }
  },
  applicationName: "Example | Chandu Bobbili",
  appleWebApp: {
    title: "Example | Chandu Bobbili",
    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
    ]
  },
  openGraph: {
    url: "https://example.com",
    type: "website",
    title: "This is title",
    description: "This is the description",
    images: [
      {
        url: "https://example.com/images/home/thumbnail.png",
        width: 1200,
        height: 630,
        alt: "image"
      }
    ]
  },
  twitter: {
    card: "summary_large_image",
    title: "This is title",
    description: "This is the description",
    creator: "@ChanduBobbili",
    site: "@ChanduBobbili",
    images: [
      {
        url: "https://example.com/images/home/thumbnail.png",
        width: 1200,
        height: 630,
        alt: "dminhvu"
      }
    ]
  },
};
Enter fullscreen mode Exit fullscreen mode

Dynamic Metadata

Implement the generateMetadata function to define dynamic metadata for pages with dynamic route segments, such as [slug]/page.tsx or [id]/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} | Webpage`,
    authors: [
      {
        name: post.author || "Chandu"
      }
    ],
    description: post.description,
    keywords: post.keywords,
    openGraph: {
      title: `${post.title} | Webpage`,
      description: post.description,
      type: "article",
      url: `https://example.com/${post.slug}`,
      publishedTime: post.created_at,
      modifiedTime: post.modified_at,
      authors: ["https://example.com/about"],
      tags: post.categories,
      images: [
        {
          url: `https://example/assets/${post.slug}/thumbnail.png?tr=f-png`,
          width: 1024,
          height: 576,
          alt: post.title,
          type: "image/png"
        }
      ]
    },
    twitter: {
      card: "summary_large_image",
      site: "@ChanduBobbili",
      creator: "@ChanduBobbili",
      title: `${post.title} | webpage`,
      description: post.description,
      images: [
        {
          url: `https://example/assets/${post.slug}/thumbnail.png?tr=f-png`,
          width: 1024,
          height: 576,
          alt: post.title
        }
      ]
    },
    alternates: {
      canonical: `https://example.com/${post.slug}`
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Next.js App Router automatically includes charSet and viewport, so you don't need to define them manually.

Meta Tags for Next.js Pages Router

You'll find these meta tags present on this page when using the Next.js Pages Router:

import Head from "next/head";

export default function Page() {
  return (
    <Head>
      <title>Example | Webpage</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 | Webpage" />
      <meta property="og:locale" content="en_US" />
      <meta
        property="og:title"
        content="Enter your title here"
      />
      <meta
        property="og:description"
        content="Enter your description here"
      />
      <meta property="og:type" content="website" />
      <meta property="og:url" content="https://example.com/nextjs-seo" />
      <meta
        property="og:image"
        content="https://example/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/chandu-bobbili-15863319b"
      />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:site" content="@ChanduBobbili" />
      <meta name="twitter:creator" content="@ChanduBobbili" />
      <meta
        name="twitter:title"
        content="Enter your title here"
      />
      <meta
        name="twitter:description"
        content="Enter your description here"
      />
      <meta
        name="twitter:image"
        content="https://example/assets/nextjs-seo/thumbnail.png?tr=f-png"
      />
    </Head>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Sitemap

A sitemap helps search engines find and index your website's content, which is crucial for SEO.

Sitemap for Next.js App Router

For Next.js App Router, you can define the sitemap.ts file at 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://example.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1
    },
    {
      url: "https://example.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.9
    },
    {
      url: "https://example.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://example.com/${e.slug}`,
      lastModified: e.modified_at,
      changeFrequency: "daily",
      priority: 0.8
    })),
    ...categorySlugs.map((e: any) => ({
      url: `https://example.com/category/${e}`,
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 0.7
    }))
  ];

  return sitemap;
}
Enter fullscreen mode Exit fullscreen mode

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

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

Visit Next.js App Router Sitemap to learn more.

Sitemap for Next.js Page Router

If you're using the Pages Router in Next.js, next-sitemap is a convenient tool for sitemap creation following your build.

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

npm install next-sitemap
npx next-sitemap
Enter fullscreen mode Exit fullscreen mode

next-sitemap requires a basic config file (next-sitemap.config.js) under your project root, checkout

A sitemap will be generated at 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://example.com</loc>
    <lastmod>2024-01-11T02:03:09.613Z</lastmod>
    <changefreq>daily</changefreq>
  <priority>0.7</priority>
</url>
<!-- other pages -->
</urlset>
Enter fullscreen mode Exit fullscreen mode

3. robots.txt

A robots.txt file guides search engine crawlers by defining which pages they are allowed or disallowed to access.

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:

import { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: ["/"],
      disallow: ["/search?q=", "/admin/"]
    },
    sitemap: ["https://example.com/sitemap.xml"]
  };
}
Enter fullscreen mode Exit fullscreen mode

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

robots.txt for Next.js Pages Router

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

User-agent: *
Disallow:
Sitemap: https://example.com/sitemap.xml
Enter fullscreen mode Exit fullscreen mode

For pages you don't want indexed, such as search results or those marked 'noindex', this line will block search engine crawlers:

User-agent: *
Disallow: /search?q=
Disallow: /admin
Enter fullscreen mode Exit fullscreen mode

4. Link Tags

Beyond meta tags, your website requires the inclusion of several key link tags:

  • canonical
  • alternative
  • icon
  • apple-touch-icon
  • manifest

Link Tags for Next.js App Router

Link tags in Next.js App Router are handled in the same way as meta tags, via export const metadata or generateMetadata.

import type { Viewport, Metadata } from "next";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  themeColor: "#ffffff"
};

export const metadata: Metadata = {
  // other parts
  alternates: {
    canonical: "https://example.com",
    types: {
      "application/rss+xml": "https://example.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
    ]
  },
};
Enter fullscreen mode Exit fullscreen mode

Link Tags for Next.js Pages Router

To demonstrate the link tags, this page would contain the following when using the Pages Router:

import Head from "next/head";

export default function Page() {
  return (
    <Head>
      {/* other parts */}
      <link
        rel="alternate"
        type="application/rss+xml"
        href="https://example.com/rss.xml"
      />
      <link rel="canonical" href="https://example.com/nextjs-seo" />
      <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>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. JSON-LD Schema

JSON-LD, a lightweight Linked Data format, facilitates machine parsing and generation, contributing to its widespread adoption.

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

You can easily generate JSON-LD Schema for your website by using the Schema Markup Generator Tool.
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

6. 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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Script Optimization for Common Third-Party Integrations

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

  • Google Tag Manager
  • Google Analytics
  • Google Maps Embed
  • Youtube Embed To use the @next/third-parties library, you need to install it:
npm install @next/third-parties
Enter fullscreen mode Exit fullscreen mode
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

7. 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 directly influences your Google PageSpeed score, which in turn benefits user experience and SEO.

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

import Image from "next/image";

export default function Page() {
  return (
    <Image
      src="https://example/assets/nextjs-seo/thumbnail.png?tr=f-webp"
      alt="Next.js SEO"
      width={1200}
      height={630}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Conclusion

Here's a conclusion summarizing the key areas for website performance and SEO optimization in Next.js:

  • Meta Tags: Implement comprehensive meta tags for improved search engine understanding and display.
  • JSON-LD Schema: Utilize JSON-LD to provide structured data, enhancing search engine comprehension and rich results.
  • Sitemap: Generate and submit a sitemap to ensure search engines can effectively crawl and index your website.
  • robots.txt: Use robots.txt to control search engine crawler access, specifying which pages to crawl or ignore.
  • Link Tags: Implement <link> tags for canonical URLs, icons, and other essential link-related information.
  • Script Optimization:
    • Leverage Next.js's <Script> component for efficient script loading.
    • Utilize @next/third-parties for optimized handling of common third-party scripts.
  • Image Optimization:
    • Use Next.js's <Image> component for optimized image loading and performance.
    • Implement a CDN to cache images, improving loading speeds.
    • Adopt modern image formats like WebP for reduced file sizes and faster delivery.

Top comments (0)

๐Ÿ‘‹ Kindness is contagious

Please leave a โค๏ธ or a friendly comment on this post if you found it helpful!

Okay