Next.jsのSEO対策完全ガイド|メタタグ・OGP・構造化データの実装

kento_morota 26分で読めます

「Next.jsでWebサイトを構築したけれど、SEO対策が十分にできているか不安…」「メタタグやOGPの設定方法がわからない」という悩みを抱えていませんか?Next.jsはReactベースのフレームワークとして人気がありますが、SEO対策を正しく実装しなければ、せっかく作ったサイトが検索結果に表示されないことになりかねません。

本記事では、Next.js(App Router対応)でのSEO対策を網羅的に解説します。メタタグの設定からOGP(Open Graph Protocol)、構造化データ(JSON-LD)、サイトマップの自動生成まで、実際のコード例とともに実践的なテクニックをお伝えします。この記事を読めば、Next.jsプロジェクトで必要なSEO対策の全体像がつかめるでしょう。

Next.jsがSEOに強い理由とレンダリング戦略

Next.jsはSEOに強いフレームワークとして知られていますが、なぜそう言われるのでしょうか。まず、その技術的な背景を理解しておきましょう。

SSR・SSG・ISRの違いとSEOへの影響

Next.jsが提供する主要なレンダリング戦略は以下の3つです。

SSR(Server-Side Rendering)は、リクエストごとにサーバー側でHTMLを生成する方式です。常に最新のコンテンツをクローラーに提供できるため、頻繁に更新されるページに適しています。

SSG(Static Site Generation)は、ビルド時にHTMLを事前生成する方式です。CDNから配信されるため表示速度が速く、Core Web Vitalsのスコア改善に直結します。ブログやドキュメントサイトに最適です。

ISR(Incremental Static Regeneration)は、SSGの利点を活かしつつ、一定間隔でページを再生成できる方式です。ECサイトの商品ページなど、ある程度の更新頻度があるコンテンツに向いています。

// app/blog/[slug]/page.tsx - ISRの例
export const revalidate = 3600; // 1時間ごとに再生成

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

CSR(クライアントサイドレンダリング)のSEOリスク

一方で、use clientを多用してクライアントサイドでレンダリングするコンポーネントが多いと、クローラーがコンテンツを正しく認識できない場合があります。Googlebotはある程度JavaScriptを実行できますが、すべてのコンテンツがインデックスされる保証はありません。

SEOが重要なページでは、サーバーコンポーネント(Server Components)を基本とし、インタラクティブな部分だけをクライアントコンポーネントにする設計が推奨されます。

Metadata APIによるメタタグの最適化

Next.js 13以降のApp Routerでは、Metadata APIを使ってメタタグを宣言的に設定できます。これにより、ページごとに適切なタイトルやディスクリプションを設定しやすくなりました。

静的メタデータの設定

最もシンプルな方法は、layout.tsxpage.tsxでMetadataオブジェクトをエクスポートすることです。

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

export const metadata: Metadata = {
  title: {
    default: 'サイト名 | キャッチコピー',
    template: '%s | サイト名',
  },
  description: 'サイト全体のデフォルトディスクリプション。120文字程度で簡潔に。',
  metadataBase: new URL('https://example.com'),
  alternates: {
    canonical: '/',
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
};

title.templateを設定することで、子ページでタイトルを指定した際に自動的に「ページタイトル | サイト名」の形式になります。

動的メタデータの生成

ブログ記事や商品ページなど、データに基づいてメタタグを動的に生成する場合はgenerateMetadata関数を使います。

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

type Props = {
  params: { slug: string };
};

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

  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: `/blog/${params.slug}`,
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.ogImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage],
    },
  };
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

この方法なら、CMSや外部APIからデータを取得し、記事ごとに最適なメタタグを自動生成できます。

OGP(Open Graph Protocol)の完全実装

OGPは、SNSでURLがシェアされた際に表示されるタイトル・画像・説明文を制御するプロトコルです。適切に設定することで、SNSからの流入を大幅に増やせます。

OGP画像の要件と最適なサイズ

OGP画像(og:image)はSNSシェア時の印象を大きく左右します。以下のポイントを押さえましょう。

  • 推奨サイズ:1200×630ピクセル(アスペクト比1.91:1)
  • 最小サイズ:600×315ピクセル
  • ファイル形式:JPEGまたはPNG
  • ファイルサイズ:1MB以下を推奨
  • テキスト配置:中央80%の範囲内に収める(SNSによってトリミングされるため)

OGP画像の動的生成(next/og)

Next.jsではnext/og(旧@vercel/og)を使って、ページごとにOGP画像を動的に生成できます。

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title') ?? 'デフォルトタイトル';

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          width: '100%',
          height: '100%',
          backgroundColor: '#1a1a2e',
          color: '#ffffff',
          padding: '60px',
        }}
      >
        <div
          style={{
            fontSize: '52px',
            fontWeight: 'bold',
            textAlign: 'center',
            lineHeight: 1.4,
            maxWidth: '900px',
          }}
        >
          {title}
        </div>
        <div
          style={{
            marginTop: '40px',
            fontSize: '24px',
            color: '#a0a0b0',
          }}
        >
          example.com
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}

