SEO

React SEO対策とは?SSR・SSG・プリレンダリングの選び方とReact 19の実装手順

React SEO対策とは?SSR・SSG・プリレンダリングの選び方とReact 19の実装手順

ReactのSEO対策とは、検索エンジンやSNSのクローラーが読む最初のHTMLに本文とメタデータを入れておくための設計です。Reactそのものが検索に弱いわけではありません。標準的なSPA構成(クライアントサイドレンダリング=CSR)だと、サーバーが返すHTMLの<body>がほぼ空になる点が問題になります。Vite 8.3.5とReact 19.3.1で最小構成のSPAをビルドしたところ、配信されるHTMLの本文は0文字でした。この記事では、方式の選び方とNext.js 16・React Router v8・Vite単体での設定、React 19の<title>で実際に起きた落とし穴を説明します。

まとめ:React SEO対策の要点

  • 不利になる原因はCSR:初期HTMLの本文が空になり、Googleは後からJavaScriptを実行して読みます。待ち時間はページによって秒単位から十数時間まで幅があり、SNSやAI検索のクローラーの多くはJavaScriptを実行しません。
  • 対策の本筋はSSRかSSG:公開ページはHTMLにして返します。更新が頻繁ならSSR、内容が固定ならSSG(プリレンダリング)です。ログイン後の画面はCSRのままで構いません。
  • ダイナミックレンダリングは選ばない:Googleは「回避策であり、長期的な解決策ではなかった」と位置づけ、SSR・静的レンダリング・ハイドレーションを推奨しています。
  • メタタグはReact 19で直書きできる:ただし<title>の子が配列になると空になる、index.htmlの固定titleと2本並ぶ、部分だけSSRするとbodyに残る、の3点に注意が必要です。
  • SPAの作法:リンクは<a href>とHistory API、#/のURLは使わない、存在しないページはソフト404にしない、noindexを後からJSで外さない、の4点を守ります。

CSRのReactがSEOで不利になる仕組み

初期HTMLの本文0文字(Vite 8.3.5での実測)

Vite 8.3.5・@vitejs/plugin-react 6.1.2・React 19.3.1(いずれも2026年10月時点のnpm最新版)で、商品名と説明文を表示するだけのSPAをビルドしました。vite buildが出力したdist/index.htmlは次のとおりです。

<!doctype html>
<html lang="ja">
  <head><meta charset="UTF-8" /><title>Vite App</title>  <script type="module" crossorigin src="/assets/index-hasBNTwa.js"></script>
</head>
  <body><div id="root"></div></body>
</html>

本文もページ固有のtitleも入っていません。商品名・説明文・canonicalは、220KB(gzip後69KB)のJavaScriptが実行されて初めてDOMに現れます。同じページをヘッドレスChromium(Playwright同梱のchrome-headless-shell)で開き、JavaScript実行後の状態と比べた結果が次の表です。

項目 配信HTML(JS実行前) JS実行後のDOM
bodyの文字数 0 53
document.title Vite App React SEO入門 | サンプルストア
meta description なし あり
link rel=”canonical” なし あり

検索エンジンがJavaScriptを実行してくれれば右の列が読まれ、実行しなければ左の列しか残りません。React SEO対策は、左の列に右の列の中身を入れる作業だと考えると判断がぶれません。

Googlebotのクロール・レンダリング・インデックスの3段階

Google検索セントラルのJavaScript SEOの基本(2026年3月4日更新)によると、Googleは取得したHTMLを一度解析し、HTTPステータスが200のページをレンダリングのキューに入れます。キューに留まるのは「数秒の場合もあるが、それより長くかかることもある」とされ、その後ヘッドレスChromiumがJavaScriptを実行し、レンダリング後のHTMLでインデックスします。同じドキュメントは、200以外のステータスではレンダリングが省かれることがあるとも書いています。そのうえで「サーバーサイドレンダリングやプリレンダリングは今でも良い考えだ。ユーザーにもクローラーにも速く、すべてのボットがJavaScriptを実行できるわけではない」と明記しています。

