言語を切り替える
テーマを切り替える

Next.js SEO 完全ガイド:Metadata API + 構造化データ実践

午前3時、私は Google Search Console の統計画面を見つめていました。画面上の大きな「0」という数字が、パソコンを叩き壊したくなるほど目に刺さります。

製品をリリースしてから23日目。2ヶ月の開発期間、数え切れない徹夜、丁寧に磨き上げたUI、スムーズなユーザー体験――それらすべてが、この冷酷な「0」の前では何の意味も持たないように思えました。さらに腹立たしいのは、Twitter にリンクを共有した時、プレビューエリアが真っ白だったことです。まともなカバー画像すら出ません。

「Next.js って SSR が標準じゃないの? なんで SEO がこんなに悪いの?」 公式ドキュメントを読み漁って、ようやく残酷な事実に気づきました。「SSR = SEO フレンドリー」ではないのです。meta タグを間違えたり、構造化データを設定しなかったり、Open Graph プロトコルを知らなければ、検索エンジンにとってあなたのサイトは空気と同じです。

この痛み、あなたも感じたことがあるはずです。苦労して作った製品が検索されず、共有してもプロっぽく見えず、広告費を燃やして宣伝するしかない状況。でも実は、Next.js 15 の Metadata API といくつかの重要な設定さえ押さえれば、これらの問題はすべて解決できます。

この記事では、手とり足とり教えます。Metadata API を使って各ページに独自の meta タグを設定する方法、検索結果で目立つための構造化データの設定方法、完璧なソーシャルシェアプレビューを実現する方法。さらに重要なこととして、私が踏んだ地雷を避けるための「よくある SEO の5つの落とし穴」もお伝えします。

なぜあなたの Next.js サイトの SEO はダメなのか?

SSR ≠ SEO フレンドリー

正直に言うと、私も最初はそう思っていました。Next.js を使い、サーバーサイドレンダリング(SSR)され、HTML がクローラーに直接渡されるのだから、SEO は完璧なはずだと。

甘かったです。

後に友人のプロジェクトのソースコードを見て、ブラウザの開発ツールで HTML を確認した時、すべてのページの <title> が “My App” で、<meta name="description"> は存在しないか、すべて同じ内容になっているのを見つけました。これは、ブティックを開いたのに、看板に永遠に「店舗」としか書かれていないようなものです。客は何を売っているのか全く分かりません。

Next.js は確かに SSR 機能を提供しますが、meta タグの設定は完全にあなたの責任です。設定しなければ、ただの空っぽの HTML であり、検索エンジンはあなたのページが何について書かれているのか理解できません。

5つの致命的な SEO の間違い

私も含め、多くの開発者が踏んでしまう落とし穴があります:

1. 全ページで同じ title と description を共有している

最もありがちなミスです。_document.tsx に title をハードコードするか、そもそも書いていないかです。結果として、Google があなたのホームページ、アバウトページ、製品ページを検索しても、表示されるタイトルと説明はすべて同じになります。

想像してください。本屋で本を見ている時、すべての本の表紙に同じタイトルが印刷されていたら? あなたは買いますか?

2. canonical URL の設定忘れによる重複コンテンツ

これは非常に気づきにくい問題です。サイトにはページネーション(?page=2)、フィルタ(?category=tech)、ソート(?sort=date)などの URL パラメータがあるかもしれません。これらは実際には同じページの異なるビューですが、検索エンジンはこれらを「異なるページ」と見なし、「重複コンテンツ」としてペナルティを与える可能性があります。

私のクライアントのブログはこれが原因でトラフィックが40%落ちました。canonical URL を追加した後、2週間で元に戻りました。

3. 構造化データがなく、リッチリザルトを逃している

「アップルパイ レシピ」で検索した時、一部の結果に評価(4.8星)、調理時間(45分)、カロリー(320kcal)が直接表示されているのに気づいたことはありますか? あれは Google が推測したのではなく、ウェブサイトが構造化データ(JSON-LD)を通じて Google に能動的に伝えているのです。

研究によると、構造化データを実装したサイトは、クリック率が平均 20-30% 向上します。これはどういうことかというと、数行のコードを追加するだけで、トラフィックが3割増えるということです。

4. 画像に alt 属性がなく、画像検索のトラフィックを無駄にしている

多くの開発者は alt 属性を視覚障害者のためのもので、SEO には関係ないと考えています。間違いです。

Google 画像検索は巨大なトラフィックの入り口です。画像に明確な alt 説明があれば、画像検索でランクインするチャンスがあります。ある素材サイトでは、トラフィックの30%が Google 画像検索から来ていました。彼らがすべての画像の alt を真剣に書いていたからです。