生成したOGP画像のURLをメタデータに組み込む方法は以下のとおりです。

// generateMetadata内で指定
openGraph: {
  images: [
    {
      url: `/api/og?title=${encodeURIComponent(post.title)}`,
      width: 1200,
      height: 630,
    },
  ],
},

Twitter Card(Xカード)の設定

X(旧Twitter)では独自のカードメタタグが使用されます。Next.jsのMetadata APIではtwitter プロパティで指定します。

twitter: {
  card: 'summary_large_image',  // または 'summary'
  site: '@your_account',
  creator: '@author_account',
  title: 'ページタイトル',
  description: 'ページの説明',
  images: ['https://example.com/og-image.jpg'],
},

summary_large_imageを指定すると大きな画像付きカードが表示され、視認性が向上します。summaryは小さなサムネイルが左側に表示される形式です。コンテンツの性質に応じて使い分けましょう。

構造化データ(JSON-LD)でリッチリザルトを獲得する

構造化データは、ページの内容を検索エンジンに機械的に伝えるための仕組みです。正しく実装すると、検索結果にリッチリザルト(パンくず、FAQ、レビュー星など)が表示され、CTR(クリック率)が大幅に向上します。

JSON-LDの基本と実装方法

Next.jsでJSON-LDを実装する最もシンプルな方法は、<script>タグを使う方法です。

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    image: post.ogImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      '@type': 'Person',
      name: post.author.name,
      url: post.author.url,
    },
    publisher: {
      '@type': 'Organization',
      name: 'サイト名',
      logo: {
        '@type': 'ImageObject',
        url: 'https://example.com/logo.png',
      },
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://example.com/blog/${params.slug}`,
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

FAQページの構造化データ

よくある質問ページでは、FAQPage スキーマを使うと検索結果にQ&Aが直接表示されます。

const faqJsonLd = {
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: [
    {
      '@type': 'Question',
      name: 'Next.jsとは何ですか?',
      acceptedAnswer: {
        '@type': 'Answer',
        text: 'Next.jsはReactベースのフルスタックWebフレームワークです。SSR、SSG、ISRなどの機能を提供します。',
      },
    },
    {
      '@type': 'Question',
      name: 'Next.jsはSEOに強いですか?',
      acceptedAnswer: {
        '@type': 'Answer',
        text: 'はい。サーバーサイドレンダリングにより検索エンジンがコンテンツを確実に取得でき、Metadata APIによるメタタグ管理も容易です。',
      },
    },
  ],
};

パンくずリストの構造化データ

パンくずリストの構造化データを追加すると、検索結果にサイト階層が表示されます。

const breadcrumbJsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: [
    {
      '@type': 'ListItem',
      position: 1,
      name: 'ホーム',
      item: 'https://example.com',
    },
    {
      '@type': 'ListItem',
      position: 2,
      name: 'ブログ',
      item: 'https://example.com/blog',
    },
    {
      '@type': 'ListItem',
      position: 3,
      name: post.title,
      item: `https://example.com/blog/${params.slug}`,
    },
  ],
};

実装後はGoogleリッチリザルトテストで正しく認識されるか確認しましょう。

サイトマップとrobots.txtの自動生成

サイトマップは検索エンジンにページ一覧を伝え、クロール効率を向上させる重要なファイルです。Next.js App Routerでは、TypeScriptで動的に生成できます。

サイトマップの自動生成

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

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

  // 静的ページ
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1.0,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: `${baseUrl}/contact`,
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 0.5,
    },
  ];

  // 動的ページ(ブログ記事)
  const posts = await getAllPosts();
  const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  return [...staticPages, ...blogPages];
}

このファイルを配置するだけで、/sitemap.xmlにアクセスするとXML形式のサイトマップが返されます。

robots.txtの設定

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/_next/'],
      },
    ],
    sitemap: 'https://example.com/sitemap.xml',
  };
}

不要なページ(管理画面、APIエンドポイントなど)をクロール対象から除外することで、クロールバジェットの無駄遣いを防げます。

Core Web Vitalsの最適化テクニック

GoogleはCore Web Vitalsをランキング要因に含めています。Next.jsには表示速度を最適化するための機能が豊富に備わっています。

next/imageによる画像最適化

next/imageコンポーネントを使えば、自動的に画像の最適化が行われます。

import Image from 'next/image';

export default function HeroSection() {
  return (
    <section>
      <Image
        src="/hero-image.jpg"
        alt="ヒーロー画像の説明"
        width={1200}
        height={600}
        priority  // LCPに関わる画像にはpriorityを指定
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
      />
    </section>
  );
}

主なポイントは以下の通りです。

  • priority属性:ファーストビューに表示される画像に付与し、LCP(Largest Contentful Paint)を改善
  • sizes属性:ビューポートに応じて適切なサイズの画像を配信
  • placeholder="blur":画像読み込み中にぼかしプレビューを表示し、CLS(Cumulative Layout Shift)を防止

next/fontによるフォント最適化