待ち時間の実測としては、VercelとMERJが2024年4月にnextjs.orgで10万回超のGooglebotの取得を分析した調査があります。ステータスエラーとインデックス対象外のページを除くHTMLページはすべてレンダリングされ、照合できた約3万7,000件でのレンダリングまでの遅延は中央値10秒、75パーセンタイル26秒、90パーセンタイル約3時間、99パーセンタイル約18時間でした。クエリ文字列付きURLは90パーセンタイルが約8.5時間と長くなっています。nextjs.orgはSSG・SSR・CSRを併用するサイトで、調査元はNext.jsの開発元です。「Googleは最終的に読む」という裏付けにはなりますが、CSRだけのサイトでも同じ速さになる根拠にはなりません。

JavaScriptを実行しないAIクローラー・SNSボットの取得範囲

Google以外は事情が違います。Vercelが2024年12月17日に公開したAIクローラーの調査では、GPTBot・OAI-SearchBot・ChatGPT-User(OpenAI)、ClaudeBot(Anthropic)、PerplexityBotなど主要なAIクローラーはJavaScriptを実行していませんでした。GeminiはGooglebotの基盤を使うためレンダリングされ、AppleBotもブラウザベースで実行します。CSRだけのサイトは、これらのクローラーには本文が空のページとして取得されます。

SNSのリンクプレビューも同じです。Next.jsのドキュメントは、facebookexternalhitをJavaScriptを実行できないボットの例に挙げ、既定の対象リストにBingbot・Twitterbot・Slackbotを入れています。OGPタグを初期HTMLに入れないと、ページごとのタイトルや画像はシェア時に出ません。OGPの書き方はOGPタグの種類と書き方|og:typeの値一覧とproductの扱い・画像サイズ・確認方法で解説しています。

SSR・SSG・プリレンダリングの選び方

方式の違いは「そのページのHTMLを、いつ・どこで作るか」です。

方式 HTMLを作るタイミング 向くページ SEO上の扱い
CSR ブラウザでJS実行時 ログイン後の管理画面 検索に出さない前提なら可
SSR リクエストごとにサーバーで 在庫・価格が変わる商品、検索結果一覧 初期HTMLに本文が入る
SSG(プリレンダリング) ビルド時 ブログ、製品紹介、ヘルプ 初期HTMLに本文が入りCDNから配信可
ISR ビルド後に一定間隔で再生成 件数が多く更新もあるカタログ SSGと同等
ダイナミックレンダリング ボットにだけ別途 新規には使わない Googleが回避策と明記

判断は2つの問いで足ります。そのページを検索に出したいか。出したいなら、内容がリクエストごとに変わるか。変わるならSSR、変わらないならSSGです。ユーザーごとに中身が違うダッシュボードや、ログインしないと見られない画面をSSRにしても検索流入は増えず、サーバー費用だけが増えます。

ダイナミックレンダリング(ユーザーエージェントを見てボットにだけレンダリング済みHTMLを返す方式)について、Googleは公式ドキュメント(2025年12月10日更新)の冒頭で「回避策であり、長期的な解決策ではなかった」と書き、代わりにサーバーサイドレンダリング・静的レンダリング・ハイドレーションを推奨しています(既存構成の扱いは後述のよくある質問を参照)。

各方式の定義と速度面の比較はSSR・CSR・SSG・ISG・ISRの違いと使い分け|定義・SEO・速度を比較、CSRとSSRの2択に絞った比較はCSRとSSRの違いと使い分け|レンダリング方式を比較して実装で選ぶにまとめています。

フレームワーク別のSSR・SSGの設定手順

Next.js 16のgenerateMetadataとストリーミングメタデータ

