Nextjs SEO
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.
- Skill ID
- laguagu/claude-code-nextjs-skills/nextjs-seo
- Publisher
- laguagu
- Repository
- claude-code-nextjs-skills
- Installs
- 0
- Files
- 7
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
laguagu/claude-code-nextjs-skills/nextjs-seoInstalls these files- SKILL.md
- references/ai-search.md
- references/checklist.md
- references/json-ld.md
- references/metadata-api.md
- references/sitemap-robots.md
- references/troubleshooting.md
What this skill tells the agent
Next.js SEO Optimization
Comprehensive SEO guide for Next.js App Router applications.
Start with evidence and useful content
Read the installed Next.js version and relevant node_modules/next/dist/docs/ guides before changing framework code; use current official docs when local docs are unavailable. Use Vercel MCP documentation search for platform behavior when available, but check retrieved examples against the installed framework. Do not copy a legacy cache API from a search snippet into a newer app.
- Solve the reader's task first. A short useful page does not need padding. Give the H1, subtitle and section headings distinct jobs; do not repeat the same phrase in each for SEO. Keep UI copy concise, concrete and useful.
- Do not add generic introductions, keyword blocks, decorative badges, a FAQ or TL;DR just to make a page look optimized. Add content only when it answers a real question. Preserve necessary explanations and disclosures.
- Create team, location or category pages only when each offers distinct, maintainable value. A swapped name/logo and duplicate list is insufficient.
- Describe seasons, availability and update times from current data. Do not leave pre-launch copy on an active service or change timestamps for freshness.
- Separate evidence: a Search Console performance export shows queries and traffic, not indexing status, Google's canonical choice or CWV. Use the relevant report or URL Inspection; mark unavailable checks as unverified.
- Report findings and verification before predicting impact. No ranking, indexing, rich-result or AI-citation guarantees.
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'],
},
// Set alternates.canonical per page; a root '/' would be inherited by children.
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.imagesexamples above) — fine for externally hosted images. - Static file convention (recommended default): drop
opengraph-image.(png|jpg|gif)and/ortwitter-image.*into a route segment (app/opengraph-image.pngfor the root,app/blog/opengraph-image.pngfor/blog). Next.js auto-emitsog:image/twitter:image+:type/:width/:height. A deeper, more specific image overrides one above it. Add alt text with a siblingopengraph-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.tsxin the route segment exportingalt,size,contentTypeand a defaultImage({ params })(params is a Promise in v16) that returnsnew 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,generateImageMetadataand the favicon/icon.tsx/apple-iconconventions: references/metadata-api.md.
Key Principles
Cache Components & SEO
With cacheComponents: true, use "use cache" for data/components that can be shared and whose freshness requirements permit caching. Static content needs no extra cache directive merely for SEO:
// 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>;
}Choose cacheLife from the product's freshness requirements and the installed
More from laguagu
Use this skill in a project
Open any RiverX project, search the Skills panel for laguagu/claude-code-nextjs-skills/nextjs-seo, and install it in one click.
