Next.js SEO Complete Guide
DS
Darshan Suthar

Next.js SEO Complete Guide

1. Global SEO Setup (Root Layout)

Start by defining global metadata in:

app/layout.js

export const metadata = {
  metadataBase: new URL("https://yourdomain.com"),
  title: {
    default: "Your Brand",
    template: "%s | Your Brand",
  },
  description: "Your brand default description.",
  openGraph: {
    siteName: "Your Brand",
    type: "website",
    locale: "en_US",
  },
  twitter: {
    card: "summary_large_image",
  },
  robots: {
    index: true,
    follow: true,
  },
};

Now let’s clearly explain what each part does.

metadataBase

metadataBase: new URL("https://yourdomain.com"),
  • Defines your main domain.

  • Used to generate absolute URLs for:

    • Canonical links

    • Open Graph URLs

    • Social previews

Without this, relative URLs may not resolve correctly.

title.default

default: "Your Project Name"
  • Used when a page does not define its own title.

  • Acts as a fallback title.

title.template

template: "%s | Your Project Name"
  • Automatically formats page titles.

  • %s is replaced by the page title.

Example:
If a page sets:

title: "About"

The final title becomes:

About | Your Project Name

This keeps branding consistent across the site.

description

description: "Your brand default description."
  • Default meta description.

  • Used when a page does not define its own description.

  • Appears in Google search results.

Every important page should override this.

openGraph

openGraph: {
  siteName: "Your Brand",
  type: "website",
  locale: "en_US",
}

Controls how your website appears when shared on:

  • Facebook

  • LinkedIn

  • WhatsApp

  • Slack

Fields:

  • siteName → Displayed site name in previews

  • type → Usually "website" or "article"

  • locale → Language version of your site

Individual pages can override this.

twitter

twitter: {
  card: "summary_large_image",
}

Controls Twitter/X preview style.

summary_large_image means:

  • Big preview image

  • Better engagement

Other options:

  • summary

  • app

  • player

robots

robots: {
  index: true,
  follow: true,
}

Tells search engines:

  • index: true → This page can appear in search results.

  • follow: true → Follow links on this page.

If you set:

index: false

Google will not index the page.

Used for:

  • Admin pages

  • Private dashboards

  • Internal tools


2. Static Page SEO (About, Contact, Landing Pages)

Start by defining global metadata in:

app/about/page.jsx

export const metadata = {
  title: "About Us",
  description: "Learn more about our company and mission.",
  alternates: {
    canonical: "/about",
  },
};

export default function AboutPage() {
  return <div>About Page</div>;
}

Use this for:

  • About

  • Contact

  • Privacy policy

  • Static landing pages

No need for generateMetadata() here.


3. Dynamic Page SEO (Blog / Product Pages)

Dynamic routes require dynamic metadata.

Example:

app/blog/[slug]/page.jsx

Fetch Data

async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 60 },
  });

  if (!res.ok) return null;

  return res.json();
}

Add generateMetadata()

export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  if (!post) {
    return {
      title: "Post Not Found",
    };
  }

  return {
    title: post.seoTitle || post.title,
    description: post.seoDescription || post.excerpt,
    alternates: {
      canonical: `/blog/${slug}`,
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [
        {
          url: post.featuredImage,
          width: 1200,
          height: 630,
        },
      ],
      type: "article",
    },
  };
}

Important:

  • Always await params

  • Always set canonical

  • Always define an Open Graph image

  • Use revalidate for SEO-friendly caching


4. Open Graph Setup (Social Sharing)

Open Graph controls previews on:

  • Facebook

  • LinkedIn

  • WhatsApp

  • Slack

Minimum recommended setup:

openGraph: {
  title: "Page Title",
  description: "Page description",
  url: "https://yourdomain.com/page",
  images: [
    {
      url: "https://yourdomain.com/og-image.jpg",
      width: 1200,
      height: 630,
    },
  ],
  type: "website",
}

Image size recommendation:
1200 × 630 px


5. Dynamic Open Graph Image (Advanced but Powerful)

Instead of uploading images manually for each blog post, generate them dynamically.

Create:

app/blog/[slug]/opengraph-image.jsx

import { ImageResponse } from "next/og";

export const size = {
  width: 1200,
  height: 630,
};

export default async function Image({ params }) {
  const { slug } = await params;

  return new ImageResponse(
    (
      <div
        style={{
          background: "#111",
          color: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          fontSize: 60,
        }}
      >
        {slug}
      </div>
    ),
    size
  );
}

Now every blog page automatically gets a share image.


6. Add Structured Data (JSON-LD)

Add this inside your dynamic page component:

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      "@context": "https://schema.org",
      "@type": "Article",
      headline: post.title,
      description: post.excerpt,
      image: post.featuredImage,
      author: {
        "@type": "Person",
        name: "Your Name",
      },
    }),
  }}
/>

This improves:

  • Rich results

  • Google understanding

  • Content classification


7. Canonical URL Setup

Prevents duplicate content issues.

Example:

alternates: {
  canonical: "/blog/nextjs-seo-guide",
}

Very important for:

  • Pagination

  • Filtered product pages

  • Query parameters


8. Create Sitemap

Create:

app/sitemap.js

export default async function sitemap() {
  const posts = await fetch("https://api.example.com/posts").then(res => res.json());

  return [
    {
      url: "https://yourdomain.com",
      lastModified: new Date(),
    },
    ...posts.map(post => ({
      url: `https://yourdomain.com/blog/${post.slug}`,
      lastModified: new Date(post.updatedAt),
    })),
  ];
}

Your sitemap will be available at:

/sitemap.xml


9. Add robots.txt

Create:

app/robots.js

export default function robots() {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: "https://yourdomain.com/sitemap.xml",
  };
}

10. Final Production SEO Checklist

Before publishing:

  • Unique title for every page

  • Unique description for every page

  • Canonical URL set

  • Open Graph image defined

  • Sitemap working

  • robots.txt working

  • Dynamic pages using generateMetadata()

  • No duplicate content issues

  • Good Core Web Vitals


Final Folder Structure

app/
  layout.js
  sitemap.js
  robots.js
  about/page.jsx
  blog/
    [slug]/
      page.jsx
      opengraph-image.jsx

#Next.js SEO Setup#Next.js SEO#Next.js App Router SEO#Static and Dynamic SEO in Next.js#Open Graph in Next.js#Next.js Sitemap#Next.js Metadata API#Next.js Structured Data#SEO in Next.js#App Router
DS

Darshan Suthar

JavaScript Developer (Next.js & React Native)

I build accessible and high-performance modern web applications. Passionate about open source, UI/UX design, and sharing knowledge through writing.