Next.js(2026年10月時点の最新は16.4.2)のApp Routerは、サーバーコンポーネントが既定なので、何もしなくてもページ本文はサーバーでHTMLになります。ページごとのtitle・description・OGPはgenerateMetadataで返します。Next.js 15以降はparamsがPromiseなのでawaitが必要です。

// app/products/[id]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";

type Props = { params: Promise<{ id: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const res = await fetch(`https://api.example.com/products/${id}`);
  if (!res.ok) notFound();
  const product = await res.json();
  return {
    title: `${product.name} | サンプルストア`,
    description: product.summary,
    alternates: { canonical: `https://example.com/products/${id}` },
    openGraph: { title: product.name, images: [product.imageUrl] },
  };
}

注意点は、15.2.0で入ったストリーミングメタデータです。Next.jsのドキュメントによると、generateMetadataの完了を待たずに画面を送り始め、解決したメタデータは<body>の末尾に追加されます。Googlebotのように完全なDOMを見るボットでは正しく解釈されることを確認済みとされ、JavaScriptを実行できないボットには従来どおり<head>に入れて返します。ボットの判定はユーザーエージェントで、next.configのhtmlLimitedBotsで上書きでき、/.*/を指定するとストリーミングを無効にできます。社内の監視ツールやAIクローラーでtitleが取れないときは、まずここを疑ってください。

SSG・ISR・SSRの切り替え方はNext.jsのレンダリング方式|SSG・ISR・SSR・CSRの違いとApp Routerでの選び方で扱っています。

React Router v8のフレームワークモードでのprerender設定

Next.jsに移らずに済ませたいなら、React Router(2026年10月19日公開の8.5.0が最新)のフレームワークモードが候補です。公式ドキュメントはレンダリング方式を「クライアントサイドレンダリング」「サーバーサイドレンダリング」「静的プリレンダリング」の3つとし、react-router.config.tsで切り替えます。

// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: false,             // 実行時のサーバーを持たない
  async prerender() {     // ビルド時にHTMLを書き出すURL
    const ids = await fetch("https://api.example.com/products").then((r) => r.json());
    return ["/", "/about", ...ids.map((id: string) => `/products/${id}`)];
  },
} satisfies Config;

prerender: trueだけでは/blog/:slugのような動的パスは書き出されないため、上のように一覧を返します。ssr: falseでprerenderを指定しないと「SPAモード」になり、HTMLは1枚だけになります。書き出していないパスはSPA Fallbackを200で返すホスト設定になるため、存在しないURLの扱いは後述のソフト404対策と合わせて決めます。ページごとのtitleは、ルートモジュールのmetaexportで返します(v8では引数がloaderData)。細かい書き方とtitleが二重になる問題はReact Router v8の使い方|meta・title設定と基本ルーティング、v7からの変更点にまとめています。loaderでのデータ取得とprerenderの組み合わせはReact Routerのaction・loaderの使い方|clientAction/clientLoaderの違いとv8対応で扱っています。

Vite単体のSPAへのビルド時プリレンダリングの追加

フレームワークを入れられない既存のViteプロジェクトでも、React 19のreact-dom/staticを使えば、公開したいURLだけ静的HTMLにできます。SSR用のバンドルをvite build --ssr src/entry-server.jsx --outDir dist-ssrで作り、render()が返すHTMLをdist/index.htmlの<div id="root">に差し込んで、URLごとのindex.htmlとして書き出します。

// src/entry-server.jsx
import { prerenderToNodeStream } from "react-dom/static";
import App from "./App.jsx";

export async function render(product) {
  const { prelude } = await prerenderToNodeStream(<App product={product} />);
  let html = "";
  for await (const chunk of prelude) html += chunk;
  return html;
}

// prerender.mjs(vite build と vite build --ssr の後に node で実行)
import fs from "node:fs";
import { render } from "./dist-ssr/entry-server.mjs";

