Next.js SEO Optimization
Comprehensive SEO guide for Next.js App Router applications.
Quick SEO Audit
Run this checklist for any Next.js project:
- Check robots.txt:
curl https://your-site.com/robots.txt
- Check sitemap:
curl https://your-site.com/sitemap.xml
- Check metadata: View page source, search for
<title> and <meta name="description">
- Check JSON-LD: View page source, search for
application/ld+json
- Check Core Web Vitals: Use PageSpeed Insights (pagespeed.web.dev) and the Search Console CWV report for field data — Lighthouse is lab-only and can't measure INP
Essential Files
app/layout.tsx - Root Metadata
import type { Metadata, Viewport } from 'next';
// Viewport must be a separate export — `themeColor`, `colorScheme`, and
// `viewport` inside the `metadata` object are deprecated (since v14: still
// emitted with a warning today, not guaranteed to stay).
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 5,
userScalable: true,
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
],
};
export const metadata: Metadata = {
metadataBase: new URL('https://your-site.com'),
title: {
default: 'Site Title - Main Keyword',
template: '%s | Site Name',
},
// ~150-160 chars is a guideline, not a limit — Google truncates per device/query
description: 'Compelling description with target keywords',
// No `keywords` field: Google ignores the keywords meta tag entirely
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://your-site.com',
siteName: 'Site Name',
title: 'Site Title',
description: 'Description for social sharing',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'Site preview' }],
},
twitter: {
card: 'summary_large_image',
title: 'Site Title',
description: 'Description for Twitter',
images: ['/og-image.png'],
},
alternates: {
canonical: '/',
},
robots: {
index: true,
follow: true,
},
};
app/sitemap.ts - Dynamic Sitemap
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://your-site.com';
const posts = await getPosts(); // your CMS/DB
return [
{
url: baseUrl,
images: [`${baseUrl}/og-image.png`], // Image Sitemap entry
},
{ url: `${baseUrl}/about` },
...posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt, // real content timestamp
})),
];
}
lastModified must reflect the content's actual last change (CMS updatedAt, file mtime, git commit date) — Google uses lastmod only when it's consistently accurate, and new Date() on every build marks everything "just changed", which teaches Google to ignore it. Skip changeFrequency and priority: Google ignores both.
app/robots.ts - Robots Configuration
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://your-site.com';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/'],
// Do NOT disallow /_next/ — crawlers need render-critical CSS/JS
// Do NOT add bot-specific rules (Googlebot, Bingbot) unless overriding wildcard —
// and if you do, repeat all disallows: named groups don't inherit `*` rules
// (RFC 9309 §2.2.1; Google never merges a specific group with `*`)
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}
host was omitted intentionally — it's a non-standard directive Google ignores. Use canonical URLs / 301s to declare the preferred host instead. See references/sitemap-robots.md.
app/manifest.ts - Web App Manifest
Same MetadataRoute family as sitemap/robots, placed at the root of app/. Not an SEO requirement — a PWA-completeness nicety with no ranking effect; skip it unless the site is (or may become) a PWA. Full example in references/metadata-api.md.
OG / Twitter Images
Three ways to set social images — prefer the file conventions over hand-syncing URLs in the metadata object:
- External URL in metadata (the
openGraph.images / twitter.images examples above) — fine for externally hosted images.
- Static file convention (recommended default): drop
opengraph-image.(png|jpg|gif) and/or twitter-image.* into a route segment (app/opengraph-image.png for the root, app/blog/opengraph-image.png for /blog). Next.js auto-emits og:image/twitter:image + :type/:width/:height. A deeper, more specific image overrides one above it. Add alt text with a sibling opengraph-image.alt.txt. Build fails if the file exceeds 8 MB (OG) / 5 MB (Twitter).
- Dynamic generation with
ImageResponse (per-page/per-post images): an opengraph-image.tsx in the route segment exporting alt, size, contentType and a default Image({ params }) (params is a Promise in v16) that returns new ImageResponse(<jsx/>, { ...size }). Renders via Satori — flexbox only, no display: grid; statically optimized at build time unless it reads request-time data. Full example, fonts, generateImageMetadata and the favicon/icon.tsx/apple-icon conventions: references/metadata-api.md.
Key Principles
Cache Components & SEO
With cacheComponents: true in next.config.ts (the v16 top-level flag that unifies the old experimental.dynamicIO/ppr/useCache), use the "use cache" directive for SEO-critical server components:
// app/(home)/sections/hero-section.tsx
import { cacheLife, cacheTag } from "next/cache";
export async function HeroSection() {
"use cache";
cacheLife("hours"); // SEO content that changes a few times/day; see profiles below
cacheTag("hero"); // Invalidate via updateTag("hero") in a Server Action
const data = await fetchData();
return <div>{/* SEO-visible content */}</div>;
}
Built-in cacheLife profiles (stale / revalidate / expire): seconds (30s/1s/1m), minutes (5m/1m/1h), hours (5m/1h/1d), days (5m/1d/1w), weeks (5m/1w/30d), max (5m/30d/1y), and the implicit default (5m/15m/never). For SEO pages pick by how often content changes — days for blog/docs, max for legal/marketing. (minutes revalidates every 1 min — too aggressive for most SEO content.)
Key rules:
"use cache" must be the first statement in the function body (or at the top of the file for file-level caching)
- No
cookies()/headers()/searchParams inside a plain "use cache" scope — good for SEO, since indexable content should be request-agnostic. ("use cache: private" does allow them, but is never prerendered, so it never lands in the static SEO shell.)
- Invalidate with
updateTag("hero") inside a Server Action (read-your-writes; it throws outside one), or revalidateTag("hero", "max") from a Route Handler / webhook (pass the profile — the one-argument form is legacy behaviour) — prefer these over export const revalidate
- Very short cache profiles can change what Next.js includes in the prerendered shell. Do not infer that behavior from
revalidate alone: choose a profile from the documented freshness requirements and verify the installed Next.js version's next build output. Prefer hours/days/max for SEO-critical content unless the product genuinely needs fresher data
- Sitemaps and metadata are static by default — only add
"use cache" (+ cacheTag) if they fetch CMS/dynamic data you want to invalidate on publish
Rendering Strategy for SEO
| Strategy |
Use When |
SEO Impact |
| "use cache" |
Server components with periodic data |
Best - cached HTML, fast TTFB |
| SSG (Static) |
Content rarely changes |
Best - pre-rendered HTML |
| SSR |
Dynamic content per request |
Great - server-rendered |
| CSR |
Dashboards, authenticated areas |
Poor - avoid for SEO pages |
Core Web Vitals Targets
| Metric |
Target |
Impact |
| LCP (Largest Contentful Paint) |
< 2.5s |
Loading speed |
| INP (Interaction to Next Paint) |
< 200ms |
Interactivity |
| CLS (Cumulative Layout Shift) |
< 0.1 |
Visual stability |
- Measured on field data, not lab. Google ranks on the 75th percentile of real users (Chrome UX Report, 28-day rolling window, mobile/desktop separate). A URL group passes only when ≥75% of visits hit "Good" on all three. Use PageSpeed Insights and the Search Console CWV report for the real signal — Lighthouse is lab-only and cannot measure INP.
- INP replaced FID as a Core Web Vital on 2024-03-12; FID is deprecated. INP is the most commonly failed metric — prioritize it.
- Page experience is a tiebreaker, not a standalone ranking system (Google de-emphasized it). Good CWV won't rescue thin content; content relevance and quality come first. Treat CWV as baseline UX hygiene.
- Myths to ignore: 2026 SEO blogs falsely claim "LCP was lowered to 2.0s" and invent an "Engagement Reliability" metric. Neither exists in any Google/web.dev source — the LCP and CLS thresholds are unchanged since 2021, and INP's 200 ms has been fixed since it became a Core Web Vital in 2024.
Ranking Signals Beyond Technical SEO
Metadata + CWV alone don't drive rankings. Keep these in mind (out of scope for this skill, but pointers):
- Helpful content is part of core ranking (since 2024-03), evaluated continuously — not an episodic penalty.
- E-E-A-T (Experience, Expertise, Authoritativeness, Trust): cite real authors/credentials and first-hand experience, especially on YMYL pages.
- Mobile-first indexing is complete (since 2024-07): Google indexes the mobile rendering only. Ensure the mobile view has the same content, metadata, and structured data as desktop; never block mobile resources. (Mostly automatic with Next.js responsive design.)
References
- Metadata API — references/metadata-api.md: read when writing
generateMetadata, OG/icon files, ImageResponse, the manifest, or when streaming metadata / htmlLimitedBots is in play
- Sitemap & Robots — references/sitemap-robots.md: read for
generateSitemaps, image/video sitemaps, multi-group robots rules, static robots.txt/sitemap.xml files
- JSON-LD Structured Data — references/json-ld.md: read before adding any schema type; has the supported/deprecated rich-result list and the
@graph pattern
- AI Search (GEO/AEO) & AI Crawlers — references/ai-search.md: read when deciding robots rules for GPTBot/OAI-SearchBot/ClaudeBot etc., or when asked about llms.txt or AI Overviews
- SEO Audit Checklist — references/checklist.md: read when asked to audit a site end to end
- Troubleshooting — references/troubleshooting.md: read when a page is missing from Google, stuck in "Discovered/Crawled – currently not indexed", or indexes fine but never hydrates
Common Mistakes to Avoid
- Mixing next-seo with Metadata API - Use only Metadata API in App Router
- Missing canonical URLs - Set a self-referencing
alternates.canonical when duplicate/parameterized URLs are a risk; it's a hint, not a requirement — Google may pick its own canonical
- Using CSR for SEO pages - Use SSG/SSR for indexable content
- Blocking
/_next/ in robots.txt - Crawlers need render-critical CSS/JS; never disallow /_next/
- Missing metadataBase - Required for relative URLs in metadata
- Viewport in metadata - Must be a separate export
- Mixing metadata object and generateMetadata - Use one or the other in the same route segment
- Duplicating icons in metadata + file conventions - Prefer
favicon.ico/icon.*/opengraph-image.* file conventions; they auto-emit tags and override the metadata object
- Blanket-blocking AI crawlers -
GPTBot disallow: / blocks training but leaves you in AI search; don't accidentally block citation bots (OAI-SearchBot, PerplexityBot). See references/ai-search.md
- Adding the
keywords meta tag for Google - Google ignores it entirely (no indexing or ranking effect); it's noise, not a signal
- Assuming named robots.txt groups inherit
* rules - Per RFC 9309 §2.2.1 the * group applies only when no group matches, and Google never merges a specific group with *. A { userAgent: 'OAI-SearchBot', allow: '/' } group drops the wildcard's /api///admin/ disallows — repeat them in every named group
- Trusting browser view for bot metadata - PPR + streaming metadata has served bots pages with no
<title>/canonical (vercel/next.js #95406 — check its status on your version), and the browser view never shows it. Confirm production HTML with a bot User-Agent: curl -A "Googlebot" https://your-site.com | grep -E '<title>|canonical'
- Assuming a route that indexes well also works - A PPR route (
◐ in the build output) can serve perfect SEO HTML while none of its <Suspense> boundaries hydrate on a direct load. Load the route directly in a browser and interact with it; the observation and the check are in references/troubleshooting.md.
Quick Fixes
Add noindex to a page
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
};
Dynamic metadata per page
type Props = { params: Promise<{ id: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params; // params is a Promise in current Next.js
const product = await getProduct(id);
return {
title: product.name,
description: product.description,
};
}
Canonical for dynamic routes
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
return {
alternates: {
canonical: `/products/${slug}`,
},
};
}
1---2name: nextjs-seo3description: Next.js App Router SEO optimization and auditing. Use when implementing or fixing SEO in a Next.js app — metadata and generateMetadata, viewport/themeColor, Open Graph and og/twitter images (file conventions + ImageResponse), web app manifest, favicons/icons, sitemap.xml, robots.txt, canonical URLs, hreflang/i18n alternates, JSON-LD structured data and rich results, Core Web Vitals (LCP/INP/CLS), AI search/GEO and AI crawler rules (GPTBot, OAI-SearchBot), or diagnosing Google indexing problems (Search Console, "Discovered/Crawled - currently not indexed"). Also use to run an SEO audit checklist. Not for general Next.js feature work unrelated to SEO.4---56# Next.js SEO Optimization78Comprehensive SEO guide for Next.js App Router applications.910## Quick SEO Audit1112Run this checklist for any Next.js project:13141. **Check robots.txt**: `curl https://your-site.com/robots.txt`152. **Check sitemap**: `curl https://your-site.com/sitemap.xml`163. **Check metadata**: View page source, search for `<title>` and `<meta name="description">`174. **Check JSON-LD**: View page source, search for `application/ld+json`185. **Check Core Web Vitals**: Use PageSpeed Insights (pagespeed.web.dev) and the Search Console CWV report for field data — Lighthouse is lab-only and can't measure INP1920## Essential Files2122### app/layout.tsx - Root Metadata2324```typescript25import type { Metadata, Viewport } from 'next';2627// Viewport must be a separate export — `themeColor`, `colorScheme`, and28// `viewport` inside the `metadata` object are deprecated (since v14: still29// emitted with a warning today, not guaranteed to stay).30export const viewport: Viewport = {31 width: 'device-width',32 initialScale: 1,33 maximumScale: 5,34 userScalable: true,35 themeColor: [36 { media: '(prefers-color-scheme: light)', color: '#ffffff' },37 { media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },38 ],39};4041export const metadata: Metadata = {42 metadataBase: new URL('https://your-site.com'),43 title: {44 default: 'Site Title - Main Keyword',45 template: '%s | Site Name',46 },47 // ~150-160 chars is a guideline, not a limit — Google truncates per device/query48 description: 'Compelling description with target keywords',49 // No `keywords` field: Google ignores the keywords meta tag entirely50 openGraph: {51 type: 'website',52 locale: 'en_US',53 url: 'https://your-site.com',54 siteName: 'Site Name',55 title: 'Site Title',56 description: 'Description for social sharing',57 images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'Site preview' }],58 },59 twitter: {60 card: 'summary_large_image',61 title: 'Site Title',62 description: 'Description for Twitter',63 images: ['/og-image.png'],64 },65 alternates: {66 canonical: '/',67 },68 robots: {69 index: true,70 follow: true,71 },72};73```7475### app/sitemap.ts - Dynamic Sitemap7677```typescript78import type { MetadataRoute } from 'next';7980export default async function sitemap(): Promise<MetadataRoute.Sitemap> {81 const baseUrl = 'https://your-site.com';82 const posts = await getPosts(); // your CMS/DB8384 return [85 {86 url: baseUrl,87 images: [`${baseUrl}/og-image.png`], // Image Sitemap entry88 },89 { url: `${baseUrl}/about` },90 ...posts.map((post) => ({91 url: `${baseUrl}/blog/${post.slug}`,92 lastModified: post.updatedAt, // real content timestamp93 })),94 ];95}96```9798`lastModified` must reflect the content's actual last change (CMS `updatedAt`, file mtime, git commit date) — Google uses `lastmod` only when it's consistently accurate, and `new Date()` on every build marks everything "just changed", which teaches Google to ignore it. Skip `changeFrequency` and `priority`: Google ignores both.99100### app/robots.ts - Robots Configuration101102```typescript103import type { MetadataRoute } from 'next';104105export default function robots(): MetadataRoute.Robots {106 const baseUrl = 'https://your-site.com';107108 return {109 rules: [110 {111 userAgent: '*',112 allow: '/',113 disallow: ['/api/', '/admin/'],114 // Do NOT disallow /_next/ — crawlers need render-critical CSS/JS115 // Do NOT add bot-specific rules (Googlebot, Bingbot) unless overriding wildcard —116 // and if you do, repeat all disallows: named groups don't inherit `*` rules117 // (RFC 9309 §2.2.1; Google never merges a specific group with `*`)118 },119 ],120 sitemap: `${baseUrl}/sitemap.xml`,121 };122}123```124125> `host` was omitted intentionally — it's a non-standard directive Google ignores. Use canonical URLs / 301s to declare the preferred host instead. See [references/sitemap-robots.md](references/sitemap-robots.md).126127### app/manifest.ts - Web App Manifest128129Same `MetadataRoute` family as sitemap/robots, placed at the root of `app/`. **Not an SEO requirement** — a PWA-completeness nicety with no ranking effect; skip it unless the site is (or may become) a PWA. Full example in [references/metadata-api.md](references/metadata-api.md#web-app-manifest--icon-file-conventions).130131### OG / Twitter Images132133Three ways to set social images — prefer the file conventions over hand-syncing URLs in the metadata object:1341351. **External URL in metadata** (the `openGraph.images` / `twitter.images` examples above) — fine for externally hosted images.1362. **Static file convention (recommended default):** drop `opengraph-image.(png|jpg|gif)` and/or `twitter-image.*` into a route segment (`app/opengraph-image.png` for the root, `app/blog/opengraph-image.png` for `/blog`). Next.js auto-emits `og:image`/`twitter:image` + `:type/:width/:height`. A deeper, more specific image overrides one above it. Add alt text with a sibling `opengraph-image.alt.txt`. Build fails if the file exceeds 8 MB (OG) / 5 MB (Twitter).1373. **Dynamic generation with `ImageResponse`** (per-page/per-post images): an `opengraph-image.tsx` in the route segment exporting `alt`, `size`, `contentType` and a default `Image({ params })` (params is a Promise in v16) that returns `new ImageResponse(<jsx/>, { ...size })`. Renders via Satori — **flexbox only, no `display: grid`**; statically optimized at build time unless it reads request-time data. Full example, fonts, `generateImageMetadata` and the favicon/`icon.tsx`/`apple-icon` conventions: [references/metadata-api.md](references/metadata-api.md).138139## Key Principles140141### Cache Components & SEO142143With `cacheComponents: true` in next.config.ts (the v16 top-level flag that unifies the old `experimental.dynamicIO`/`ppr`/`useCache`), use the `"use cache"` directive for SEO-critical server components:144145```typescript146// app/(home)/sections/hero-section.tsx147import { cacheLife, cacheTag } from "next/cache";148149export async function HeroSection() {150 "use cache";151 cacheLife("hours"); // SEO content that changes a few times/day; see profiles below152 cacheTag("hero"); // Invalidate via updateTag("hero") in a Server Action153154 const data = await fetchData();155 return <div>{/* SEO-visible content */}</div>;156}157```158159**Built-in `cacheLife` profiles** (`stale` / `revalidate` / `expire`): `seconds` (30s/1s/1m), `minutes` (5m/1m/1h), `hours` (5m/1h/1d), `days` (5m/1d/1w), `weeks` (5m/1w/30d), `max` (5m/30d/1y), and the implicit `default` (5m/15m/never). For SEO pages pick by how often content changes — `days` for blog/docs, `max` for legal/marketing. (`minutes` revalidates every 1 min — too aggressive for most SEO content.)160161**Key rules:**162- `"use cache"` must be the first statement in the function body (or at the top of the file for file-level caching)163- No `cookies()`/`headers()`/`searchParams` inside a plain `"use cache"` scope — good for SEO, since indexable content should be request-agnostic. (`"use cache: private"` *does* allow them, but is never prerendered, so it never lands in the static SEO shell.)164- Invalidate with `updateTag("hero")` inside a Server Action (read-your-writes; it throws outside one), or `revalidateTag("hero", "max")` from a Route Handler / webhook (pass the profile — the one-argument form is legacy behaviour) — prefer these over `export const revalidate`165- Very short cache profiles can change what Next.js includes in the prerendered shell. Do not infer that behavior from `revalidate` alone: choose a profile from the documented freshness requirements and verify the installed Next.js version's `next build` output. Prefer `hours`/`days`/`max` for SEO-critical content unless the product genuinely needs fresher data166- Sitemaps and metadata are static by default — only add `"use cache"` (+ `cacheTag`) if they fetch CMS/dynamic data you want to invalidate on publish167168### Rendering Strategy for SEO169170| Strategy | Use When | SEO Impact |171|----------|----------|------------|172| "use cache" | Server components with periodic data | Best - cached HTML, fast TTFB |173| SSG (Static) | Content rarely changes | Best - pre-rendered HTML |174| SSR | Dynamic content per request | Great - server-rendered |175| CSR | Dashboards, authenticated areas | Poor - avoid for SEO pages |176177### Core Web Vitals Targets178179| Metric | Target | Impact |180|--------|--------|--------|181| LCP (Largest Contentful Paint) | < 2.5s | Loading speed |182| INP (Interaction to Next Paint) | < 200ms | Interactivity |183| CLS (Cumulative Layout Shift) | < 0.1 | Visual stability |184185- **Measured on field data, not lab.** Google ranks on the 75th percentile of real users (Chrome UX Report, 28-day rolling window, mobile/desktop separate). A URL group passes only when ≥75% of visits hit "Good" on all three. Use PageSpeed Insights and the Search Console CWV report for the real signal — **Lighthouse is lab-only and cannot measure INP**.186- **INP replaced FID** as a Core Web Vital on 2024-03-12; FID is deprecated. INP is the most commonly failed metric — prioritize it.187- **Page experience is a tiebreaker, not a standalone ranking system** (Google de-emphasized it). Good CWV won't rescue thin content; content relevance and quality come first. Treat CWV as baseline UX hygiene.188- **Myths to ignore:** 2026 SEO blogs falsely claim "LCP was lowered to 2.0s" and invent an "Engagement Reliability" metric. Neither exists in any Google/web.dev source — the LCP and CLS thresholds are unchanged since 2021, and INP's 200 ms has been fixed since it became a Core Web Vital in 2024.189190### Ranking Signals Beyond Technical SEO191192Metadata + CWV alone don't drive rankings. Keep these in mind (out of scope for this skill, but pointers):193194- **Helpful content** is part of core ranking (since 2024-03), evaluated continuously — not an episodic penalty.195- **E-E-A-T** (Experience, Expertise, Authoritativeness, Trust): cite real authors/credentials and first-hand experience, especially on YMYL pages.196- **Mobile-first indexing is complete** (since 2024-07): Google indexes the mobile rendering only. Ensure the mobile view has the same content, metadata, and structured data as desktop; never block mobile resources. (Mostly automatic with Next.js responsive design.)197198## References199200- **Metadata API** — [references/metadata-api.md](references/metadata-api.md): read when writing `generateMetadata`, OG/icon files, `ImageResponse`, the manifest, or when streaming metadata / `htmlLimitedBots` is in play201- **Sitemap & Robots** — [references/sitemap-robots.md](references/sitemap-robots.md): read for `generateSitemaps`, image/video sitemaps, multi-group robots rules, static `robots.txt`/`sitemap.xml` files202- **JSON-LD Structured Data** — [references/json-ld.md](references/json-ld.md): read before adding any schema type; has the supported/deprecated rich-result list and the `@graph` pattern203- **AI Search (GEO/AEO) & AI Crawlers** — [references/ai-search.md](references/ai-search.md): read when deciding robots rules for GPTBot/OAI-SearchBot/ClaudeBot etc., or when asked about llms.txt or AI Overviews204- **SEO Audit Checklist** — [references/checklist.md](references/checklist.md): read when asked to audit a site end to end205- **Troubleshooting** — [references/troubleshooting.md](references/troubleshooting.md): read when a page is missing from Google, stuck in "Discovered/Crawled – currently not indexed", or indexes fine but never hydrates206207## Common Mistakes to Avoid2082091. **Mixing next-seo with Metadata API** - Use only Metadata API in App Router2102. **Missing canonical URLs** - Set a self-referencing `alternates.canonical` when duplicate/parameterized URLs are a risk; it's a hint, not a requirement — Google may pick its own canonical2113. **Using CSR for SEO pages** - Use SSG/SSR for indexable content2124. **Blocking `/_next/` in robots.txt** - Crawlers need render-critical CSS/JS; never disallow `/_next/`2135. **Missing metadataBase** - Required for relative URLs in metadata2146. **Viewport in metadata** - Must be a separate export2157. **Mixing metadata object and generateMetadata** - Use one or the other in the same route segment2168. **Duplicating icons in metadata + file conventions** - Prefer `favicon.ico`/`icon.*`/`opengraph-image.*` file conventions; they auto-emit tags and override the metadata object2179. **Blanket-blocking AI crawlers** - `GPTBot disallow: /` blocks training but leaves you in AI search; don't accidentally block citation bots (OAI-SearchBot, PerplexityBot). See [references/ai-search.md](references/ai-search.md)21810. **Adding the `keywords` meta tag for Google** - Google ignores it entirely (no indexing or ranking effect); it's noise, not a signal21911. **Assuming named robots.txt groups inherit `*` rules** - Per RFC 9309 §2.2.1 the `*` group applies only when no group matches, and Google never merges a specific group with `*`. A `{ userAgent: 'OAI-SearchBot', allow: '/' }` group drops the wildcard's `/api/`/`/admin/` disallows — repeat them in every named group22012. **Trusting browser view for bot metadata** - PPR + streaming metadata has served bots pages with no `<title>`/canonical (vercel/next.js #95406 — check its status on your version), and the browser view never shows it. Confirm production HTML with a bot User-Agent: `curl -A "Googlebot" https://your-site.com | grep -E '<title>|canonical'`22113. **Assuming a route that indexes well also *works*** - A PPR route (`◐` in the build output) can serve perfect SEO HTML while none of its `<Suspense>` boundaries hydrate on a direct load. Load the route directly in a browser and interact with it; the observation and the check are in [references/troubleshooting.md](references/troubleshooting.md#ppr-route-serves-perfect-seo-html-but-client-components-never-hydrate).222223## Quick Fixes224225### Add noindex to a page226227```typescript228export const metadata: Metadata = {229 robots: {230 index: false,231 follow: false,232 },233};234```235236### Dynamic metadata per page237238```typescript239type Props = { params: Promise<{ id: string }> };240241export async function generateMetadata({ params }: Props): Promise<Metadata> {242 const { id } = await params; // params is a Promise in current Next.js243 const product = await getProduct(id);244 return {245 title: product.name,246 description: product.description,247 };248}249```250251### Canonical for dynamic routes252253```typescript254type Props = { params: Promise<{ slug: string }> };255256export async function generateMetadata({ params }: Props): Promise<Metadata> {257 const { slug } = await params;258 return {259 alternates: {260 canonical: `/products/${slug}`,261 },262 };263}264```