5. sitemap.xml と robots.txt がない

この2つのファイルは、検索エンジンに「私のサイトにはどのページがあり、どれをクロールしていいか」を伝えるものです。sitemap がないと、Google が新しい記事を見つけるのに数ヶ月かかるかもしれません。sitemap があり、Search Console に送信すれば、新しいページは数日でインデックスされます。

しかも Next.js App Router は現在、sitemap.tsrobots.ts の自動生成をサポートしています。手書きする必要すらないのに、設定しない理由はありません。

Metadata API 完全マスター(Next.js 15)

Next.js 15 の Metadata API は、このフレームワークが SEO に贈る最大のプレゼントです。以前は各ページで <Head> を手書きする必要がありましたが、今はオブジェクトや関数をエクスポートするだけで、Next.js が自動処理してくれます。

静的 metadata:内容が固定のページ向け

最も単純なシナリオ:「会社概要」や「プライバシーポリシー」など、内容が基本的に変わらないページです。page.tsxmetadata オブジェクトを直接エクスポートします:

// app/about/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: '私たちについて - TechBlog',
  description: '私たちは技術を愛する開発者集団です。フロントエンド、バックエンド、DevOps の実戦経験をシェアします。',
  keywords: ['技術ブログ', 'フロントエンド開発', 'Next.js', 'React'],
  authors: [{ name: '田中太郎' }],
  openGraph: {
    title: '私たちについて - TechBlog',
    description: '技術ブログ、開発実戦経験をシェア',
    url: 'https://yourdomain.com/about',
    siteName: 'TechBlog',
    images: [
      {
        url: 'https://yourdomain.com/og-about.jpg',
        width: 1200,
        height: 630,
      }
    ],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: '私たちについて - TechBlog',
    description: '技術ブログ、開発実戦経験をシェア',
    images: ['https://yourdomain.com/og-about.jpg'],
  },
}

export default function AboutPage() {
  return <div>私たちについてのコンテンツ...</div>
}

見てください。型安全で、IDE の自動補完が効き、スペルミスの心配もありません。さらに Next.js は自動的に重複排除を行います。複数の場所で同じ meta タグが定義されていても、賢くマージしてくれます。

ベストプラクティス

  • title60文字以内(全角30文字程度)に抑える。超えると検索結果で省略されます。
  • description150-160文字(全角75-80文字程度)。これは Google 検索結果のスニペットの理想的な長さです。
  • openGraph.images のサイズは 1200x630 ピクセル を推奨。これは Twitter、Facebook、LinkedIn で共通して使えるサイズです。

動的 metadata:ブログ、製品ページの救世主

本当に強力なのは generateMetadata 関数です。例えばブログ記事ページなど、記事ごとにタイトルや説明が異なる場合、手書きするわけにはいきませんよね?

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { getPostBySlug } from '@/lib/posts'

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  // データベースや CMS から記事データを取得
  const post = await getPostBySlug(params.slug)

  return {
    title: `${post.title} - TechBlog`,
    description: post.excerpt,
    authors: [{ name: post.author }],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)
  return <article>{post.content}</article>
}

これで各記事に独自の SEO 情報がつきました。Google がクロールする際、クライアントサイド JavaScript なしで完全な HTML を見ることができます。

重要なテクニック:metadataBase

上記のコードで、画像 URL が絶対パスになっていることに気づきましたか? もし画像パスが相対パス(例:/images/cover.jpg)の場合、root layout で metadataBase を設定する必要があります:

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),
}

これを設定すると、すべての相対パスが自動的に完全な URL に結合されます。そうしないと Open Graph のクロールが失敗し、ソーシャルシェアで画像が表示されません。

テンプレートシステム:全ページのタイトル形式を統一

多くのサイトで、ページタイトルが「ページ名 | サイト名」という形式になっているのに気づいたことはありますか? 例えば「ホーム | TechBlog」、「会社概要 | TechBlog」などです。

各ページでこのサフィックスを手書きするのは面倒です。title.template を使えば解決します:

// app/layout.tsx (root layout)
export const metadata: Metadata = {
  title: {
    template: '%s | TechBlog',
    default: 'TechBlog - 技術ブログ',
  },
  description: '技術ブログ、フロントエンド、バックエンド、DevOpsの実戦経験をシェア',
  metadataBase: new URL('https://yourdomain.com'),
}

子ページではページ名だけを書けばOKです:

// app/about/page.tsx
export const metadata: Metadata = {
  title: '会社概要', // 最終的に "会社概要 | TechBlog" にレンダリングされる
}

ホーム画面でサフィックスを付けたくない場合は、title.absolute を使います:

// app/page.tsx
export const metadata: Metadata = {
  title: {
    absolute: 'TechBlog - 技術ブログホーム', // template は適用されない
  },
}

この仕組みにより、タイトルの一貫性を保つことができ、変更も非常に簡単です。root layout でテンプレートを一度修正すれば、全ページに自動反映されます。

構造化データ(Schema.org)で際立つ

構造化データとは? なぜ重要なのか?

「アップルパイの作り方」を検索したことはありますか?

検索結果をよく見てください。一部の結果には評価(4.8星)、調理時間(45分)、カロリー(320kcal)、さらには手順のプレビューまで表示されています。一方、ただのタイトルと概要しかないものもあります。

この違いを生むのが、構造化データです。

構造化データとは、検索エンジンに「これはブログ記事で、著者はXXX、公開日はXXXです」とか「これは製品で、価格はXXX、評価はXXXです」と伝えるための標準フォーマット(JSON-LD)です。検索エンジンはこの情報を受け取り、検索結果でリッチリザルト(リッチスニペット)――つまり評価や価格、著者情報などがついたカード――を表示します。

データは嘘をつきません。構造化データを実装したサイトは、クリック率が平均 20-30% 向上します。コストをかけずにトラフィックを3割増やせるのです。

よく使う Schema タイプ

Schema.org には何百ものタイプが定義されていますが、ほとんどのサイトでよく使うのは以下の数種類です:

  1. Organization - 会社/組織情報(ホームページに配置)
  2. BlogPosting - ブログ記事(各記事に追加)
  3. Product - 製品情報(ECサイト必須、価格、評価、在庫を含む)
  4. FAQPage - よくある質問(FAQページ、検索結果で直接展開可能)
  5. LocalBusiness - ローカルビジネス(レストラン、美容室など、住所、営業時間、電話番号を表示)

ブログや企業サイトで最も必要となる最初の2つに焦点を当てます。

Next.js での JSON-LD 実装

Next.js 15 の <Script> コンポーネントを使えば、構造化データの実装は非常に簡単です。私は通常、汎用コンポーネントを作成します:

// components/StructuredData.tsx
import Script from 'next/script'

type StructuredDataProps = {
  data: object
}

export default function StructuredData({ data }: StructuredDataProps) {
  return (
    <Script
      id="structured-data"
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  )
}

そしてページで使用します:

例1:Organization(会社情報)

// app/layout.tsx (root layout)
import StructuredData from '@/components/StructuredData'

const organizationData = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'TechBlog',
  url: 'https://yourdomain.com',
  logo: 'https://yourdomain.com/logo.png',
  sameAs: [
    'https://twitter.com/yourusername',
    'https://github.com/yourcompany',
    'https://linkedin.com/company/yourcompany',
  ],
  contactPoint: {
    '@type': 'ContactPoint',
    email: 'hello@yourdomain.com',
    contactType: 'Customer Service',
  },
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <StructuredData data={organizationData} />
      </body>
    </html>
  )
}

例2:BlogPosting(ブログ記事)

// app/blog/[slug]/page.tsx
import StructuredData from '@/components/StructuredData'
import { getPostBySlug } from '@/lib/posts'

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)

  const articleData = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    author: {
      '@type': 'Person',
      name: post.author,
      url: `https://yourdomain.com/author/${post.authorSlug}`,
    },
    publisher: {
      '@type': 'Organization',
      name: 'TechBlog',
      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 (
    <>
      <article>{post.content}</article>
      <StructuredData data={articleData} />
    </>
  )
}

コードが多く見えますが、要はあなたの記事のメタ情報を標準形式で Google に伝えるだけです。一度設定すれば、あとはコピー&ペーストで済みます。

構造化データの検証

設定が正しいかどうやって確認しますか? 以下の2つのツールを使います:

  1. Google リッチリザルト テストhttps://search.google.com/test/rich-results)

    • ページの URL を入力すると、どのアセットが表示可能か教えてくれます。
    • エラーがあれば、具体的な箇所を指摘してくれます。
  2. Schema Markup Validatorhttps://validator.schema.org/)

    • JSON-LD フォーマットが Schema.org 標準に準拠しているかチェックします。
    • Google のツールより厳格なので、両方でテストすることをお勧めします。