const tpl = fs.readFileSync("dist/index.html", "utf8").replace(/<title>.*?<\/title>/, "");
const product = { name: "別の商品", desc: "プリレンダリングの検証用" };
const body = await render(product);
const data = `<script id="__DATA__" type="application/json">${JSON.stringify(product).replace(/</g, "\\u003c")}</script>`;
fs.mkdirSync("dist/products/react-seo", { recursive: true });
fs.writeFileSync("dist/products/react-seo/index.html",
  tpl.replace('<div id="root"></div>', `<div id="root">${body}</div>${data}`));

// src/main.jsx(書き出したデータでハイドレーションし、無ければ通常の描画)
import { createRoot, hydrateRoot } from "react-dom/client";
import App from "./App.jsx";

const el = document.getElementById("root");
const product = JSON.parse(document.getElementById("__DATA__")?.textContent ?? "null") ?? undefined;
if (el.hasChildNodes()) hydrateRoot(el, <App product={product} />);
else createRoot(el).render(<App product={product} />);

書き出したページには初期HTMLの時点で商品名と説明文が入り、ヘッドレスChromiumで開いてもエラーは出ませんでした。サーバーで使ったデータをクライアントにも渡すことが要点です。__DATA__を渡さずにhydrateRoot(el, <App />)とすると、書き出した内容とクライアントの描画が食い違ってコンソールにエラーが出て、titleがAppの既定値(React SEO入門)に書き換わりました。ブラウザで本文が別の商品に化けるため、Googleのレンダリング結果も誤った内容になります。ハイドレーションの不一致の原因と対処はハイドレーションエラー(Hydration Error)とは?原因とReact / Next.jsでの対処法を解説で詳しく扱っています。

この構成には次の章で説明する落とし穴もあり、titleとmetaが<head>に入りません。対象URLが数百を超える、在庫で中身が変わる、といった条件ならフレームワークへ移ったほうが保守は楽です。Viteの基本設定はVite + Reactの環境構築手順|Viteとは・Vite 8.3の変更点とTypeScript 7対応を参照してください。

React 19のtitle・meta設定と3つの落とし穴

React 19からは、<title>・<meta>・<link>をコンポーネントのどこに書いても、クライアントで描画するか文書全体をReactで描画すれば、Reactが<head>へ移します(react.devの<title>リファレンス)。react-helmetは6.1.0(2020年6月)を最後に更新が止まっており、新規に入れる理由はほぼありません。

export default function ProductPage({ product }) {
  return (
    <article>
      <title>{`${product.name} | サンプルストア`}</title>
      <meta name="description" content={product.desc} />
      <link rel="canonical" href={`https://example.com/products/${product.id}`} />
      <h1>{product.name}</h1>
    </article>
  );
}

コンポーネントに直書きする方式をReact 19.3.1で検証したところ、上のコードの形に落ち着くまでに次の3点でつまずきました。

titleの子の配列化によるサーバー出力の空title

<title>{name} | サンプルストア</title>と書くと、子は「変数」と「文字列」の2要素の配列になります。renderToStringの出力は<title></title>と空になり、コンソールに「React expects the children prop of <title> tags to be a string」という警告が出ました。react.devも子は単一の文字列にするよう求めています。上のコードのようにテンプレートリテラルで1本の文字列にすれば、正しく<title>React SEO入門 | サンプルストア</title>が出力されます。

index.htmlの固定titleとの2本並び

Viteのindex.htmlに<title>Vite App</title>を残したままCSRで描画すると、JavaScript実行後の<head>にはReactのtitleと固定のtitleが両方入っていました。Reactのものが先に挿入されるためdocument.titleは正しく見えますが、titleが2本ある状態です。react.devは、複数のtitleがあるときのブラウザと検索エンジンの挙動は「未定義」としています。Reactは既存のtitleを消さないため、既定値に置き換えても2本並ぶ状態は変わりません。固定のtitleはindex.htmlから消してください。レイアウトとページの両方で<title>を描画した場合も同じく2本になるので、titleを描画するコンポーネントは1つに限ります。

