# What You'll Learn#
This guide is a practical Next.js technical SEO audit checklist for Next.js App Router projects. Each step explains what to check, how to implement or fix it, and how it affects crawling, indexing, and performance.
If you want the strategic background first, read Why Next.js for SEO and then come back to this checklist for execution.
# Prerequisites and Tools#
| Tool | Why you need it | What to look for |
|---|---|---|
| Google Search Console | Indexing and canonical signals | Coverage, sitemaps, URL Inspection, rich results |
| Lighthouse or PageSpeed Insights | Lab performance | LCP, CLS, INP, TTFB, render-blocking |
| Chrome DevTools Performance | Debug runtime issues | Long tasks, layout shifts, hydration cost |
| Curl | Fast header and robots checks | Status codes, caching, X-Robots-Tag |
| Structured Data Testing | Validate JSON-LD | Errors, warnings, eligibility |
# Step 1: Confirm Rendering Strategy per Route#
Your first SEO risk in Next.js is unintentionally shipping pages that look fine to users but are harder to crawl or index because critical content is gated behind client-only rendering or delayed data fetching.
Checklist#
| Check | Pass criteria | Why it matters |
|---|---|---|
| Marketing pages render on the server | HTML contains primary content without JS | Bots and social crawlers get content instantly |
| Dynamic pages avoid client-only shells | No empty div with content only after hydration | Prevents thin HTML and rendering delays |
| Status codes are correct | 200, 301, 404, 410 as intended | Indexing depends on accurate HTTP semantics |
Quick verification#
- 1Open a page.
- 2View page source.
- 3Confirm main headings and body copy are present in HTML.
🎯 Key Takeaway: If your primary content is missing from the initial HTML, you are relying on Google to render JS. That increases crawl cost and delays indexing.
# Step 2: Audit Indexability Signals (Meta Robots and X-Robots-Tag)#
Indexing problems are often self-inflicted: accidental noindex, nofollow, or blocked paths.
What to check#
| Surface | Where it can be set | Typical failure |
|---|---|---|
| Meta robots | Metadata API or manual meta tags | noindex applied globally in a layout |
| X-Robots-Tag header | Reverse proxy, CDN, middleware, route handlers | Staging header leaking into production |
| Robots.txt | app/robots.ts or static file | Disallowing important sections |
Implement robots directives using Metadata API#
// app/(marketing)/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-snippet': -1,
'max-image-preview': 'large',
'max-video-preview': -1,
},
},
};Add noindex for non-public areas only#
// app/(app)/account/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
};Header-level check with curl#
curl -I https://example.com/Look for unintended x-robots-tag: noindex.
⚠️ Warning: A single global layout-level
robots: { index: false }can deindex an entire site, even if your robots.txt and sitemap are correct.
# Step 3: Canonical URLs and Duplicate Content Controls#
Canonicals are how you tell search engines which URL is the “main” version when multiple URLs can show the same content.
In Next.js, duplication commonly happens due to:
- Trailing slash variants
- Query parameters
- Pagination pages
- Locale routes
- Multiple category paths pointing to the same item
Canonical checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| Every indexable page has a self canonical | Canonical matches preferred URL | Consolidates ranking signals |
| Canonical points to a 200 indexable URL | No canonical to 404 or redirected URL | Prevents canonical ignoring |
| Parameters do not create duplicate canonicals | Canonical strips tracking params | Avoids splitting authority |
Implement canonical with metadataBase and alternates.canonical#
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
};// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
props: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await props.params;
return {
title: `Post: ${slug}`,
alternates: {
canonical: `/blog/${slug}`,
},
};
}Normalize redirects for trailing slash#
Do this at the edge or in your platform rules. In Next.js, you can also use redirects.
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/:path*/',
destination: '/:path*',
permanent: true,
},
];
},
};💡 Tip: Treat canonicals as a consistency test: if your canonical logic depends on request headers or query strings, you will eventually generate conflicting canonicals and confuse indexing.
# Step 4: Metadata Coverage Audit (Titles, Descriptions, Open Graph)#
Missing or duplicated metadata reduces CTR and can weaken relevance signals. With App Router, use generateMetadata so every dynamic page has deterministic metadata.
Minimum metadata checklist#
| Element | Recommendation | Why it matters |
|---|---|---|
| Title | Unique, keyword-aligned, 50 to 60 chars | CTR and topical relevance |
| Meta description | Unique, 140 to 160 chars | CTR and snippet control |
| Open Graph | Title, description, image, URL | Social previews, sharing |
| Twitter card | summary_large_image | Consistent previews |
Example: dynamic metadata with Open Graph#
// app/products/[id]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
props: { params: Promise<{ id: string }> }
): Promise<Metadata> {
const { id } = await props.params;
const product = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store',
}).then((r) => r.json());
return {
title: `${product.name} | Example Store`,
description: product.shortDescription,
alternates: { canonical: `/products/${id}` },
openGraph: {
title: `${product.name} | Example Store`,
description: product.shortDescription,
url: `/products/${id}`,
images: [
{ url: product.ogImageUrl, width: 1200, height: 630, alt: product.name },
],
type: 'product',
},
twitter: {
card: 'summary_large_image',
title: `${product.name} | Example Store`,
description: product.shortDescription,
images: [product.ogImageUrl],
},
};
}If you need a broader SEO foundation across teams, SEO for Developers is a good baseline for aligning code and content decisions.
# Step 5: Robots.txt and XML Sitemap (App Router-native)#
Robots.txt affects crawling. Sitemaps affect discovery and crawl efficiency. Both directly impact how fast new pages get indexed and how consistently large sites get revisited.
Implement robots.txt in App Router#
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/account', '/checkout', '/api'],
},
],
sitemap: 'https://example.com/sitemap.xml',
};
}Implement sitemap.xml in App Router#
// app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
}).then((r) => r.json());
return [
{ url: 'https://example.com/', lastModified: new Date() },
...posts.map((p: { slug: string; updatedAt: string }) => ({
url: `https://example.com/blog/${p.slug}`,
lastModified: new Date(p.updatedAt),
})),
];
}Sitemap audit checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| Sitemap returns 200 | No auth, no caching errors | Google can fetch reliably |
| URLs are absolute | Full https URLs | Prevents parsing ambiguity |
| Only canonical URLs included | No parameter variants | Improves crawl budget efficiency |
| lastModified is realistic | Matches real updates | Better recrawl timing |
ℹ️ Note: Google does not guarantee using
lastmod, but accurate timestamps correlate with smarter recrawling on large sites.
# Step 6: Pagination and Facets (Prevent Crawl Traps)#
Pagination and filters can explode into thousands of URLs. That can dilute crawl budget and create duplicate content clusters.
Pagination checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| Paginated pages are indexable only if valuable | Page 1 indexable, deeper pages case-by-case | Avoids thin index bloat |
| Canonical strategy is consistent | Self-canonical per page, or canonical to page 1 | Consolidates or preserves long-tail |
| Internal links are crawlable | Real links, not JS-only handlers | Discovery and crawl paths |
Implement paginated route with consistent canonicals#
// app/blog/page/[page]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
props: { params: Promise<{ page: string }> }
): Promise<Metadata> {
const { page } = await props.params;
const pageNum = Number(page);
return {
title: `Blog - Page ${pageNum}`,
alternates: {
canonical: `/blog/page/${pageNum}`,
},
};
}Prevent indexing of infinite filter combinations#
If you have facet URLs like /search?color=red&size=m, either:
- Block crawling via robots.txt for the parameterized path pattern where possible, or
- Keep crawlable but set
noindexon low-value combinations.
A pragmatic rule: index only filters with search demand and stable inventory.
⚠️ Warning: Blocking via robots.txt prevents crawling, not indexing, when external links exist. If a URL is already discovered,
noindexis the reliable deindexing directive, but it requires the page to be crawlable.
# Step 7: HTTP Status Codes, Redirects, and Error Pages#
Google treats redirects and errors as strong signals. A Next.js site with incorrect 200 responses for missing pages will accumulate soft 404s and waste crawl.
Checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| 404 pages return real 404 | Not a 200 with “Not found” text | Prevents soft 404 classification |
| Redirect chains are avoided | One hop max for common URLs | Faster crawling, less loss of signals |
| 410 used for permanently removed content | Optional but useful | Faster deindexing than 404 in practice |
App Router 404 handling#
Use notFound() for missing records.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
export default async function Page(
props: { params: Promise<{ slug: string }> }
) {
const { slug } = await props.params;
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (res.status === 404) notFound();
const post = await res.json();
return <main><h1>{post.title}</h1></main>;
}Keep redirects centralized and stable.
// next.config.js
module.exports = {
async redirects() {
return [
{ source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
];
},
};# Step 8: Core Web Vitals Audit (LCP, INP, CLS) for App Router#
Performance is now inseparable from SEO. Google’s Core Web Vitals are evaluated on real-user data, typically at the 75th percentile.
Targets to hit:
LCP <= 2.5sINP <= 200msCLS <= 0.1
For a deeper performance playbook, use Website Performance Optimization.
What to check in Next.js specifically#
| Metric | Most common Next.js causes | Fixes that move the needle |
|---|---|---|
| LCP | Unoptimized hero image, slow TTFB, blocking fonts | Next Image, caching, streaming SSR, font optimization |
| INP | Heavy hydration, large client bundles, expensive event handlers | Move logic server-side, reduce JS, code split, use Server Components |
| CLS | Late-loading images, dynamic injected banners, font swaps | Explicit sizes, stable layout, font-display strategy |
LCP: fix hero image and priority#
// app/(marketing)/page.tsx
import Image from 'next/image';
export default function Home() {
return (
<main>
<Image
src="/images/hero.jpg"
alt="Product screenshot"
width={1600}
height={900}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
<h1>Fast, SEO-friendly Next.js websites</h1>
</main>
);
}INP: reduce client components and hydration surface#
Audit where you use "use client". In App Router, many UI pieces can be Server Components by default, which can cut JS shipped to the browser.
A simple rule: if it does not need browser-only APIs, do not make it a client component.
CLS: reserve space for dynamic elements#
If you inject banners or consent UI, reserve space to avoid shifts. Also ensure all images have explicit dimensions.
💡 Tip: Measure with real-user monitoring. Lab tools are good for debugging, but only field data tells you whether 75th percentile users meet LCP and INP thresholds.
# Step 9: Caching and Revalidation (Performance and Crawl Efficiency)#
Fast responses improve user experience and allow search engines to crawl more URLs per unit time. Poor caching often shows up as high TTFB and slow LCP.
Checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| Static where possible | Marketing and evergreen pages pre-rendered | Faster indexing and better CWV |
| Revalidation is intentional | revalidate aligned with content freshness | Avoids stale SERP snippets |
| CDN caching not broken | Cache headers are consistent | Reduces origin load and speeds bots |
Example: revalidate content every hour#
// app/blog/[slug]/page.tsx
export const revalidate = 3600;Example: fetch caching aligned with revalidation#
// app/blog/[slug]/page.tsx
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
}).then((r) => r.json());# Step 10: Structured Data (JSON-LD) Audit and Implementation#
Structured data improves eligibility for rich results and reduces ambiguity about entities like articles, products, breadcrumbs, and organizations. It does not guarantee rich results, but it increases your chances when content and policies match.
Schema checklist#
| Schema type | Where it helps | Minimum fields to validate |
|---|---|---|
| Organization | Brand knowledge and trust | name, url, logo |
| WebSite + SearchAction | Sitelinks search box eligibility | url, potentialAction |
| BreadcrumbList | Breadcrumb rich results | itemListElement |
| Article | Article rich results | headline, image, datePublished, author |
| Product | Product rich results | name, offers, aggregateRating if present |
Add JSON-LD safely in App Router#
In MDX and JSX, scripts are allowed, but keep it minimal and valid. The simplest approach is to render a script tag with application/ld+json.
// app/blog/[slug]/StructuredData.tsx
export function ArticleJsonLd(props: {
url: string;
title: string;
imageUrl: string;
datePublished: string;
dateModified: string;
}) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
mainEntityOfPage: props.url,
headline: props.title,
image: [props.imageUrl],
datePublished: props.datePublished,
dateModified: props.dateModified,
author: [{ '@type': 'Person', name: 'Adrijan Omićević' }],
publisher: {
'@type': 'Organization',
name: 'Samioda',
logo: {
'@type': 'ImageObject',
url: 'https://example.com/logo.png',
},
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}Then include it on the page:
// app/blog/[slug]/page.tsx
import { ArticleJsonLd } from './StructuredData';
export default async function Page(
props: { params: Promise<{ slug: string }> }
) {
const { slug } = await props.params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) => r.json());
return (
<main>
<ArticleJsonLd
url={`https://example.com/blog/${slug}`}
title={post.title}
imageUrl={post.ogImageUrl}
datePublished={post.publishedAt}
dateModified={post.updatedAt}
/>
<h1>{post.title}</h1>
<article>{post.content}</article>
</main>
);
}Breadcrumb schema example#
// app/components/BreadcrumbJsonLd.tsx
export function BreadcrumbJsonLd(props: {
items: Array<{ name: string; url: string }>;
}) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: props.items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}# Step 11: Internal Linking and Crawl Depth#
Even with perfect sitemaps, internal links drive most discovery and priority signals. Pages more than ~3 clicks deep often get crawled less frequently on medium-sized sites.
Checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| Category and hub pages exist | Clear topic hubs with links to key pages | Better discovery and relevance |
| Links are actual anchor links | Not click handlers only | Crawlable navigation |
| No orphan pages | Every indexable URL linked internally | Prevents low discovery and weak signals |
Use server-rendered navigation components and ensure links are standard Next.js Link with real href values.
# Step 12: Audit Output Consistency Between Environments#
A common production-only SEO bug is environment drift: staging differs from production in indexing, canonical hostnames, or robots directives.
Checklist#
| Check | Pass criteria | Impact |
|---|---|---|
| metadataBase uses the production domain | No staging URLs in canonicals or OG URLs | Prevents wrong canonical consolidation |
| robots rules differ appropriately | Staging is blocked, production is open | Avoids staging indexing |
| Sitemaps list only production URLs | No mixed hosts | Prevents wasted crawl and confusion |
If your environment variables can influence metadata, lock them down and validate with automated checks in CI.
# Key Takeaways#
- Treat indexability as a first-class feature: audit
robots, meta robots, and X-Robots-Tag to prevent accidental deindexing. - Generate deterministic metadata and canonicals with the App Router Metadata API, especially for dynamic routes and pagination.
- Ship App Router-native
robots.tsandsitemap.tsand keep sitemaps strictly canonical to improve discovery and crawl efficiency. - Optimize Core Web Vitals with Next Image for LCP, fewer client components for INP, and stable layout sizing for CLS.
- Add JSON-LD schema for the content types you publish and validate in Search Console to improve rich result eligibility.
# Conclusion#
A solid Next.js technical SEO audit checklist is mostly about consistency: consistent indexability signals, consistent canonicals, consistent metadata, and consistent performance. If you implement the steps above, you reduce crawl waste, speed up indexing, and protect rankings from regression during releases.
If you want Samioda to run this audit on your Next.js App Router project and ship the fixes, contact us through our site and we will provide a prioritized backlog tied to indexing impact and Core Web Vitals wins.
FAQ
More in Web Development
All →Dynamic Open Graph Images in Next.js: OG Generation, Caching, Fonts, and Edge Runtime Tips
A practical 2026 guide to Next.js dynamic Open Graph images: per-page OG generation, font loading, cache headers, Edge runtime gotchas, and deployment troubleshooting.
Building a React Design System with Design Tokens: Tailwind CSS + Radix UI + TypeScript
A practical 2026 guide to building a React design system with Tailwind and Radix using design tokens, accessible primitives, theming, and reusable packages across multiple apps.
Next.js + Supabase SaaS Starter Architecture (App Router): Auth, RLS, Billing, and Multi-Tenancy
A production-ready blueprint for a Next.js App Router + Supabase SaaS starter architecture: auth, Postgres data model, RLS policies, Stripe billing, and multi-tenant organization design with concrete examples.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Next.js App Router Migration Checklist (From Pages Router) + Common Pitfalls
A practical, step-by-step Next.js App Router migration plan from Pages Router, including a checklist for routing, data fetching, SEO metadata, deployment, and a troubleshooting guide for common pitfalls.
Website Performance Optimization: The Complete Checklist (Next.js + Core Web Vitals) for 2026
A practical, production-ready checklist for website performance optimization in Next.js: Core Web Vitals, images, lazy loading, CDN, and caching—plus before/after metrics and copy-paste config.
Dynamic Open Graph Images in Next.js: OG Generation, Caching, Fonts, and Edge Runtime Tips
A practical 2026 guide to Next.js dynamic Open Graph images: per-page OG generation, font loading, cache headers, Edge runtime gotchas, and deployment troubleshooting.