私がよく見る間違いは、publisher(BlogPosting では必須項目)を忘れることや、画像 URL が絶対パスでないことです。検証ツールが何が足りないか教えてくれるので、修正して再テストすればOKです。

Open Graph と Twitter Cards 実践

ソーシャルシェアはなぜ重要か?

あなたの記事が Twitter や Facebook でシェアされた時、プレビューエリアが空白だったり、画像が全く関係ないもの(サイトロゴやランダムな装飾など)だったりした経験はありませんか?

これは非常にプロ意識に欠ける印象を与えます。一方、正しく設定されたサイトは、リンクをシェアすると美しいカバー画像、タイトル、概要が自動的に表示され、クリック率は2-3倍になります。

Open Graph と Twitter Cards を設定するのは、ソーシャルメディアでの表示効果をコントロールするためです。

Open Graph プロトコル詳細

Open Graph は元々 Facebook が策定した標準ですが、現在は Twitter、LinkedIn、Slack、Discord など主要なプラットフォームすべてでサポートされています。

Metadata API の説明で openGraph オブジェクトの設定については触れましたが、ここでは核心となるフィールドを詳しく見ていきます:

export const metadata: Metadata = {
  openGraph: {
    // 必須フィールド
    title: '記事タイトル',              // ソーシャルメディアで表示されるタイトル
    description: '記事の概要',        // 概要、150文字程度
    url: 'https://yourdomain.com/article', // このコンテンツの URL
    siteName: 'TechBlog',          // サイト名

    // 画像(最重要!)
    images: [
      {
        url: 'https://yourdomain.com/og-image.jpg',
        width: 1200,
        height: 630,  // 推奨サイズ 1200x630
        alt: '画像の説明(アクセシビリティとSEO)',
      },
    ],

    // コンテンツタイプ
    type: 'article',  // 記事は article、その他は website

    // 記事固有フィールド(type が article の場合)
    publishedTime: '2025-01-15T08:00:00.000Z',
    modifiedTime: '2025-01-16T10:30:00.000Z',
    authors: ['田中太郎', '鈴木次郎'],
    tags: ['Next.js', 'SEO', 'フロントエンド開発'],

    // ローカライズ(多言語版がある場合)
    locale: 'ja_JP',
    alternateLocale: ['en_US', 'zh_CN'],
  },
}

重要ポイント:画像サイズ

1200x630 ピクセルは黄金比(1.91:1)で、全プラットフォームで完璧に表示されます:

  • Facebook、LinkedIn:フル表示
  • Twitter:2:1 にクロップされても違和感なし
  • Slack、Discord:同様に適用

画像サイズは 8MB を超えないようにしてください。ビルドが失敗する可能性があります。

Twitter Cards 設定

Twitter には独自の meta タグシステムがありますが、Open Graph にフォールバックすることも多いです。しかし、最適化のために個別に設定するのがベストです:

export const metadata: Metadata = {
  twitter: {
    card: 'summary_large_image',  // 大きな画像モード(推奨)
    site: '@yourusername',         // サイトの Twitter アカウント
    creator: '@authorusername',    // 著者の Twitter アカウント
    title: '記事タイトル',
    description: '記事概要',
    images: ['https://yourdomain.com/twitter-image.jpg'],
  },
}

card フィールドには2つの値があります:

  • summary:小画像モード(画像左、テキスト右)
  • summary_large_image:大画像モード(画像がカード上部全体を占める。こちらがおすすめ)

Twitter の画像制限:サイズは 5MB 以内(OG より厳しい)。

ソーシャルシェア画像の動的生成(上級)

記事ごとに 1200x630 のカバー画像を手動で作る? 死ぬほど面倒ですよね。

Next.js 13.3 以降、コードで OG 画像を動的生成できるようになりました。ブログに最適です:

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

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

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 60,
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
          padding: '80px',
        }}
      >
        <h1 style={{ fontSize: 72, fontWeight: 'bold', textAlign: 'center' }}>
          {post.title}
        </h1>
        <p style={{ fontSize: 36, marginTop: 20, opacity: 0.9 }}>
          by {post.author}
        </p>
      </div>
    ),
    {
      ...size,
    }
  )
}

これで、記事ごとにタイトルに基づいたシェア画像が自動生成されます! 手動デザインは不要です。

カスタムフォントや背景画像を使ってさらにカスタマイズしたい場合は、Next.js 公式ドキュメントの next/og セクションを参照してください。

シェア効果のテストと検証