部分ツリーのプリレンダリングでbodyに残るtitle

前の章のようにアプリの部分ツリーだけをprerenderToNodeStreamで描画すると、<title>・<meta>・<link>は<div id="root">の中に出力されます。ハイドレーション後も<head>内のtitleは0本、bodyに1本のままでした。<html>から</html>まで文書全体をReactで描画した場合は、titleが<head>に入ることを確認しています。Googleのページメタデータのドキュメントは、メタデータを指定する主な場所は<head>で、マークアップの誤りは検索での扱いに問題を起こしうるとしています。rel=”canonical”は<head>内にある場合だけ受け付けるとも明記されており(重複URLの統合)、body内に出たcanonicalは効きません。自前でSSRを組むなら、文書全体をReactで描画するか、titleとmetaを文字列としてテンプレートの<head>に差し込んでください。

構造化データ(JSON-LD)も同じく初期HTMLに入れます。型の選び方は構造化データとは?SEO効果とJSON-LD実装・型の選び方【2026年時点】、React 19のそのほかの変更はReact 19とは?18からの変更点・新機能とReact Compiler・最新バージョンを解説で解説しています。

SPAでGoogleが求めるルーティング・404・noindexの扱い

SSRやSSGにしても、画面遷移をクライアント側で行う性質は残ります。GoogleのJavaScriptの問題の修正ガイドとJavaScript SEOの基本には、SPA固有の注意が具体的に書かれています。

  • リンクは<a href>で書く:Googleがリンクを見つけられるのは、href属性を持つ<a>要素だけです。onClickでnavigate()を呼ぶだけのボタンはクロールの経路になりません。
  • #/products形式のURLを使わない:AJAXクロールの仕組みは2015年から非推奨となっており、フラグメントで画面を切り替えるURLはGooglebotが確実に解決できません。History APIでパスを切り替えます(React RouterならBrowserRouter系を使い、HashRouterは避けます)。
  • 存在しないページをソフト404にしない:CSRのSPAは、存在しない商品でも200を返しがちです。サーバーが404を返すURL(例:/not-found)へJavaScriptでリダイレクトするか、エラー画面で<meta name="robots" content="noindex">を追加します。
  • noindexを後からJavaScriptで外さない:Googleは元のHTMLにnoindexを見つけるとレンダリングを省くことがあるため、JSで書き換えても効かない場合があります。インデックスさせたいページの初期HTMLにはnoindexを書きません。
  • JS・CSSのファイル名にハッシュを付ける:Googleのレンダリング基盤はキャッシュヘッダーを無視して古いファイルを使うことがあります。Viteのindex-hasBNTwa.jsのような内容ハッシュ付きの名前はこの対策になります。

SPAとMPAの構造の違いそのものはSPAとは?開発言語・フレームワークの選び方とMPA・SSRとの違い【2026年10月】で整理しています。

公開後のURL検査ツールによるレンダリング結果の確認手順

実装後は、Googleが実際に何を読んだかをSearch Consoleで確認します。ローカルの確認だけでは、ボット判定やキャッシュの違いを見落とします。

  1. Search ConsoleのURL検査に対象URLを入れ、「公開URLをテスト」を実行します。
  2. 「テスト済みのページを表示」の「HTML」タブで、本文・title・description・canonicalがレンダリング後のHTMLに入っているかを見ます。
  3. 同じ画面の「その他の情報」で、読み込めなかったリソースとJavaScriptコンソールのエラーを確認します。
  4. JavaScriptを実行しないクローラー向けには、curl -A "facebookexternalhit/1.1" https://example.com/products/1のように取得し、初期HTMLだけでtitleとOGPが揃っているかを見ます。

