nextjs-seo skill
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.
Is the nextjs-seo skill safe?
Clean: nothing in its files matched our rules. We read 7 files in the folder on 2026-09-28.
No findings.
Install the nextjs-seo skill
A skill is a folder. Copy it into your agent's skills folder and the agent loads it when the task matches its description.
git clone --depth 1 https://github.com/laguagu/claude-code-nextjs-skills.git /tmp/claude-code-nextjs-skills mkdir -p ~/.claude/skills cp -r /tmp/claude-code-nextjs-skills/skills/nextjs-seo ~/.claude/skills/nextjs-seo
In the Claude apps, zip the folder and upload it from the Skills settings. The folder on GitHub
The instructions your agent would load
SKILL.md as published, without the frontmatter. Read it on GitHub
Next.js SEO Optimization
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.
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.
- Solve the reader's task first. A short useful page does not need padding.
or TL;DR just to make a page look optimized. Add content only when it answers a real question. Preserve necessary explanations and disclosures.
- Do not add generic introductions, keyword blocks, decorative badges, a FAQ
maintainable value. A swapped name/logo and duplicate list is insufficient.
- Create team, location or category pages only when each offers distinct,
leave pre-launch copy on an active service or change timestamps for freshness.
- Describe seasons, availability and update times from current data. Do not
traffic, not indexing status, Google's canonical choice or CWV. Use the relevant report or URL Inspection; mark unavailable checks as unverified.
- Separate evidence: a Search Console performance export shows queries and
indexing, rich-result or AI-citation guarantees.
- Report findings and verification before predicting impact. No ranking,
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 and
- 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 `metadata` are dropped: Next.js 16 emits no tag, only an
// "Unsupported metadata" build warning.
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' }],
},
twapp/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/'],
// Never disallow /_next/: crawlers need render-critical CSS/JS.
// A named group (Googlebot, OAI-SearchBot…) ignores these rules; see mistake 11.
},
],
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.(jpg|jpeg|png|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(, { ...size }). Renders via Satori — flexbox only, no display: grid, 500 KB bundle cap (JSX, fonts, images), fonts ttf/otf/woff only; 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, 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 Next.js documentation. Do not infer a cache lifetime from the page category; marketing and legal pages also need timely publication and invalidation.
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()/runtime searchParams inside a plain "use cache" scope. Keep public content separate from personalized data; do not accidentally cache private information for all users. Check the installed docs before using experimental private caching.
- Invalidate with updateTag("hero") inside a Server Action (read-your-writes; it throws outside one), or revalidateTag("hero", "max") from a Route Handler / webhook (the one-argument form is deprecated)
- Route segment exports revalidate, dynamic and fetchCache error with Cache Components enabled; without it, follow the installed version's cache model. Verify next build, the served content and publish-time invalidation.
- A database read does not by itself make a sitemap update after deployment. Configure and verify its refresh/invalidation path. Metadata can be static or runtime-dependent; inspect the route rather than assuming.
Rendering Strategy for SEO
These are rendering trade-offs, not ranking tiers. A Client Component can still be server-prerendered; "use client" does not mean its content is absent from HTML.
Core Web Vitals Targets
each metric separately; this does not mean the same 75% of visits pass all three simultaneously. Lighthouse is a lab diagnostic, not an INP field score.
- Use the 75th percentile of field measurements, segmented by device. Evaluate
accurate metadata remain necessary; avoid invented numerical ranking weights.
- Good CWV does not guarantee ranking. Relevant content, crawlability and
Verify authorship and credentials rather than generating them.
- Keep mobile content, metadata and structured data equivalent to desktop.
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 - Not a build error: relative OG/Twitter image URLs fall back to the Vercel deployment URL or http://localhost:3000 with only a warning, and relative canonical/hreflang URLs stay relative (Google requires absolute hreflang URLs)
- Viewport in metadata - themeColor/colorScheme/viewport in metadata are ignored; use export const viewport
- 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 search bots (OAI-SearchBot, Claude-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 - Check status, headers and the complete production response for Googlebot and a relevant HTML-limited bot. Streaming may place metadata outside the initial head. A spoofed User-Agent tests response behavior, not Google's actual crawl access or indexing; use URL Inspection and verified bot logs where available.
Quick Fixes
Add noindex to a page
export const metadata: Metadata = {
robots: {
index: false,
follow: true,
},
};Keep the page crawlable for noindex to be seen. Robots disallow is not an index-removal or access-control mechanism. Preview protection and noindex are separate concerns; see references/sitemap-robots.md.
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}`,
},
};
}More skills from laguagu/claude-code-nextjs-skills
- Aai-appFull-stack AI application generator with Next.js, AI SDK, and ai-elements. Use when creating chatbots, agent dashboards, or custom AI applications.
- Aai-elementsBuild AI chat interfaces with pre-built shadcn-style components (Message, Conversation, PromptInput, Reasoning, Sources, Tool, Artifact, CodeBlock, Suggestion, Task, Image, ChainOfThought, InlineCitation, WebPreview, Checkpoint, Plan, Queue, ModelSelector, and more). Use when adding AI chat UI to a Next.js + AI SDK app, installing AI Elements components via the CLI (`bun x ai-elements@latest add message` or `npx shadcn@latest add @ai-elements/message`), composing message displays with markdown, building prompt inputs with attachments, or rendering streaming reasoning and tool output.
- Aai-sdkAnswer questions about the AI SDK and help build AI-powered features. Use when developers ask about Vercel AI SDK, generateText, streamText, ToolLoopAgent, useChat, providers, tools, structured output, embeddings, streaming, or adding AI to an app. First identify the installed major version and route version-specific work: use ai-sdk-7 for AI SDK 7 features/migrations such as WorkflowAgent, HarnessAgent, reasoning, runtime/tools context, toolApproval, telemetry, realtime, or v6-to-v7 upgrades; use ai-sdk-6 for v6 code.
- Aai-sdk-6Vercel AI SDK v6 development, for projects already on ai@6. Use when building or maintaining AI agents, chatbots, tool integrations, streaming apps, or structured output in a v6 codebase. New projects and ai@7 code use ai-sdk-7; an unknown version goes through ai-sdk. Covers ToolLoopAgent, useChat, generateText, streamText, tool approval, smoothStream, provider tools, MCP integration, and Output patterns.
- Aai-sdk-7Vercel AI SDK v7 development and migration. Use when building or upgrading AI SDK 7 apps, especially ToolLoopAgent, WorkflowAgent, HarnessAgent, Claude Code/Codex/Pi harnesses, runtimeContext, toolsContext, toolApproval, telemetry, reasoning, file or skill uploads, realtime, video generation, or v6-to-v7 breaking changes. For AI SDK v6 code use ai-sdk-6; for version discovery and general doc lookup use ai-sdk.
- Acache-componentsExpert guidance for Next.js Cache Components and Partial Prerendering (PPR). Use when implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, instant navigation validation, 'use cache: private', pass-through/interleaving patterns, GET Route Handler caching, debugging cache issues, and reviewing Cache Component implementations.
- Achrome-devtoolsTests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance (LCP/CLS/INP), or verify visual output with real runtime data. Complements Playwright — use this for live debugging and performance work, Playwright for stable E2E test suites.
- Afrontend-designGuidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
- AgoOpens the running app in a browser and verifies that recent UI changes actually work. Use for any quick smoke test of recent work — "go", "test in browser", "check in browser", "make sure it works", "verify it works", "did it work", "works on mobile" — including when the user appends "...and make sure it works" to a UI request. For design critique, use go-ui or web-design-guidelines.
- AhandoffWrite or update a HANDOFF.md so a fresh agent can continue this work. Use when the user says "handoff", "compact this", "context is full", or "/clear and continue".
- Chetzner-cloudManage Hetzner Cloud infrastructure with the `hcloud` CLI — servers, networks, firewalls, load balancers, volumes, DNS zones, SSH keys, primary/floating IPs, snapshots, certificates, placement groups, storage boxes. Use whenever the user mentions Hetzner, hcloud, VPS provisioning, or Hetzner location codes (fsn1, hel1, nbg1, ash, hil, sin) — even if they don't say "hcloud". CLI-only; does NOT cover Hetzner Robot (dedicated servers, separate product and API).
- AiconsFind, fetch, and install the right icon or logo from the right source — brand marks, country flags, file-type icons (PDF, DOCX, ZIP), and UI glyphs — and keep them visually consistent with the app. Use when the project's icon library has no match, when svgl comes up empty, or when the user asks for a flag, a file-type badge, a brand logo, or just "an icon for X". Covers the Iconify search API (200k+ icons across flags, file types, logos and UI sets), the svgl shadcn registry for full-colour brand logos, family and stroke-weight matching so a borrowed icon does not look pasted in, and fallback sources when neither Iconify nor svgl has the mark. Triggers on "add an icon", "country flag", "flag icon", "PDF icon", "file type icon", "brand logo", "sign in with Google/GitHub", "language switcher", "svgl", "iconify", "find an icon". For overall visual direction rather than sourcing one specific mark, use frontend-design; for installing shadcn components generally, use shadcn.