設定したら、投稿する前に以下のツールでテストしましょう:

  1. Facebook Sharing Debuggerhttps://developers.facebook.com/tools/debug/)

    • URL を入力し、Facebook がどうクロールして表示するか確認。
    • 注意:Facebook は OG 情報をキャッシュします。コードを変えたら、このツールで「Scrape Again」をクリックしてキャッシュを更新してください。
  2. Twitter Card Validatorhttps://cards-dev.twitter.com/validator)

    • URL を入力して Twitter カードの効果をプレビュー。
    • 注:2023年以降、Twitter 開発者アカウントが必要ですが、Twitter で直接ツイートしてテストすることも可能です。
  3. LinkedIn Post Inspectorhttps://www.linkedin.com/post-inspector/)

    • LinkedIn のプレビューツール。こちらもキャッシュ更新が可能です。

私が踏んだ地雷:OG 画像を変更しても Facebook で古い画像が表示される場合、必ず Sharing Debugger でキャッシュをクリアしてください。そうしないと人生を疑うことになります。

その他 SEO 必須設定

Metadata API、構造化データ、Open Graph が SEO の核心ですが、見落としがちな重要設定が他にもあります。

sitemap.xml - 検索エンジンにページ一覧を伝える

Sitemap は、サイト内の全ページ URL をリストした XML ファイルです。Google や Bing のクローラーはこのファイルを読み、サイト構造を把握し、インデックス速度を上げます。

Next.js App Router では、これを app/sitemap.ts を作るだけで簡単に実現できます:

// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/posts'

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

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

  // 動的に生成されるブログ記事ページ
  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]
}

Next.js は https://yourdomain.com/sitemap.xml に自動的にファイルを生成します。

設定後の2つのTODO

  1. Google Search Console に送信(https://search.google.com/search-console)
  2. Bing Webmaster Tools に送信(https://www.bing.com/webmasters)

送信すると、新ページのインデックス速度が 50%以上向上 します。以前私がブログを書いていた時、sitemap を送信しないと新記事のインデックスに2週間かかりましたが、送信後は3日で Google に出てくるようになりました。

robots.txt - クローラーのアクセス権限管理

robots.txt は検索エンジンのクローラーに「どのページをクロールしてよくて、どれがダメか」を伝えます。

同様に、Next.js App Router はコード生成をサポートしています:

// 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',  // sitemap の場所
  }
}

これで https://yourdomain.com/robots.txt が生成されます。

内容の例:

User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /private

Sitemap: https://yourdomain.com/sitemap.xml

よくあるシナリオ

  • 管理画面(/admin)は検索されたくない
  • API ルート(/api)も不要
  • 下書き、プレビューページなどは noindex meta タグか Disallow ルールを使用

canonical URL - 重複コンテンツのペナルティ回避

Canonical URL は検索エンジンに「このページには複数の URL があるけど、これが正規版です」と伝えるものです。

典型的シーン:

  • ページネーション:/blog?page=1/blog?page=2
  • フィルタ:/products?category=tech
  • ソート:/products?sort=price

これらは実際には同じページの異なるビューですが、canonical を宣言しないと検索エンジンはこれらを「重複コンテンツ」と見なし、評価を分散させてしまいます。

Next.js での canonical 設定:

// app/blog/page.tsx
export const metadata: Metadata = {
  alternates: {
    canonical: 'https://yourdomain.com/blog',
  },
}

動的生成も可能:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `https://yourdomain.com/blog/${params.slug}`,
    },
  }
}

多言語サイトの場合、alternates.languages で言語バージョンを伝えることもできます:

export const metadata: Metadata = {
  alternates: {
    canonical: 'https://yourdomain.com/blog/nextjs-seo',
    languages: {
      'en-US': 'https://yourdomain.com/en/blog/nextjs-seo',
      'ja-JP': 'https://yourdomain.com/ja/blog/nextjs-seo',
    },
  },
}

画像最適化 - next/image + alt 属性

多くの開発者が画像の SEO を軽視していますが、Google 画像検索は巨大なトラフィック源です。

2つの重要ポイント

  1. <img> の代わりに next/image を使う
import Image from 'next/image'

<Image
  src="/cover.jpg"
  alt="Next.js SEO 完全ガイド表紙"
  width={1200}
  height={630}
  priority  // ファーストビュー画像にはこれを追加して優先ロード
/>

next/image は自動的に以下を行います:

  • 遅延読み込み(画面外画像のロード遅延)
  • WebP 形式変換(ファイルサイズ削減)
  • レスポンシブサイズ(デバイスに応じたサイズ読み込み)
  • CLS 防止(レイアウトシフトを防ぐ、Core Web Vitals 指標の一つ)
  1. alt 属性は必須

alt はオプションではありません。SEO 要件であり、アクセシビリティ要件(スクリーンリーダーが読み上げる)でもあります。

良い alt 説明

  • ✅ “Next.js Metadata API 設定コード例”
  • ✅ “Google 検索結果でのブログ記事リッチリザルト表示”

悪い alt 説明

  • ❌ “画像”
  • ❌ “screenshot.png”
  • ❌ alt なし

Google 画像検索は alt の内容に基づいて画像をランク付けします。私が知っているある素材サイトは、すべての画像の alt を真剣に書いたおかげで、トラフィックの30%を画像検索から得ています。

実戦ケース - ブログサイト SEO 完全設定

理論とコード断片ばかり話してきましたが、実際にブログプロジェクトでどう SEO を設定するか、全体像を見てみましょう。

プロジェクト構造

Next.js 15 App Router で技術ブログを作ると仮定します。ディレクトリ構造はこんな感じです:

app/
├── layout.tsx                 # Root layout - 全体設定
├── page.tsx                   # ホーム
├── about/page.tsx            # アバウトページ
├── blog/
│   ├── page.tsx              # ブログ一覧
│   └── [slug]/
│       ├── page.tsx          # ブログ詳細
│       └── opengraph-image.tsx  # OG画像動的生成(オプション)
├── sitemap.ts                # Sitemap 生成
└── robots.ts                 # Robots.txt 生成

完全なコード例

1. Root Layout - グローバル SEO 設定

// app/layout.tsx
import { Metadata } from 'next'
import StructuredData from '@/components/StructuredData'

export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),  // 必須設定、相対パスの結合に使用
  title: {
    template: '%s | TechBlog',  // 子ページのタイトルテンプレート
    default: 'TechBlog - フロントエンド開発技術ブログ',
  },
  description: 'Next.js、React、TypeScript の実戦経験をシェアする技術ブログ',
  keywords: ['Next.js', 'React', 'TypeScript', 'フロントエンド開発', '技術ブログ'],
  authors: [{ name: '田中太郎', url: 'https://yourdomain.com/about' }],
  openGraph: {
    type: 'website',
    siteName: 'TechBlog',
    locale: 'ja_JP',
  },
  twitter: {
    card: 'summary_large_image',
    site: '@yourusername',
  },
}

// Organization 構造化データ(グローバル、root layout に配置)
const organizationData = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'TechBlog',
  url: 'https://yourdomain.com',
  logo: 'https://yourdomain.com/logo.png',
  sameAs: [
    'https://twitter.com/yourusername',
    'https://github.com/yourcompany',
  ],
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        {children}
        <StructuredData data={organizationData} />
      </body>
    </html>
  )
}

2. ホーム - 静的 metadata

// app/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    absolute: 'TechBlog - フロントエンド開発技術ブログ',  // template を適用しない
  },
  description: 'Next.js、React、TypeScript などのフロントエンド技術の実戦経験をシェアし、開発者のスキルアップを支援します',
  openGraph: {
    title: 'TechBlog - フロントエンド開発技術ブログ',
    description: 'フロントエンド技術の実戦経験をシェア',
    url: 'https://yourdomain.com',
    images: [
      {
        url: 'https://yourdomain.com/og-home.jpg',
        width: 1200,
        height: 630,
        alt: 'TechBlog ホームカバー',
      },
    ],
  },
}

export default function HomePage() {
  return <div>ホームコンテンツ...</div>
}

3. ブログ詳細 - 動的 metadata + 構造化データ

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
import StructuredData from '@/components/StructuredData'

// metadata 動的生成
export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPostBySlug(params.slug)
  if (!post) return {}

  return {
    title: post.title,  // template が適用され "記事タイトル | TechBlog" になる
    description: post.excerpt,
    keywords: post.tags,
    authors: [{ name: post.author }],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://yourdomain.com/blog/${post.slug}`,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
    alternates: {
      canonical: `https://yourdomain.com/blog/${post.slug}`,
    },
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)
  if (!post) notFound()

  // BlogPosting 構造化データ
  const articleData = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt || post.publishedAt,
    author: {
      '@type': 'Person',
      name: post.author,
    },
    publisher: {
      '@type': 'Organization',
      name: 'TechBlog',
      logo: {
        '@type': 'ImageObject',
        url: 'https://yourdomain.com/logo.png',
      },
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://yourdomain.com/blog/${post.slug}`,
    },
  }

  return (
    <>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
      <StructuredData data={articleData} />
    </>
  )
}

4. Sitemap と Robots

(前述のコードと同じなので省略。忘れずに実装してください)

デプロイ後のチェックリスト

設定完了後、リリースの前に必ずチェックしてください:

  1. HTML ソースを確認

    • ブラウザで F12 → Elements → <head> タグを確認
    • <title><meta name="description">、OG タグが正しくレンダリングされているか
  2. sitemap と robots をテスト

    • https://yourdomain.com/sitemap.xml にアクセスして正常か確認
    • https://yourdomain.com/robots.txt の内容を確認
  3. 構造化データを検証

    • Google リッチリザルト テストで数ページを検証
    • エラーや警告がないか確認
  4. ソーシャルシェアをテスト

    • Facebook Sharing Debugger と Twitter Card Validator でテスト
    • 画像、タイトル、説明が正しく表示されるか
  5. 検索エンジンに送信

    • Google Search Console に sitemap を送信
    • Bing Webmaster Tools に sitemap を送信

これをやれば、Next.js サイトの SEO 設定は完璧です。

結論

ここまで真剣に読んでくれたなら、もうお分かりでしょう。Next.js の SSR は SEO フレンドリーと同義ではありませんが、Next.js 15 のツールを使えば SEO 設定は以前よりずっと簡単になっています

核心ポイントの振り返り:

  • Metadata API を使えば型安全に meta タグを設定できる。静的ページは metadata オブジェクト、動的ページは generateMetadata 関数を使う。
  • 構造化データ(JSON-LD) はクリック率を20-30%アップさせる秘密兵器。<Script> コンポーネントで簡単に実装できる。
  • Open Graph と Twitter Cards はソーシャルメディアでの見え方を決める。画像サイズ 1200x630 は万能。
  • sitemap.xml と robots.txt.ts ファイルで自動生成できる。Search Console への送信を忘れずに。
  • 画像最適化next/image + 真剣な alt 記述で。画像検索からもしっかり流入を得よう。

正直、これらの設定は面倒に見えますが、費用対効果は極めて高いです。技術力は高いのに、SEO が不十分でトラフィックが伸びず、広告費に頼る製品をたくさん見てきました。一方、正しく設定されたサイトは、自然検索流入が絶えず、コストはほぼゼロです。

SEO はオカルトではありません。科学です。この記事のリストに従って設定し、ツールで検証すれば、効果は必ず現れます――即効性はないかもしれませんが、3ヶ月後には明らかなトラフィックの増加が見られるはずです。

トラフィックが完全に消えるまで待ってから SEO を気にし出すのはやめましょう。今すぐプロジェクトを開いて、半日かけて設定してみてください。この記事はいつでも見返せるように保存しておいてください。

この記事が役に立ったら、ぜひ他の開発者の友人にもシェアしてください。彼らが落とし穴を避ける手助けになれば幸いです。

"},{"@type":"HowToStep","position":4,"name":"sitemap と robots.txt の設定","text":"sitemap.ts の作成:\n• default 関数をエクスポートし sitemap 配列を返す\n• 全ページの URL、lastModified、changeFrequency を含める\n• 動的生成をサポート\n\nrobots.ts の作成:\n• 許可/禁止するクローラーを設定\n• sitemap パスを設定\n• クロールルールを設定"},{"@type":"HowToStep","position":5,"name":"画像 SEO の最適化","text":"画像最適化のポイント:\n• next/image コンポーネントを使用\n• 意味のある alt テキストを追加\n• 画像サイズを設定(OG 画像には 1200x630 が最適)\n• WebP/AVIF フォーマットを使用\n• 画像構造化データ(ImageObject)を追加"},{"@type":"HowToStep","position":6,"name":"検証和テスト","text":"検証ツール:\n• Google リッチリザルト テスト:構造化データを検証\n• Facebook Sharing Debugger:OG タグをテスト\n• Twitter Card Validator:Twitter Cards をテスト\n• Google Search Console:sitemap を送信し監視\n\nチェックリスト:\n• 各ページに独自の title と description がある\n• OG 画像サイズが正しい(1200x630)\n• 構造化データのフォーマットが正しい\n• sitemap が Search Console に送信済み"}],"totalTime":"PT4H"}

Next.js SEO 最適化完全設定フロー

Metadata API 設定から構造化データ、sitemap、robots.txt までの完全な SEO 最適化手順

⏱️ Estimated time: 4 hr

  1. 1

    Step1: 基本 Metadata の設定

    Next.js 15 Metadata API を使用:
    • layout.js または page.js で metadata オブジェクトをエクスポート
    • title、description、keywords を設定
    • Open Graph と Twitter Cards を設定

    例:
    export const metadata = {
    title: 'ページタイトル',
    description: 'ページ説明',
    openGraph: {
    title: 'OGタイトル',
    description: 'OG説明',
    images: ['/og-image.jpg']
    }
    }
  2. 2

    Step2: 動的 Metadata の設定

    動的ルートの場合:
    • generateMetadata 関数を使用
    • ルートパラメータに基づいて metadata を生成
    • データの非同期取得(async)をサポート

    例:
    export async function generateMetadata({ params }) {
    const post = await getPost(params.id)
    return {
    title: post.title,
    description: post.description
    }
    }
  3. 3

    Step3: 構造化データの追加

    JSON-LD フォーマットを使用:
    • script タグを作成し JSON-LD を追加
    • Article、Product、FAQ などのタイプをサポート
    • Schema.org の語彙を使用

    例:
    <script type="application/ld+json">
    {JSON.stringify({
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "記事タイトル"
    })}
    </script>
  4. 4

    Step4: sitemap と robots.txt の設定

    sitemap.ts の作成:
    • default 関数をエクスポートし sitemap 配列を返す
    • 全ページの URL、lastModified、changeFrequency を含める
    • 動的生成をサポート

    robots.ts の作成:
    • 許可/禁止するクローラーを設定
    • sitemap パスを設定
    • クロールルールを設定
  5. 5

    Step5: 画像 SEO の最適化

    画像最適化のポイント:
    • next/image コンポーネントを使用
    • 意味のある alt テキストを追加
    • 画像サイズを設定(OG 画像には 1200x630 が最適)
    • WebP/AVIF フォーマットを使用
    • 画像構造化データ(ImageObject)を追加
  6. 6

    Step6: 検証和テスト

    検証ツール:
    • Google リッチリザルト テスト:構造化データを検証
    • Facebook Sharing Debugger:OG タグをテスト
    • Twitter Card Validator:Twitter Cards をテスト
    • Google Search Console:sitemap を送信し監視

    チェックリスト:
    • 各ページに独自の title と description がある
    • OG 画像サイズが正しい(1200x630)
    • 構造化データのフォーマットが正しい
    • sitemap が Search Console に送信済み

FAQ

SSR と SEO にはどんな関係がありますか?
SSR(サーバーサイドレンダリング)は HTML をサーバーで生成するだけですが、SEO には meta タグ、構造化データ、Open Graph などの正しい設定が必要です。SSR は SEO フレンドリーと同義ではありません。Metadata API を能動的に設定して初めて、検索エンジンのコンテンツ理解とインデックスが可能になります。
Metadata API と Head コンポーネントの違いは何ですか?
Metadata API は Next.js 15 推奨の方法で、型安全、静的・動的 metadata のサポート、重複タグの自動処理が特徴です。Head コンポーネントは React の方法で、手動管理が必要でミスが起きやすいです。新規プロジェクトでは Metadata API が推奨されます。
動的ルートの metadata はどう設定しますか?
generateMetadata 関数を使用します。params パラメータを受け取り、async でデータを取得して metadata オブジェクトを返します。

例:
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Open Graph 画像のサイズはどれくらいがいいですか?
推奨サイズは 1200x630 ピクセルです。これは Facebook、Twitter、LinkedIn など主要プラットフォームがサポートする万能サイズです。ファイルサイズは 1MB 以内、フォーマットは JPEG か PNG を推奨します。
構造化データは必須ですか?
必須ではありませんが、強く推奨されます。構造化データにより検索結果にリッチリザルト(評価、価格、FAQなど)が表示され、クリック率が向上します。Google、Bing などの検索エンジンは JSON-LD 形式の構造化データをサポートしています。
sitemap と robots.txt は手動で作る必要がありますか?
いいえ。Next.js は sitemap.ts と robots.ts ファイルの作成をサポートしており、sitemap.xml と robots.txt を自動生成します。sitemap.ts で全ページの URL を動的生成し、robots.ts でクロールルールを設定できます。
SEO 最適化後、どれくらいで効果が出ますか?
通常 1〜3ヶ月かかります。検索エンジンが新しいコンテンツをクロールしインデックスするのに時間が必要です。

アドバイス:
1) sitemap を Google Search Console に送信
2) Google リッチリザルト テストで検証
3) Search Console のデータを継続的に監視
4) コンテンツの更新を続ける

9 min read · 公開日: 2025年12月19日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事