Search Consoleの登録と基本の見方はサーチコンソールとは?できること・設定手順とSEO改善への使い方【2026年時点】で解説しています。レンダリング以外の内部対策(サイト構造・内部リンク・表示速度)を含めた優先順位はSEO内部対策とは?クロール・インデックス・UXの3分類で施策を優先度順に解説を参照してください。社内に検証の手が足りない場合は、SEO内部施策・キーワード設計の支援のような外部の伴走を使い、レンダリング方式の見直しから進める方法もあります。

古いReact SEO解説の置き換え表(CRA・React Snap・Helmet・FID)

「React SEO」で見つかる解説には、数年前の前提のまま更新されていないものが多く含まれます。次の4つが出てきたら、現行の情報に置き換えて読んでください。

古い記述 現状 代わりに使うもの
Create React Appで始める 2025-02-14に新規向けを非推奨化 Next.js・React Router・Vite
React Snapでプリレンダリング 最終版1.23.0は2018-12-13 React Routerのprerender等
React Helmetでmeta管理 最終版6.1.0は2020-06-08 React 19の<title>・<meta>
FIDを改善する 2024-03-12にINPへ置き換え INP(200ms以下が良好)

Create React Appの非推奨化はReact公式ブログで発表され、既存アプリにはフレームワークかViteなどのビルドツールへの移行が勧められています。INPへの切り替えはweb.devの告知のとおりです。

よくある質問

ReactはSEOに弱いのですか?

React自体ではなく、CSRだけで配信する構成が不利です。Googleは後からJavaScriptを実行して読みますが、Vercelの調査ではレンダリングまでの待ち時間が99パーセンタイルで約18時間でした。GPTBotやClaudeBotなどのAIクローラーと、SNSのリンクプレビュー用クローラーの多くはJavaScriptを実行しません。公開ページをSSRかSSGにすれば、この不利はなくなります。

React SEO対策はSSRとSSGのどちらを選べばよいですか?

内容がリクエストごとに変わるならSSR、ビルド時点で確定するならSSGです。ブログや製品紹介はSSG、在庫や価格が変わる商品ページや検索結果一覧はSSRが向きます。ページ数が多く更新もあるカタログなら、一定間隔で再生成するISRが中間の選択肢になります。Next.jsやReact Routerならページ単位で方式を混在させられるので、サイト全体を1つの方式に揃える必要はありません。

ダイナミックレンダリングはまだ使えますか?

Googleは回避策であり長期的な解決策ではないとしており、SSR・静的レンダリング・ハイドレーションを推奨しています。Googleは、ボットに返す内容がユーザー向けと同等である限り原則としてクローキングとはみなさないと説明しています。既存の構成がすぐ問題になるわけではありませんが、レンダリング用サーバーの保守が別に必要になるため、新しく作るなら選ばないでください。

エッジレンダリングにSEO効果はありますか?

エッジレンダリングは、SSRをCDNの拠点で実行する配置の話です。検索エンジンが受け取るHTMLは通常のSSRと同じなので、インデックスのされ方は変わりません。差が出るのは利用者に近い拠点で応答できることによる表示の速さで、評価への影響もページ体験を通じた間接的なものにとどまります。

Next.jsを使わずにReactのSEO対策はできますか?

できます。React Router v8のフレームワークモードでprerenderを設定する方法と、Viteのままreact-dom/staticで公開URLだけを静的HTMLにする方法があります。後者は手軽ですが、titleとmetaを<head>へ入れる処理と、書き出しに使ったデータをハイドレーション時にクライアントへ渡す処理を自分で書く必要があります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

ほか 3 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  2. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  3. 2026.10.23 テックブログ Splunk 脆弱性CVE-2026-76299:影響版の確認と10.2.8などへの更新手順
  4. 2026.10.22 テックブログ Tomcatのバージョン一覧とサポート期限【2026年10月】最新版11.0.27、9.0系は2027年3月末終了・9.1系は2030年末まで
  5. 2026.10.21 テックブログ ユミルリンク不正アクセスで延べ約900万件:Cuenote FC利用企業と受信者の確認点

RELATED POSTS 関連記事

目次