Webフォントの読み込みはCLSの原因になりがちです。next/fontを使えば、フォントの最適化が自動的に行われます。

// app/layout.tsx
import { Noto_Sans_JP } from 'next/font/google';

const notoSansJP = Noto_Sans_JP({
  subsets: ['latin'],
  weight: ['400', '700'],
  display: 'swap',
  preload: true,
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja" className={notoSansJP.className}>
      <body>{children}</body>
    </html>
  );
}

display: 'swap'を指定することで、フォント読み込み中はフォールバックフォントで表示し、読み込み完了後に切り替えます。これによりFCPが改善されます。

動的インポートによるバンドルサイズ削減

import dynamic from 'next/dynamic';

// 重いコンポーネントを遅延読み込み
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>グラフを読み込み中...</p>,
  ssr: false, // クライアントでのみレンダリング
});

export default function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <HeavyChart />
    </div>
  );
}

ファーストビューに不要なコンポーネントを遅延読み込みにすることで、初期バンドルサイズを削減し、TTI(Time to Interactive)を改善できます。

canonicalタグとリダイレクトの設定

重複コンテンツは検索順位を下げる大きな原因です。canonical タグとリダイレクトを正しく設定しましょう。

canonicalタグの設定

// 各ページのgenerateMetadataで設定
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return {
    alternates: {
      canonical: `/blog/${params.slug}`,
      // 多言語対応の場合
      languages: {
        'ja': `/ja/blog/${params.slug}`,
        'en': `/en/blog/${params.slug}`,
      },
    },
  };
}

リダイレクトの設定

URLの変更やページの統合時は、next.config.jsでリダイレクトを設定します。

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true, // 301リダイレクト(SEO評価を引き継ぐ)
      },
      {
        source: '/posts/:path*',
        destination: '/blog/:path*',
        permanent: true,
      },
    ];
  },
};

permanent: trueは301リダイレクトを意味し、旧URLのSEO評価を新URLに引き継ぎます。一時的な変更の場合はpermanent: false(302リダイレクト)を使用してください。

末尾スラッシュの統一

/about/about/の両方でアクセスできると重複コンテンツとみなされる場合があります。

// next.config.js
module.exports = {
  trailingSlash: false, // 末尾スラッシュなしに統一
};

SEO対策の検証とモニタリング

実装したSEO対策が正しく機能しているか、継続的に検証・モニタリングすることが重要です。

開発時のSEOチェックリスト

以下のチェックリストを開発フローに組み込みましょう。

  • タイトルタグ:30〜60文字以内で、ターゲットキーワードを含む
  • メタディスクリプション:120〜160文字以内で、ページの内容を要約
  • 見出し階層:h1は1ページ1つ、h2→h3と階層構造を正しく
  • OGP画像:1200×630pxで設定されている
  • 構造化データ:リッチリザルトテストでエラーなし
  • canonical:正しいURLが設定されている
  • 画像のalt属性:すべての画像に適切なalt属性がある

Lighthouse CIによる自動チェック

CI/CDパイプラインにLighthouse CIを組み込むことで、デプロイ前にパフォーマンスとSEOスコアを自動チェックできます。

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000/', 'http://localhost:3000/blog'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['warn', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.95 }],
        'categories:accessibility': ['warn', { minScore: 0.9 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};
# GitHub Actionsワークフロー例
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - run: npx @lhci/cli@0.13.x autorun

Google Search Consoleとの連携

Google Search Consoleでの確認ポイントは以下の通りです。

  • インデックス状況:「ページ」レポートで、意図したページが正しくインデックスされているか
  • Core Web Vitals:「ウェブに関する主な指標」で、LCP・FID・CLSの状況
  • 構造化データ:「拡張」レポートで、エラーがないか
  • モバイルユーザビリティ:モバイルフレンドリーに問題がないか

サイトマップの送信も忘れずに行いましょう。

// サイトマップURLをSearch Consoleに登録
// https://example.com/sitemap.xml

まとめ:Next.jsのSEO対策チェックリスト

本記事で解説したNext.jsのSEO対策を整理すると、以下のようになります。

基本設定

  • Metadata APIによるメタタグの設定(タイトル、ディスクリプション、robots)
  • canonicalタグによるURL正規化
  • サイトマップとrobots.txtの自動生成

SNS対策

  • OGP(Open Graph Protocol)の設定
  • Twitter Cardの設定
  • OGP画像の動的生成

リッチリザルト

  • JSON-LDによる構造化データ(Article、FAQPage、BreadcrumbList)

パフォーマンス

  • next/imageによる画像最適化
  • next/fontによるフォント最適化
  • 動的インポートによるバンドルサイズ削減

モニタリング

  • Lighthouse CIによる自動チェック
  • Google Search Consoleでのインデックス確認

Next.jsのSEO対策は、一度設定すれば終わりではありません。検索エンジンのアルゴリズムは常にアップデートされるため、定期的なモニタリングと改善が不可欠です。まずは本記事のコードを参考に基本設定を整え、Google Search Consoleのデータを見ながら継続的に改善していきましょう。

#Next.js#SEO#OGP
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう