/SKILL.md
Comprehensive Next.js SEO optimization skill for metadata, sitemaps, robots.txt, structured data, and Core Web Vitals. Use when building or auditing Next.js applications for SEO, implementing metadata API, generating sitemaps, adding Open Graph tags, creating JSON-LD structured data, optimizing for search engines, or improving search rankings. Works with Next.js 13+ App Router and Pages Router.
npx skillsauth add jasonroy19357/nextjs-seo-optimizer nextjs-seo-optimizerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Optimize Next.js applications for search engines with proper metadata, sitemaps, robots.txt, and structured data.
Follow this order for maximum impact:
Use for pages with unchanging content (home, about, contact):
// app/layout.tsx or app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'Your Site Name',
template: '%s | Your Site Name' // Pages will be "Page Title | Your Site Name"
},
description: 'Compelling 150-160 character description with keywords',
keywords: ['keyword1', 'keyword2', 'keyword3'],
authors: [{ name: 'Author Name', url: 'https://example.com' }],
creator: 'Company Name',
publisher: 'Company Name',
metadataBase: new URL('https://yourdomain.com'),
alternates: {
canonical: '/',
},
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yourdomain.com',
title: 'Your Site Name',
description: 'Compelling description for social sharing',
siteName: 'Your Site Name',
images: [{
url: '/og-image.jpg', // 1200x630px recommended
width: 1200,
height: 630,
alt: 'Image description',
}],
},
twitter: {
card: 'summary_large_image',
title: 'Your Site Name',
description: 'Compelling description',
creator: '@yourtwitterhandle',
images: ['/twitter-image.jpg'], // 1200x628px
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'verification-code',
yandex: 'verification-code',
yahoo: 'verification-code',
},
}
Use for blog posts, products, dynamic pages:
// app/blog/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// Fetch data
const post = await fetchBlogPost(params.slug)
// Optionally access and extend parent metadata
const previousImages = (await parent).openGraph?.images || []
const previousKeywords = (await parent).keywords || []
return {
title: post.title, // Will use template from layout
description: post.excerpt.substring(0, 160),
keywords: [...previousKeywords, ...post.tags],
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [post.coverImage, ...previousImages],
url: `https://yourdomain.com/blog/${params.slug}`,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://yourdomain.com/blog/${params.slug}`,
},
}
}
export default function Page({ params }: Props) {
// Page content
}
Title Optimization:
%s | Brand NameDescription Optimization:
Image Requirements:
For sites with predictable static routes:
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://yourdomain.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://yourdomain.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://yourdomain.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}
For sites with dynamic content from CMS or database:
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default async function sitemap(): MetadataRoute.Sitemap {
// Fetch dynamic routes
const posts = await fetch('https://api.example.com/posts').then((res) => res.json())
const products = await fetch('https://api.example.com/products').then((res) => res.json())
const postEntries: MetadataRoute.Sitemap = posts.map((post: any) => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7,
}))
const productEntries: MetadataRoute.Sitemap = products.map((product: any) => ({
url: `https://yourdomain.com/products/${product.slug}`,
lastModified: new Date(product.updatedAt),
changeFrequency: 'daily',
priority: 0.9,
}))
return [
{
url: 'https://yourdomain.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
...postEntries,
...productEntries,
]
}
See references/next-sitemap-guide.md for detailed setup with automatic generation.
Priority Guidelines:
Change Frequency Guidelines:
always: Stock prices, real-time datahourly: News sites, frequently updated contentdaily: Blogs, active e-commerceweekly: Standard blog postsmonthly: Feature pagesyearly: Static pages (about, contact)never: Archived content// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/private/'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
}
}
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://yourdomain.com'
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/_next/', '/private/'],
crawlDelay: 1,
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/admin/', '/private/'],
},
{
userAgent: 'GPTBot', // Block AI scrapers if desired
disallow: ['/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
}
}
Common Paths to Disallow:
/admin/ - Admin panels/api/ - API endpoints (unless public)/_next/ - Next.js internal files/private/ - Private/restricted content/search?* - Search result pages (prevent duplicate content)/cart/ - Shopping cart pages/checkout/ - Checkout pages*?* - All pages with query parameters (optional, use carefully)Environment-Specific Rules:
const isDevelopment = process.env.NODE_ENV === 'development'
const isStaging = process.env.VERCEL_ENV === 'preview'
if (isDevelopment || isStaging) {
return {
rules: {
userAgent: '*',
disallow: '/', // Block all indexing in dev/staging
},
}
}
Add structured data for rich search results. Always test at https://search.google.com/test/rich-results
// app/page.tsx or components/StructuredData.tsx
export default function HomePage() {
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Your Company Name',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
description: 'Company description',
address: {
'@type': 'PostalAddress',
streetAddress: '123 Main St',
addressLocality: 'City',
addressRegion: 'State',
postalCode: '12345',
addressCountry: 'US',
},
contactPoint: {
'@type': 'ContactPoint',
telephone: '+1-234-567-8900',
contactType: 'Customer Service',
},
sameAs: [
'https://twitter.com/yourcompany',
'https://facebook.com/yourcompany',
'https://linkedin.com/company/yourcompany',
],
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
{/* Page content */}
</>
)
}
export default function BlogPost({ post }: { post: Post }) {
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
author: {
'@type': 'Person',
name: post.author.name,
url: `https://yourdomain.com/authors/${post.author.slug}`,
},
publisher: {
'@type': 'Organization',
name: 'Your Company',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
{/* Article content */}
</>
)
}
const productSchema = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.images,
description: product.description,
sku: product.sku,
brand: {
'@type': 'Brand',
name: product.brand,
},
offers: {
'@type': 'Offer',
url: `https://yourdomain.com/products/${product.slug}`,
priceCurrency: 'USD',
price: product.price,
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
seller: {
'@type': 'Organization',
name: 'Your Company',
},
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.rating,
reviewCount: product.reviewCount,
},
}
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
}
const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbs.map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: `https://yourdomain.com${crumb.path}`,
})),
}
These directly impact search rankings. See references/performance-optimization.md for detailed strategies.
Quick Wins:
<Image> component (automatic optimization)experimental.optimizePackageImports in next.config.jsconst HeavyComponent = dynamic(() => import('./HeavyComponent'))'use client')When auditing or implementing SEO, check:
Metadata:
Technical SEO:
Structured Data:
Performance:
Content:
Issue: Duplicate metadata
Issue: Poor Core Web Vitals
Issue: Missing structured data
Issue: Incorrect robots.txt blocking important pages
Issue: Sitemap not updating
Use these tools to validate SEO implementation:
Google Tools:
Third-Party Tools:
Testing Commands:
# Test metadata locally
curl -I http://localhost:3000/page-to-test
# View sitemap
curl http://localhost:3000/sitemap.xml
# View robots.txt
curl http://localhost:3000/robots.txt
# Run Lighthouse audit
npx lighthouse http://localhost:3000 --view
For new projects:
For existing projects:
This skill supports:
# Validate sitemap locally
curl http://localhost:3000/sitemap.xml | head -n 20
# Test robots.txt
curl http://localhost:3000/robots.txt
# Run SEO audit
npm run build && npm start
# Then in browser: DevTools > Lighthouse > SEO audit
# Test metadata extraction
curl -s http://localhost:3000 | grep -E '<title>|<meta'
Set these in .env.local:
NEXT_PUBLIC_BASE_URL=https://yourdomain.com
NEXT_PUBLIC_SITE_NAME="Your Site Name"
NEXT_PUBLIC_OG_IMAGE=https://yourdomain.com/og-image.jpg
GOOGLE_VERIFICATION_CODE=your-verification-code
For specific scenarios and advanced patterns:
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.