スキル一覧に戻る
Naw3

seo-next

by Naw3

0🍴 0📅 2026年1月16日
GitHubで見るManusで実行

SKILL.md


name: seo-next description: | SEO patterns for Next.js App Router including metadata, Open Graph, structured data, sitemaps, and Core Web Vitals optimization. Use when implementing SEO features or optimizing for search engines.

SEO Patterns for Next.js

Comprehensive SEO implementation patterns for Next.js App Router.

Metadata API

Static Metadata

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'My App',
    template: '%s | My App', // "Page Title | My App"
  },
  description: 'The best app for doing things',
  keywords: ['nextjs', 'react', 'seo'],
  authors: [{ name: 'Your Name', url: 'https://yoursite.com' }],
  creator: 'Your Company',
  publisher: 'Your Company',
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://example.com',
    siteName: 'My App',
    title: 'My App - Do Amazing Things',
    description: 'The best app for doing things',
    images: [
      {
        url: '/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'My App',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'My App',
    description: 'The best app for doing things',
    creator: '@yourhandle',
    images: ['/twitter-image.jpg'],
  },
  verification: {
    google: 'google-verification-code',
    yandex: 'yandex-verification-code',
  },
  alternates: {
    canonical: 'https://example.com',
    languages: {
      'en-US': 'https://example.com/en-US',
      'fr-FR': 'https://example.com/fr-FR',
    },
  },
}

Dynamic Metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

type Props = {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  if (!post) {
    return { title: 'Not Found' }
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

Dynamic OG Images

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const alt = 'Blog Post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'flex-start',
          justifyContent: 'center',
          width: '100%',
          height: '100%',
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          padding: 60,
        }}
      >
        <div
          style={{
            fontSize: 60,
            fontWeight: 'bold',
            color: 'white',
            marginBottom: 20,
            lineHeight: 1.2,
          }}
        >
          {post.title}
        </div>
        <div
          style={{
            fontSize: 30,
            color: 'rgba(255,255,255,0.8)',
          }}
        >
          {post.author.name} • {post.readTime} min read
        </div>
      </div>
    ),
    { ...size }
  )
}

Structured Data (JSON-LD)

// components/JsonLd.tsx
type JsonLdProps = {
  data: Record<string, unknown>
}

export function JsonLd({ data }: JsonLdProps) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  )
}

// Usage in page
import { JsonLd } from '@/components/JsonLd'

export default function ProductPage({ product }) {
  const structuredData = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images,
    sku: product.sku,
    brand: {
      '@type': 'Brand',
      name: product.brand,
    },
    offers: {
      '@type': 'Offer',
      url: `https://example.com/products/${product.slug}`,
      priceCurrency: 'USD',
      price: product.price,
      availability: product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
    },
    aggregateRating: product.rating && {
      '@type': 'AggregateRating',
      ratingValue: product.rating,
      reviewCount: product.reviewCount,
    },
  }

  return (
    <>
      <JsonLd data={structuredData} />
      <ProductContent product={product} />
    </>
  )
}

Common Schema Types

// Article
const articleSchema = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: 'Article Title',
  description: 'Article description',
  image: 'https://example.com/image.jpg',
  datePublished: '2024-01-15',
  dateModified: '2024-01-16',
  author: {
    '@type': 'Person',
    name: 'Author Name',
    url: 'https://example.com/author',
  },
  publisher: {
    '@type': 'Organization',
    name: 'Company Name',
    logo: {
      '@type': 'ImageObject',
      url: 'https://example.com/logo.png',
    },
  },
}

// Organization
const orgSchema = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'Company Name',
  url: 'https://example.com',
  logo: 'https://example.com/logo.png',
  sameAs: [
    'https://twitter.com/company',
    'https://linkedin.com/company/company',
  ],
  contactPoint: {
    '@type': 'ContactPoint',
    telephone: '+1-555-555-5555',
    contactType: 'customer service',
  },
}

// BreadcrumbList
const breadcrumbSchema = {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: [
    {
      '@type': 'ListItem',
      position: 1,
      name: 'Home',
      item: 'https://example.com',
    },
    {
      '@type': 'ListItem',
      position: 2,
      name: 'Products',
      item: 'https://example.com/products',
    },
    {
      '@type': 'ListItem',
      position: 3,
      name: 'Product Name',
    },
  ],
}

// FAQ
const faqSchema = {
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: [
    {
      '@type': 'Question',
      name: 'What is this product?',
      acceptedAnswer: {
        '@type': 'Answer',
        text: 'This product is...',
      },
    },
  ],
}

Sitemap

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://example.com'

  // Static pages
  const staticPages = [
    '',
    '/about',
    '/contact',
    '/pricing',
  ].map(route => ({
    url: `${baseUrl}${route}`,
    lastModified: new Date(),
    changeFrequency: 'monthly' as const,
    priority: route === '' ? 1 : 0.8,
  }))

  // Dynamic pages from database
  const posts = await db.post.findMany({
    select: { slug: true, updatedAt: true },
  })

  const postPages = posts.map(post => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.6,
  }))

  return [...staticPages, ...postPages]
}

Multiple Sitemaps (Large Sites)

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export async function generateSitemaps() {
  const totalProducts = await db.product.count()
  const sitemapsNeeded = Math.ceil(totalProducts / 50000)
  
  return Array.from({ length: sitemapsNeeded }, (_, i) => ({ id: i }))
}

export default async function sitemap({
  id,
}: {
  id: number
}): Promise<MetadataRoute.Sitemap> {
  const start = id * 50000
  const products = await db.product.findMany({
    skip: start,
    take: 50000,
    select: { slug: true, updatedAt: true },
  })

  return products.map(product => ({
    url: `https://example.com/products/${product.slug}`,
    lastModified: product.updatedAt,
  }))
}

Robots.txt

// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  const baseUrl = 'https://example.com'

  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/private/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
  }
}

Canonical URLs

// lib/seo/canonical.ts
export function getCanonicalUrl(path: string): string {
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com'
  
  // Remove trailing slash
  const cleanPath = path.replace(/\/$/, '')
  
  // Remove query params for canonical
  const pathWithoutQuery = cleanPath.split('?')[0]
  
  return `${baseUrl}${pathWithoutQuery}`
}

// Usage in generateMetadata
export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    alternates: {
      canonical: getCanonicalUrl(`/blog/${params.slug}`),
    },
  }
}

Viewport Configuration

// app/layout.tsx
import type { Viewport } from 'next'

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 5,
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
    { media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
  ],
}

Performance SEO

Font Optimization

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevents FOIT
  preload: true,
})

Image Optimization

import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Descriptive alt text for SEO"
  width={1200}
  height={600}
  priority // LCP image
  sizes="(max-width: 768px) 100vw, 1200px"
/>

Core Web Vitals

// Measure and report CWV
export function reportWebVitals(metric: NextWebVitalsMetric) {
  if (metric.label === 'web-vital') {
    // Send to analytics
    analytics.track('Web Vital', {
      name: metric.name,    // CLS, FID, FCP, LCP, TTFB
      value: metric.value,
      rating: metric.rating, // good, needs-improvement, poor
    })
  }
}

SEO Checklist

  • Unique title and description per page
  • Open Graph and Twitter cards configured
  • Structured data for relevant content types
  • Sitemap generated and submitted
  • Robots.txt configured
  • Canonical URLs set
  • Images optimized with alt text
  • Core Web Vitals passing
  • Mobile-friendly design
  • HTTPS enabled

スコア

総合スコア

55/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

0/5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です