The Ultimate Next.js SEO Checklist for 2026
Why Next.js Is Great for SEO (and Where It Can Go Wrong)
Next.js gives you a serious SEO advantage out of the box — static generation, server rendering, and a Metadata API that makes per-page meta tags trivial. But the advantage is only realized if you use these features correctly. A poorly configured Next.js site can rank just as badly as any other.
This checklist covers every SEO lever available in Next.js 16 App Router. Work through it systematically and your site will be in the top tier of technical SEO quality.
1. generateMetadata
The Metadata API is the foundation of on-page SEO in the App Router. Every page needs unique title and description tags.
For static pages, export a `metadata` object:
```tsx
// app/about/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "About — Craftly",
description: "Learn how Craftly helps developers ship faster with beautiful templates.",
};
```
For dynamic pages (blog posts, product pages), use `generateMetadata`:
```tsx
// 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} — Craftly Blog`,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
publishedTime: post.date,
},
};
}
```
**Checklist:**
- [ ] Unique title tag on every page (under 60 characters)
- [ ] Unique meta description on every page (under 155 characters)
- [ ] OpenGraph title and description set
2. Dynamic OG Images with ImageResponse
Open Graph images dramatically increase click-through rates from social media and messaging apps. Next.js generates them server-side using the `ImageResponse` API — no design tools required.
```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function OgImage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return new ImageResponse(
(
<div
style={{
background: "#0f172a",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "80px",
}}
>
<p style={{ color: "#6366f1", fontSize: 24 }}>Craftly Blog</p>
<h1 style={{ color: "#f8fafc", fontSize: 64, fontWeight: 700 }}>
{post.title}
</h1>
</div>
),
{ ...size }
);
}
```
Place this file next to your page file and Next.js serves it automatically at the correct URL.
**Checklist:**
- [ ] OG image on homepage
- [ ] Dynamic OG images on blog posts and key landing pages
- [ ] Image is 1200x630px
3. sitemap.ts
A sitemap tells search engines about every page on your site. In Next.js 16, generate it programmatically:
```ts
// app/sitemap.ts
import { MetadataRoute } from "next";
import { blogPosts } from "@/data/blog";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://yourcraftly.com";
const staticPages = ["/", "/blog", "/about"].map((path) => ({
url: `${baseUrl}${path}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: path === "/" ? 1 : 0.8,
}));
const blogPages = blogPosts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "never" as const,
priority: 0.6,
}));
return [...staticPages, ...blogPages];
}
```
Next.js serves this at `/sitemap.xml` automatically. Submit the URL in Google Search Console.
**Checklist:**
- [ ] sitemap.xml generated and accessible
- [ ] All important pages included
- [ ] Submitted to Google Search Console
4. robots.ts
Control crawler access with a `robots.ts` file:
```ts
// app/robots.ts
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: "https://yourcraftly.com/sitemap.xml",
};
}
```
**Checklist:**
- [ ] robots.txt accessible at /robots.txt
- [ ] Sitemap URL referenced in robots.txt
- [ ] No important pages accidentally blocked
5. JSON-LD Structured Data
Structured data helps Google understand your content and can unlock rich results (star ratings, article dates, breadcrumbs). Add it to your pages as a JSON-LD script tag:
```tsx
// For a blog post page
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.description,
datePublished: post.date,
author: { "@type": "Organization", name: "Craftly" },
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* content */}</article>
</>
);
}
```
**Checklist:**
- [ ] Article schema on blog posts
- [ ] Organization or Person schema on homepage
- [ ] BreadcrumbList on nested pages
- [ ] Validated with Google's Rich Results Test
6. Canonical URLs
Canonical URLs prevent duplicate content penalties when the same content is accessible at multiple URLs (e.g., with and without trailing slashes, or with query parameters).
```tsx
export const metadata: Metadata = {
alternates: {
canonical: "https://yourcraftly.com/blog/my-post",
},
};
```
Set this for every page, especially those accessible via multiple URLs.
**Checklist:**
- [ ] Canonical URL set on every page
- [ ] Self-referencing canonical on paginated content
7. Core Web Vitals Optimization
Google uses Core Web Vitals as a ranking signal. The three metrics to target:
- **LCP (Largest Contentful Paint):** Under 2.5s. Use `priority` on hero images, preload critical fonts.
- **CLS (Cumulative Layout Shift):** Under 0.1. Set explicit `width` and `height` on images, reserve space for dynamic content.
- **INP (Interaction to Next Paint):** Under 200ms. Minimize client-side JavaScript, defer non-critical scripts.
```tsx
// Always set width and height on images to prevent CLS
import Image from "next/image";
<Image
src="/hero.png"
alt="Hero"
width={1200}
height={630}
priority
/>
```
**Checklist:**
- [ ] LCP image has `priority` prop
- [ ] All images have explicit width/height
- [ ] Fonts loaded with `next/font` (eliminates FOUT and reduces CLS)
- [ ] Lighthouse score above 90 on mobile
8. Server Components for Performance
Server components don't ship JavaScript to the browser, which directly improves your Core Web Vitals. Keep as much logic on the server as possible.
```tsx
// Good — fetches data on the server, no client JS
export default async function BlogList() {
const posts = await getPosts(); // runs on server
return <ul>{posts.map(p => <li key={p.slug}>{p.title}</li>)}</ul>;
}
```
Reserve `"use client"` for components that genuinely need interactivity (search boxes, theme toggles, interactive charts). Everything else should be a server component.
9. Image Optimization
The `next/image` component handles compression, format conversion (WebP/AVIF), lazy loading, and responsive sizing automatically. Never use a plain `<img>` tag in production.
```tsx
import Image from "next/image";
<Image
src="/og-image.png"
alt="Descriptive alt text"
width={800}
height={400}
sizes="(max-width: 768px) 100vw, 800px"
/>
```
**Checklist:**
- [ ] All images use `next/image`
- [ ] Alt text on every image (descriptive, not just "image")
- [ ] `sizes` prop set for responsive images
- [ ] Hero image uses `priority`
10. Internal Linking and Heading Structure
Crawlers follow internal links to discover pages. Make sure every important page is linked from at least one other page. Use descriptive anchor text — not "click here."
Headings tell crawlers and screen readers what each section is about. Every page gets exactly one `<h1>`. Sections use `<h2>`. Subsections use `<h3>`.
**Checklist:**
- [ ] One H1 per page, matches the title tag
- [ ] Logical H2/H3 hierarchy
- [ ] All important pages linked from navigation or related content
- [ ] Descriptive anchor text (no "click here" or "read more")
Putting It Into Practice
The Craftly showcase site uses all of these techniques: `generateMetadata` on every page, dynamic OG images for blog posts, a programmatic sitemap, JSON-LD article schema, and `next/image` throughout. It consistently scores 100 on Lighthouse SEO audits.
The good news is that most of this is a one-time setup. Once your `sitemap.ts`, `robots.ts`, and root `layout.tsx` metadata are configured, every new page you add inherits the foundation automatically.
Work through this checklist top to bottom on your next project and you'll start with a technical SEO score that most sites never achieve — even after years of optimization.
Check out [Craftly's template collection](https://getcraftly.gumroad.com) — every template ships with this SEO foundation pre-configured so you can focus on content, not configuration.