The Ultimate Next.js SEO Guide: From Zero to Page One
A comprehensive, actionable guide to Next.js SEO covering metadata, structured data, Core Web Vitals, rendering strategies, sitemaps, and common pitfalls. Everything you need to rank your Next.js app.
Next.js is one of the best frameworks for SEO, but shipping a Next.js app doesn't automatically mean good search rankings. You still need to handle metadata, structured data, rendering strategies, and performance correctly.
This guide covers everything you need to get your Next.js site from zero to page one.
Why Next.js is great for SEO
Unlike client-side React apps that send an empty HTML shell to the browser, Next.js can render pages on the server. That means search engine crawlers see your full content, links, and metadata without executing JavaScript.
Key advantages:
- Server-side rendering (SSR) and static site generation (SSG) deliver complete HTML to crawlers
- Built-in metadata API makes managing titles, descriptions, and Open Graph tags straightforward
- Automatic code splitting keeps bundle sizes small and pages fast
- Image optimization with
next/imagehandles lazy loading, responsive sizes, and modern formats - File-based routing creates clean, crawlable URL structures
Setting up metadata with the Metadata API
Next.js 14+ has a powerful Metadata API that replaces the old Head component.
Static metadata
For pages where the metadata doesn't change, export a metadata object:
// app/about/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us",
description: "Learn about our team and mission.",
openGraph: {
title: "About Us | My App",
description: "Learn about our team and mission.",
type: "website",
},
};
Dynamic metadata
For pages like blog posts or product pages, use generateMetadata:
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
},
};
}
Title templates
Set a template in your root layout so every page gets a consistent suffix:
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: "My App — Build Faster",
template: "%s | My App",
},
};
Now any child page that sets title: "About Us" will render as "About Us | My App".
Structured data (JSON-LD)
Structured data helps Google understand your content and can trigger rich results (review stars, FAQ dropdowns, breadcrumbs, etc.).
Add JSON-LD directly in your page components:
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.date,
author: { "@type": "Person", name: post.author },
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* content */}</article>
</>
);
Common schema types to implement:
- WebSite with SearchAction (enables sitelinks search box)
- Organization with logo and sameAs
- Article for blog posts
- Product for product pages
- FAQPage for FAQ sections
- BreadcrumbList for navigation breadcrumbs
Rendering strategies and when to use each
Choosing the right rendering strategy directly affects SEO:
Static Site Generation (SSG)
Best for pages that rarely change: blog posts, documentation, landing pages.
- Pre-rendered at build time
- Served from CDN — fastest possible TTFB
- Content is always crawlable
Server-Side Rendering (SSR)
Best for pages with frequently changing content: dashboards, search results, real-time data.
- Rendered on each request
- Always shows fresh data
- Slightly slower TTFB than static
Incremental Static Regeneration (ISR)
Best of both worlds for content that changes periodically:
// Revalidate every 60 seconds
export const revalidate = 60;
Client-side only
Avoid for SEO-critical content. Content loaded via useEffect or client-side fetch won't be in the initial HTML that crawlers see.
Core Web Vitals optimization
Google uses Core Web Vitals as a ranking signal. Here's how to optimize each one in Next.js:
Largest Contentful Paint (LCP)
- Use
next/imagewithpriorityfor above-the-fold images - Preload critical fonts
- Avoid render-blocking third-party scripts
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} priority />
Cumulative Layout Shift (CLS)
- Always set
widthandheighton images - Use
placeholder="blur"on images - Reserve space for dynamic content with CSS
Interaction to Next Paint (INP)
- Keep JavaScript bundles small
- Use
React.lazyand dynamic imports for heavy components - Avoid long-running synchronous tasks
Sitemaps and robots.txt
Next.js makes generating these easy with the App Router:
Sitemap
// app/sitemap.ts
import { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
return [
{ url: "https://example.com", lastModified: new Date(), priority: 1.0 },
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
priority: 0.8,
})),
];
}
Robots.txt
// app/robots.ts
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/", disallow: ["/api/", "/dashboard/"] },
sitemap: "https://example.com/sitemap.xml",
};
}
Canonical URLs
Canonical tags prevent duplicate content issues. Set them in your metadata:
export const metadata: Metadata = {
alternates: {
canonical: "https://example.com/blog/my-post",
},
};
Important canonical rules:
- Paginated pages should self-canonicalize (
/?page=2→/?page=2) - Search/filter pages should canonicalize to the clean listing
- Always use absolute URLs
Open Graph and social previews
Make your pages look great when shared on social media:
// app/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const alt = "My App";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
<div style={{ /* your design */ }}>
<h1>My Page Title</h1>
</div>,
{ ...size }
);
}
Common Next.js SEO mistakes
- Using client-side rendering for important content — If crawlers can't see it, it doesn't exist for SEO
- Missing canonical tags — Leads to duplicate content across pagination, filters, and query parameters
- Not setting image alt text — Missed opportunity for image search traffic
- Blocking crawlers from JavaScript/CSS — Check your robots.txt isn't too restrictive
- Ignoring Core Web Vitals — Performance is a ranking signal; test with PageSpeed Insights
- Using generic meta descriptions — Each page needs a unique, compelling description
- Not submitting a sitemap to Google Search Console — Don't wait for Google to discover your pages
Next.js SEO checklist
Use this checklist for every page:
- Unique, descriptive title under 60 characters
- Compelling meta description under 160 characters
- Canonical URL set
- Open Graph and Twitter meta tags
- Structured data (JSON-LD) where applicable
- All images have descriptive alt text
- Images use
next/imagewith proper sizing - Page loads in under 2.5 seconds (LCP)
- No layout shifts (CLS < 0.1)
- Internal links to related content
- Included in sitemap.xml
- Mobile-responsive layout
Start with a template
Building all of this from scratch takes time. A production-ready Next.js template gives you metadata, structured data, sitemaps, and performance optimization already wired up so you can focus on your product instead of your SEO plumbing.
Ready to ship faster?
Browse production-ready web framework templates and starter kits.
Browse templates