SPAのSEO
シングルページアプリケーション(React Router、Vue Router、Angular Router)をクロール可能かつインデックス可能にする方法 — アプリシェルとソフト404の問題、History APIとハッシュルーティング、SSR/プリレンダリングによるルートごとのHTML、ルートごとのcanonicalとタイトル、クライアントサイドルートのサイトマップ生成。
言語
シングルページアプリケーションは1つのドキュメントを読み込み、サーバーへ新しいページを要求する代わりにJavaScriptでビューを切り替えます。多くのSPAは「アプリシェル」を使い、各ルートをJavaScriptがレンダリングするまで、サーバーが1つのURL分の実際のHTMLではなく、ほぼ空のシェルを返します。ただし、これは一般的な実装の1つであり、すべてのSPAに当てはまる規則ではありません。まず、各ルートへの直接リクエストが実際に返す内容を確認してください。素のアプリシェルが問題なら、対策には2つの独立した側面があります。アドレス可能性(ハッシュや#!フラグメントではなくHistory APIを使い、各ビューに実際のURLを与えること。History APIによるソフトナビゲーションはURLとUIを変えますが、それ自体は新しいサーバーレスポンスを作りません)と、コンテンツの可用性(SSR、プリレンダリング、またはメタフレームワークにより、各URLがリクエスト時に固有のHTMLを返せるようにすること)です。さらに、各ルートのtitle、canonical、robotsの状態を直接アクセス時とレンダリング後の両方で確認し、ソフトエラーを処理します。ルーターは「見つかりません」ビューでも200を維持しがちなため、それ自体がエラーを返すURLへリダイレクトするか、レンダリング後のnoindexを追加します。ただし初期状態のnoindexはレンダリングをスキップさせる可能性があります。最後に、サイトマップ内のすべてのルートが実在するインデックス可能なHTMLを返すことを確認してください。SPA専用のサイトマップ形式はありません。
TL;DR — シングルページアプリケーション(SPA)は、サーバーから1つのページを読み込み、その後JavaScriptを使用して完全なリロードなしで「ページ」を切り替えます。問題は、多くのSPAが検索エンジンがどのURLを要求しても同じほぼ空の初期ページを送信することです。これはSPAの一般的な構築方法によるリスクであり、すべてのSPAに当てはまるわけではありません。修正するには、各ルートに独自の実際のURLと独自の実際のHTMLを持たせることです。通常は、サーバー側でページをレンダリングするか、事前にビルドします。
SPAとは
シングルページアプリケーションは、通常、完全なドキュメントナビゲーションなしでクライアント側のビューとルートを更新します。 Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application GoogleはJavaScript SPAをレンダリングできますが、クロール可能なURL、リンク、ステータス処理、レンダリングされたコンテンツが必要です。 Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
シングルページアプリケーションは、1つのHTMLページとして構築されたウェブサイトです。商品ページからアバウトページへクリックして移動するとき、JavaScriptがサーバーに新しいページ全体を要求する代わりに、画面上の表示を切り替えます。React(React Routerを使用)、Vue(Vue Routerを使用)、Angularはすべてデフォルトでこのように動作します。高速でアプリのような感覚があり、人気があります。
リスクはサーバーが送信するものです。多くのSPAは「アプリシェル」実装を使用しています。最初に誰か(Googleを含む)がアプリを読み込むとき、サーバーはほぼ空のシェルを返し、実際のコンテンツはその後ブラウザで構築されます。これはSPAを構築する一般的な方法の1つであり、SPAの定義でも、すべてのSPAが行うことでもありません。しかし、サイトが裸のアプリシェルを配信する場合、障害モードは現実のものです。Googleがサーバーに/aboutと/productsを別々に要求すると、ルートがサーバーが送信するものを変更しないため、両方に対してまったく同じ空白のシェルを受け取る可能性があります。アプリがその状況にあるかどうかを知る方法は、各ルートへの直接リクエストが実際に何を返すかを確認することであり、SPAであるという事実から推測することではありません。
それがSEOに悪影響を与える理由
検索エンジンはコンテンツを見る必要があります。裸のSPAでは、3つのことが問題になる傾向があります:
- すべてのURLがサーバーにとって同じに見える。 ディープリンク、共有、クローラーはすべて同じシェルに到達します。
- 404ページが「200 OK」を返す。 JavaScriptルーターは「見つかりません」画面を表示しながら、ページが技術的に成功を報告するため、Googleが空のページをインデックスする可能性があります。
- 間違ったURL。 古いSPAは
example.com/#/productsのようなアドレスを使用していました。Googleはこれらを確実にインデックスできません。
修正方法(短いバージョン)
- 各ビューに実際のURLを割り当てる ブラウザのHistory APIを使用して(
/productsのようなクリーンなパス)、#ベースのものではなく。 - 各URLに対して実際のHTMLを送信する。 サーバー側でページをレンダリングする(SSR)か、事前にビルドする(プリレンダリング)。新しく始める場合は、これを自動で行うフレームワーク(Next.js、Nuxt、SvelteKit、AngularのSSR)を使用すると手間が省けます。
- 各ページに独自のタイトルと説明を設定する ルートが変更されたときに変更されるもの。
- リンクを実際のリンクにする(
<a href>)、クリック可能な<div>ではなく。
仕組みを知りたいですか?— なぜハッシュルーティングが失敗するのか、カノニカルに関する「最も制限的なディレクティブ」の罠、クライアント側ルートのサイトマップを生成する方法。詳細タブに切り替えてください。
TL;DR — SPAのSEOにおける本質的なリスクは、抽象的な「JavaScript」ではなく、クライアントサイドルーティングがサーバーの応答を変えずにユーザーに見える内容を変えることです。対策には、混同されがちな2つの独立した側面があります。アドレス可能性(History API、ルートごとに固有のURL、
#!フラグメントを使わないこと)と、コンテンツの可用性(SSR、プリレンダリング/SSG、またはメタフレームワーク)です。前者だけを実施すると、どのURLも同じシェルをレンダリングするだけの整然としたサイトマップができあがります。さらに、各ルートのレンダリング後のDOMに固有のcanonical、title、descriptionを設定し、200のままになるソフトエラーを処理し、サイトマップ内のすべてのルートがそれぞれ実際のHTMLを返すようにする必要があります。
SPA SEOとは実際には何か
SPAはアプリケーションアーキテクチャであり、SEOが必ず失敗する状態ではありません。 Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application サーバーレンダリング、プリレンダリング、または慎重に実装されたクライアントレンダリングによってコンテンツを公開できますが、いずれもインデックスを保証するものではありません。 Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
これはシングルページアプリケーションに特化した詳細解説です。React Router、Vue Router、Angular Routerを使用したクライアントサイドルーティングで、最初の読み込み以降、サーバーが完全なページを送信することはありません。これは、より広範なJavaScript SEOガイド(パリティ、遅延読み込み、無限スクロール、JSレンダリング全般をカバー)や、React、Next.js、Nuxt、Angular、Vue、Svelte、Astroに関するフレームワーク別の記事と並ぶものです。ここでは、ルーティングレイヤーとそれがクロールとインデックスに与える影響のみに焦点を当てます。
素のクライアントサイドルーティングSPAがSEOで失敗する理由
1つのURL、1つのHTMLレスポンス — アプリシェル問題
Googleはこの失敗モードを正確に説明しています:“Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (翻訳) 「一部のJavaScriptサイトでは、初期HTMLに実際のコンテンツが含まれておらず、Googleが実際のページコンテンツを表示する前にJavaScriptを実行する必要があるアプリシェルモデルを使用する場合があります。」 素のSPAでは、「アプリシェル」がサーバーが返すすべてです。/productsを新しく取得しても、/aboutで取得するのと同じほぼ空のドキュメントが返されます。コンテンツが分岐するのは、ブラウザがJavaScriptを実行し、ルーターが表示内容を決定した後だけです。
問題の核心は一文で表せます。クライアントサイドルーティングは、サーバーが返す内容を変えずに、ユーザーに見える内容を変えます。 ソフトエラー、重複コンテンツ、titleの欠落といった後続の問題は、すべてその症状です。
サーバーはどの「ページ」がリクエストされたかを認識しない(ハッシュルーティングが失敗する理由)
従来のSPAでは、example.com/#/productsのようなURLフラグメントからビューを読み込んでいました。Googleの原文では、“A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” (翻訳) 「SPAでは、異なるビューを読み込むためにURLフラグメント(例:example.comの/#/products)を使用する場合があります。」と説明されています。これが失敗する理由は明確で、ブラウザはHTTPリクエストでフラグメント(#以降)をサーバーへ送信しないからです。サーバーには、どの「ページ」が要求されたかを知る手段がないため、ルートごとに異なるコンテンツやステータスコードを返せません。クローラーがURLを取得しても、フラグメントにかかわらず同一のHTMLを受け取ります。
これが、Googleが2015年10月に2009年のAJAXクローリング方式を正式に非推奨とした理由でもあります:“In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (翻訳) 「要するに、2009年に提案したAJAXクローリングの提案はもう推奨していません。」 古い_escaped_fragment_回避策は、リクエスト時にフラグメントルートをサーバー側でプリレンダリングできるようにしましたが、ルーティング問題を修正するのではなく、回避策を講じたにすぎません。それに代わるGoogle自身の推奨は、以下で説明するHistory APIです。
ソフトエラー — クライアントサイドルーターはすべてに200を維持する
素のSPAでは、この問題はほぼ避けられません。クライアントサイドルーターは設計上、「見つかりません」の状態を含むすべての仮想ナビゲーションで、元のページの200ステータスを維持します。Googleも明示しています。“In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (翻訳) 「シングルページアプリケーション(SPA)では、これは特に難しい場合があります。エラーページがインデックスされるのを防ぐには、次の戦略の一方または両方を使用できます。」 その理由についても、“When a SPA is using client-side JavaScript to handle errors they often report a 200 HTTP status code instead of the appropriate status code.” (翻訳) 「SPAがクライアントサイドJavaScriptでエラーを処理すると、適切なステータスコードではなく成功ステータスを報告することがよくあります。」と説明しています。
その結果、空のビューやエラービューが薄い200ページとしてインデックスされることになります。Googleはここで正確に2つの戦略を文書化しています:サーバーが実際の404/エラーステータスを返すURLへのリダイレクト(または完全なリクエスト)を行うか、JavaScriptでエラービューにnoindexタグを追加します。2つ目に注意してください:そのnoindexが、アプリがルートが無効だと判断した後に追加されるのではなく、最初のペイントから存在する場合、Googleがページのレンダリングを完全にスキップする原因になる可能性があります。そのため、JSが実行される前に直接リクエストが何を返すかを確認してください。後でレンダリングされたDOMに表示されるものだけを確認するのではありません。
共有シェルの重複 — 可能性のある原因であり、自動的な診断ではない
2つ目の、より見つけにくい失敗があります。異なるルートが同じ共有ヘッダー、ナビゲーション、フッターの定型部分だけをレンダリングし、互いの重複としてインデックス登録される場合です。Gary Illyes氏は、その発生パターンの1つを次のように説明しています。“I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (翻訳) 「受信トレイには、中心となるコンテンツの読み込みに時間がかかりすぎてレンダリングがタイムアウトし(私が最も可能性が高いと考える説明)、定型部分しかないページが大量に残ったという相談が多数あります。定型部分しかなければ、それらのページは重複です。」 対策については、“Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (翻訳) 「コンテンツ(周辺の定型部分を含む)が最初に読み込まれるよう、JavaScript呼び出しの構成を変えてみてください。」 (LinkedInの投稿経由で伝えられたもので、自動検証が難しいため、信頼性の高い二次情報として扱います。)
Illyes氏が「最も可能性の高い説明」と表現しており、確定診断とはしていない点は、文字どおり受け取るべきです。「レンダリングのタイムアウト」は症状だけでは診断できません。ルートの重複コンテンツをタイムアウトのせいにする前に、リクエスト単位の証拠(直接取得した応答)、レンダリング後の証拠(中心となるコンテンツが存在するか)、Search Consoleの証拠(重複やインデックス登録のシグナル)が必要です。バグ、ブロックされたリソース、本当に同一のシェルといった別の原因も考えられます。また、設計の基準にできる固定の公開タイムアウトはありません。Googleの基本ドキュメントでは、ページが*“may stay on this queue for a few seconds, but it can take longer than that,”* (翻訳) 「このキューに数秒間留まる場合がありますが、それ以上かかることもあります」とされ、具体的な時間は示されていません。想定したレンダリングキューの長さに合わせるのではなく、実際の所要時間にかかわらず主要コンテンツができるだけ早く読み込まれるようにしてください。
修正方法: ルートごとに実際の HTML を取得する
完全な修正には、人々が常に混同している2つの独立した部分があります:
- URL アドレス可能性 — History API、ルートごとに一意の URL、フラグメントなし。
- コンテンツの可用性 — SSR、静的プリレンダリング/SSG、またはメタフレームワーク。
#1 だけを行うと、クリーンで共有可能な URL が得られますが、それらはすべて同じ空白のシェルを返します。 実際にインデックスさせたいルートには、両方が必要です。
ただし、これはどこにでも SSR を追加するという普遍的な義務ではありません。SSR とプリレンダリングは、クローラーが JavaScript の実行に成功することへの依存度を減らすものであり、サイトが技術的に SPA であるからといって、即座に必須になるわけではありません。特定のルートのアーキテクチャを選択する前に、3つのことを確認してください: そのルートがそもそもランキング対象になる必要があるか(内部管理パネルは対象外)、直接の JS なしのリクエストがすでに何を返すか(一部のセットアップはすでに意味のある HTML を送信します)、そして実際に満たす必要があるクローラーはどれか(Google は JS をかなり確実にレンダリングしますが、Bing とほとんどの AI クローラーはそれほどでもありません — 以下を参照)。これらのチェックをすでに通過している CSR は、サイトが SPA であるという理由だけで SSR に変更する必要はありません。
サーバーサイドレンダリング (SSR)
サーバーはリクエストごとにアプリを実行し、そのルートの完全に形成された HTML を返し、その後クライアントがそれをライブの SPA に「ハイドレート」します。これは最も堅牢なオプションです。クローラーは最初のフェッチで完全なコンテンツを取得でき、JS の実行は不要だからです。
静的プリレンダリング / SSG
リクエストごとにレンダリングする代わりに、デプロイ時にすべてのルートの HTML を事前にビルドします。 ユーザーごとに変わらないコンテンツに最適です。私自身の JavaScript SEO に関する記事から: あらゆる種類の SSR、静的レンダリング、およびプリレンダリングのセットアップは、検索エンジンにとって問題ありません。避けるべきことは、コンテンツをクライアントのみのレンダリングの背後にロックしたままにすることです。
または: 手作りせずにメタフレームワークを使用する
新しいビルドの場合、正直な推奨事項は、React-Routerのみ、またはVue-Routerのみのクライアントサイドルーティングを手作りしないことです。メタフレームワーク(Next.js、Nuxt、SSRパッケージを備えたAngular、SvelteKit、Remix)を使用すると、SSR/SSGとルートベースのメタデータが標準で提供され、このカテゴリの問題全体を回避できます。(これらのそれぞれについて、このサイトで詳細な解説があります。)裏面についても正直に述べてください:既存の手作りのSPAにSSRを後付けすることは、実際のエンジニアリング作業、つまり設定トグルではなく移行プロジェクトです。
動的レンダリングは最終手段ではなく、一時しのぎとして
クローラーには、別途レンダリングしたHTMLスナップショット(動的レンダリング)を配信できます。GoogleとBingはいずれもこれを認めています。Bingは*“recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (翻訳) 「JavaScriptに大きく依存するウェブサイトには、動的レンダリングを優れた代替手段として推奨する」* (Bingの2018年のブログからの伝聞です。後述するbingbotの能力に関する短い2つの引用は直接検証済みですが、この長い引用は今回独立して再確認していません。)としています。一方、Googleはこれが回避策だと明記しています。“Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (翻訳) 「動的レンダリングは回避策であり、長期的な解決策ではありませんでした。代わりに、サーバーサイドレンダリング、静的レンダリング、またはハイドレーションを解決策として使用することを推奨します。」 (Googleの動的レンダリングに関するドキュメントからの伝聞です。この文言は、このサイトのより広範なJavaScript SEOガイドで引用済みの内容と一致します。) これは橋渡しであり、アーキテクチャではありません。
History API vs. ハッシュルーティング(#!)
2つの状態ではなく、5つの状態
SPAルートのテストは、「機能するかどうか」が実際には5つの異なる、個別に検証可能な状態に及ぶため、混乱を招きます:
| 状態 | 内容 |
|---|---|
| サーバーレスポンス | JSなしの新しいHTTPリクエストがURLに対して実際に取得するバイト。 |
| レンダリングされたDOM | ブラウザ(またはGooglebotのレンダラー)がそのサーバーレスポンスに対してJavaScriptを実行した後に構築するもの。 |
| 検索処理 | Googleがサーバーレスポンスを個別にクロールし、後でページをレンダリングし、その両方に基づいてインデックスを作成する方法。 |
| ブラウザのフルドキュメントナビゲーション | URLへの実際の新しいHTTPリクエスト。サーバーレスポンスまたはステータスコードを変更できる唯一の状態。 |
| ブラウザのソフトナビゲーション | 表示されるURL、ブラウザ履歴、および画面上のUIを変更するHistory API遷移(pushState/replaceState)。 |
誰もが混同する点:ソフトナビゲーションはURLとUIを変更しますが、それ自体では新しいHTTPレスポンスやステータスを作成しません。それはフルナビゲーション(またはcurlなどの同等の直接リクエスト)でのみ発生します。ホームページからアプリ内をクリックしてルートをテストすることはソフトナビゲーションを実行することであり、URLを直接リクエストしてテストすることはサーバーレスポンスを実行することです。両方が重要であり、それらは一致しない可能性があります。
History APIが提供するもの
Googleの推奨は明確です。“We recommend using the History API to load
different content based on the URL in a SPA.” (翻訳) 「SPAでは、URLに基づいて異なるコンテンツを読み込むためにHistory APIを使用することを推奨します。」 (公開中のドキュメントでは「History API」がリンクになっているため、このディープリンクは導入部分を対象としています。) History API(pushState/replaceState)を使うと、ルーターはページ全体を再読み込みせずに、表示URLを/productsのような実在するブックマーク可能なパスへ変更できます。/#/productsのようなパスは使いません。これでアドレス可能性の側面が解決され、各ビューにサーバーが異なる応答を返せるURLが与えられます。
フラグメント/ハッシュURLがサーバーから見えない理由 — そしてGoogleからも
フラグメントはサーバーへ届かないため(前述)、各ルートにサーバーが実際に配信できるURLを与える方法はHistory APIだけです。Googleの基本ドキュメントでも、*“don’t use fragments to load different page content. The
following example is a bad practice, because Googlebot can’t reliably resolve the
URLs.” (翻訳) 「異なるページコンテンツを読み込むためにフラグメントを使用しないでください。次の例は、GooglebotがURLを確実に解決できないため、望ましくない方法です。」*と最低限の要件を示しています。これは私が別の記事でも書いている案内と同じです。/productsのような通常のURLを使い、/#/productsのようなハッシュURLは避けてください。GoogleはハッシュURLを確実にインデックス登録できません。
2015年の非推奨化について簡単に
ハッシュバング(#!)の時代は、Googleの2015年の投稿で終わりました:「時代は変わりました。今日では、GooglebotがJavaScriptやCSSファイルのクロールをブロックしていない限り、私たちは一般的にモダンブラウザのようにウェブページをレンダリングして理解することができます」、そして*「History APIのpushState()を使用して、より広範囲のブラウザ(および当社のシステム)へのアクセシビリティを確保できます」*。覚えておく価値のあるニュアンス:Googleは古いハッシュバングサイトを即座にインデックスから削除したわけではありません — 「私たちは一般的に#! URLをクロール、レンダリング、インデックスします」 — しかし「まだ試すことはできます」は「あなたはまだこれを行うべきです」ではありません。
各ルートを個別にインデックス可能にする
クリーンなURLと実際のHTMLがあればクロールされます。正しくインデックスされるためには、各ルートに独自のシグナルが必要です。
ルートごとのcanonicalタグ — そして「最も制限的なディレクティブ」の罠
すべてのルートには、レンダリングされたDOMに独自のrel=canonicalが必要です。罠:プレースホルダーのcanonical(またはnoindex)が生のHTMLシェルに含まれていて、JavaScriptが後でそれを上書きすることになっている場合、競合が発生する可能性があります。Googleは、生のバージョンとレンダリングされたバージョンの間の競合を、より制限的なシグナルを採用することで解決します — 以前私が述べたように、GoogleはHTMLとページのレンダリングされたバージョンの間で最も制限的なステートメントを選択します。シェルに組み込まれた迷子のnoindexや間違ったcanonicalは、JSが「修正」した後でも、ルート全体を静かに抑制する可能性があります。
JavaScriptによるルートごとのタイトルとメタディスクリプション
これらをJavaScriptで設定しても問題ありません。Googleも、“You can use JavaScript to set
or change the meta description as well as the <title> element.” (翻訳) 「JavaScriptを使用して、メタディスクリプションやtitle要素を設定または変更できます」と明記しています。必要なのは、値が一瞬表示されるだけでなく、Googleが評価するレンダリング後のDOMに残ることです。ルートの変更に応じて更新される固有のtitleとdescriptionを各ページに設定してください。
ルート間の実際の<a href>リンク
ルート間のリンクには、href属性を持つ本物のアンカーを使い、<div>のクリックハンドラーは使わないでください。Googleはhrefを抽出してURLを発見するため、ルーターで移動する<div onClick>はクローラーのリンク抽出では見えません。また、ナビゲーション間でコンテンツを引き継ぐためにクライアント側の状態へ依存しないでください。Googleのレンダラーは、“does
not retain state across page loads: Local Storage and Session Storage data are cleared
across page loads. HTTP Cookies are cleared across page loads.” (翻訳) 「ページ読み込み間で状態を保持しません。Local StorageとSession Storageのデータはページ読み込みのたびに消去され、HTTP Cookieも消去されます。」
SPAルートのサイトマップ生成
特別な「SPAサイトマップ」形式はありません — それは会計の問題です
サイトマッププロトコルはSPAでも変わりません。実際の作業は、リストするすべてのルートが実際の、一意の、レンダリングされたHTMLに解決されることを確認することです。 500のクライアント側ルートのサイトマップは、それらのルートがすべて同じシェルを返す場合、価値がありません。したがって、サイトマップはレンダリング戦略の下流にあり、その代わりではありません。
動的/パラメータ化されたルートの処理(/product/:id)
パラメータ化されたルートを持つアプリの場合、リストを手動で維持することはできません。サイトマップジェネレーターは、アプリが使用するのと同じデータソースに対して実行する必要があります — すべてのidを列挙するビルドスクリプトまたはサーバーエンドポイント — これにより、サイトマップとアプリが決して乖離しません。それらの列挙されたURLは、SSRまたはプリレンダリングされた後にのみリストする価値があります。
サイトマップをサーバーで解決可能なものと同期させる
ビルドの一部として、またはコンテンツソースに連動するスケジュールでサイトマップを再生成してください。エラーになるルート(さらに悪い場合は、200のソフトエラー)がサイトマップに残っていると、クロール予算と品質シグナルを無駄にします。
Googleが実際に見ているものをテストする
1つのURLをスポットチェックしないでください。SPA問題の要点は、ルートがブラウザでは異なるがワイヤー上では異ならない可能性があることです。したがって、生のHTMLレベルで複数のルートをテストしてください:
- ルートごとに生のHTMLを取得する。
curlを使い(必要に応じてGooglebotのユーザーエージェントを指定)、コンテンツが共有シェルではなくURLごとに固有であることを確認します。 - Search ConsoleのURL検査 — 複数のルートについてクロール後・レンダリング後のHTMLを比較し、ルートごとのtitle、description、canonicalが存在することを確認します。
- エラールートが正しいシグナルを返すことを確認する。 「見つかりません」ビューは、実際のエラーステータスへリダイレクトするか、レンダリング後のDOMに
noindexを含める必要があります。
SPA SEOに関するよくある誤解
- 「GoogleはSPAをまったくインデックス登録できない」 古い認識です。Googleは常時更新されるChromiumを実行し、通常はSPAコンテンツをレンダリングします。実際のリスクは、ソフトエラー、ハッシュルーティング、レンダリングのタイムアウト、Google以外のクローラー(Bingは信頼性が低く、多くのAIクローラーはレンダリングしない)など、より具体的です。
- 「History APIを追加すればSPA SEOは解決する」 解決するのはアドレス可能性だけです。サーバーがすべてのルートに同じシェルを返すなら、Googleは内容を見るために引き続きJavaScriptを実行する必要があります。
- 「ハッシュルーティングはフォールバックとしてまだ使える」 2015年に非推奨となり、それ以降も推奨されていません。
- 「クライアントサイドレンダリングはランキング上のペナルティになる」 CSRへの直接的なペナルティはありません。影響は間接的で、レンダリングの失敗や遅延、ソフトエラー、レンダリングのタイムアウトによる重複クラスタリングが、インデックス登録される内容を減らします。
- 「ボット向けのプリレンダリングはクローキングである」 ボットに配信する内容がユーザーに最終的に表示される内容と一致していれば、クローキングではありません。違うのはレンダリングの時点や場所であり、コンテンツではありません。
- 「特別なSPAサイトマップ生成ツールが必要である」 特別な形式はありません。必要なのは、掲載した各ルートが実際のHTMLを返すようにすることです。
FAQ
Googleはシングルページアプリケーションをインデックスできますか? はい、各ルートが実際のURLで実際の一意のHTML(SSR/プリレンダリングによる)に解決される場合です。クライアントのみのSPAはしばしばそうではありません。
SSRが必要ですか、それともクライアントサイドレンダリングで問題ないですか? CSRはランク付けする必要のないコンテンツには機能しますが、確実にインデックスしたいものにはプリレンダリングまたはSSRを使用してください — レンダラーが時間内にJSを実行することを期待しないでください。
ハッシュ(#!)ルーティングはSEOに悪いですか? はい — フラグメントはサーバーに到達しないため、GoogleはそれらのURLを確実に解決できません。History APIを使用してください。
すべてのルートに独自のcanonicalタグが必要ですか? はい、レンダリングされたDOM内で — そして、生のシェルにそれより制限的なもの(誤ったnoindexや間違ったcanonical)が含まれていないことを確認してください。
プリレンダリングサービスはクローキングですか? いいえ、ボットに提供されるコンテンツがユーザーが見るものと一致する限り。
ルーティングを自分で構築する代わりにメタフレームワークを使用すべきですか? 新しいビルドでは、通常はそうです — Next.js/Nuxt/Angular-SSR/SvelteKitはSSR/SSGとルートごとのメタデータを無料で提供します。
なぜ私のSPAは存在しないページに対して200を返すのですか? クライアントサイドルーターが仮想ナビゲーションに対して元の200ステータスを保持するためです。実際のエラーステータスへのJSリダイレクトまたはレンダリングされたnoindexで修正してください。
AIまとめ
Advancedバージョンの簡潔な見解:
- SPA SEOはクライアントサイドルーティングに特化した領域です(React Router、Vue Router、Angular Router)。SPAは1つのドキュメントを読み込み、JavaScriptでビューを切り替えます。すべてのURLへほぼ空のHTMLを送る「アプリシェル」は一般的な実装の1つであり、すべてのSPAに当てはまる規則ではありません。推測せず、直接リクエストが実際に返す内容を確認してください。
- 中心的なリスク: クライアントサイドルーティングは、サーバーの応答を変えずにユーザーに見える内容を変えられます。素のアプリシェルSPAでは、
/productsと/aboutを直接取得すると、バイト単位で同じ生のHTMLが返ることがあります。 - 混同される5つの状態: サーバーレスポンス、レンダリング後のDOM、Searchの処理、ブラウザのフルドキュメントナビゲーション、ブラウザのソフトナビゲーション。History APIによる遷移はURLとUIを変えますが、それ自体は新しいサーバーレスポンスやステータスを生成しません。
- 素のシェルに現れる3つの症状: アプリシェル問題、ソフトエラー(「見つかりません」でもルーターは
200を維持します。Googleが示す2つの対策は、それ自体がエラーを返すURLへのリダイレクト、またはJavaScriptで追加するnoindexです。初期状態のnoindexはレンダリングをスキップさせる可能性があります)、そして共有シェルの重複です。「レンダリングのタイムアウト」(Illyes)は考えられる原因の1つであり、リクエスト、レンダリング、Searchの証拠がないまま自動的に診断できるものではありません。 - ハッシュルーティングが機械的に失敗する理由:
#以降のフラグメントはサーバーへ送られないため、サーバーはルートごとに応答を変えられません。Googleは2015年に非推奨としました。History APIが提供するのはアドレス可能性であり、インデックス可能性を完全に直すものではありません。 - 素のシェルが問題なら、対策には2つの独立した側面があります: アドレス可能性(History API、ルートごとの固有URL)とコンテンツの可用性(SSR、プリレンダリング/SSG、またはメタフレームワーク)です。ただしSSRは普遍的な要件ではありません。対象ルートが検索結果に必要か、直接リクエストですでに何を返すか、どのクローラーによるレンダリングが必要かに基づいて判断します。
- ルートごとに: レンダリング後のDOMへ固有のcanonical、title、descriptionを設定します。生のHTMLにある
noindexや誤ったcanonicalがJavaScriptの設定を上書きする「より制限的なディレクティブ」の罠に注意します。本物の<a href>リンクを使い、クライアント状態に依存しません(WRSは読み込み間でストレージとCookieを消去します)。 - サイトマップ: ルートを掲載しても、そのルートが解決できる証明にはなりません。特別な形式はなく、掲載するすべてのルートが個別に実際のHTMLを返し、動的ルートはアプリのデータソースと連動する生成処理を必要とします。
- 動的レンダリングはGoogleとBingが認める一時的な回避策であり、長期的なアーキテクチャではありません。
- 複数のルートをアプリ内ナビゲーションだけでなく、生のHTMLレベル(直接リクエスト)でテストします。
公式ドキュメント
検索エンジンからの一次情報ドキュメント。
- JavaScript SEOの基本を理解する — アプリシェルモデル、「フラグメントの代わりにHistory APIを使用する」、JSによるタイトル/説明の設定。
- 検索関連のJavaScriptの問題を修正する — SPAのソフト404セクション、「URLフラグメントを使用して異なるコンテンツを読み込まない」、History APIの推奨事項。
- 回避策としての動的レンダリング — 動的レンダリングが長期的なソリューションではなく一時的な対策である理由。
- AJAXクローリングスキームの非推奨(2015年) — ハッシュバングクローリングの正式な終了とpushStateの推奨。
- サイトマップを作成して送信する — 一般的なサイトマッププロトコル(SPA固有の形式はありません)。
Bing / Microsoft
- bingbotシリーズ: JavaScript、動的レンダリング、およびクローキング。なんと。 — bingbotのJSレンダリング機能と動的レンダリングに関するスタンス。
ソースからの引用
GoogleとBingからの公式声明。各リンクは、ソースページの引用箇所にジャンプするディープリンクです。
Google — アプリシェル問題
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (翻訳) 「一部のJavaScriptサイトでは、初期HTMLに実際のコンテンツが含まれず、Googleが実際のページコンテンツを確認する前にJavaScriptを実行する必要があるアプリシェルモデルを使用する場合があります。」 引用へ移動
Google — ハッシュ/フラグメントルーティングとHistory API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (翻訳) 「SPAでは、異なるビューを読み込むためにURLフラグメント(例:example.comの/#/products)を使用する場合があります。」 引用へ移動
- “We recommend using the History API to load different content based on the URL in a SPA.” (翻訳) 「SPAでは、URLに基づいて異なるコンテンツを読み込むためにHistory APIを使用することを推奨します。」 引用へ移動 (公開中のドキュメントでは「History API」がリンクになっているため、このディープリンクは導入部分を対象としています。文全体は原文の順序どおりに掲載されています。)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (翻訳) 「異なるページコンテンツを読み込むためにフラグメントを使用しないでください。次の例は、GooglebotがURLを確実に解決できないため、望ましくない方法です。」 引用へ移動
Google — SPAのソフトエラー
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (翻訳) 「シングルページアプリケーション(SPA)では、これは特に難しい場合があります。エラーページがインデックスされるのを防ぐために、次の戦略の一方または両方を使用できます。」 引用へ移動
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (翻訳) 「SPAがクライアントサイドJavaScriptでエラーを処理すると、適切なステータスコードではなく成功ステータスを報告することがよくあります。」 引用へ移動
Google — JavaScriptによるメタタグとステートレスレンダリング
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (翻訳) 「JavaScriptを使用して、title要素だけでなくメタディスクリプションも設定または変更できます。」 引用へ移動 - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (翻訳) 「WRSはページ読み込み間で状態を保持しません。ローカルストレージとセッションストレージのデータはページ読み込み間で消去され、HTTP Cookieも消去されます。」 引用へ移動
Google — ハッシュバングクロールの非推奨化(2015年)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (翻訳) 「要するに、2009年に提案したAJAXクローリング方式はもはや推奨していません。」 引用へ移動
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (翻訳) 「時代は変わりました。今日では、GooglebotがJavaScriptやCSSファイルをクロールするのをブロックしていない限り、私たちは一般的に最新のブラウザのようにウェブページをレンダリングして理解できます。」 引用へ移動
Google — 動的レンダリングは回避策である (伝聞による引用。このサイトの広範なJavaScript SEOガイドで既に引用されている表現と一致するが、今回のパスでは独立して再検証していない)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (翻訳) 「動的レンダリングは、検索エンジンにおけるJavaScript生成コンテンツの問題に対する回避策であり、長期的な解決策ではありませんでした。」 引用へ移動
Bing — bingbotとJavaScript
- “bingbot is generally able to render JavaScript.” (翻訳) 「bingbotは一般的にJavaScriptをレンダリングできます。」 — bingbotシリーズ、Bing Webmasterブログ。 投稿を読む
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (翻訳) 「bingbotは、お気に入りの最新ブラウザでサポートされているすべてのJavaScriptフレームワークを必ずしもサポートしているわけではありません。」 — 同じ投稿。 投稿を読む
Gary Illyes(Google)— レンダリングのタイムアウトが重複を生む (LinkedInの投稿経由の伝聞。自動検証が困難なため、信頼性の高い二次情報として扱う)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (翻訳) 「中心となる要素の読み込みに時間がかかりすぎて、レンダリングがタイムアウトしました…そして、定型文しかないページが大量に残りました。定型文しかないため、それらのページは重複とみなされます。」 投稿を読む
どのレンダリングパスを選ぶべきか?
トップダウンで進めます。常に問うべきことは「クローラーが私のJSを実行せずに、このルートに対して実際のHTMLを取得できるか?」です。まず、そのルートが実際にインデックスされる必要があるかを確認し、JSなしの直接リクエストがすでに何を返すかを確認します。すべてのルートがSSRを必要とするわけではなく、一部はすでに利用可能なHTMLを返します。
1. 新規サイトを構築していますか?
- はい → メタフレームワーク(Next.js、Nuxt、SSR対応のAngular、SvelteKit、Remix)を使用します。 SSR/SSGとルートごとのメタデータが組み込まれています。ここで終了です。
- いいえ → 次へ進みます。
2. コンテンツはユーザーやリクエストごとに変わりますか?
- いいえ(ほぼ静的コンテンツ)→ ビルド時にプリレンダリング/SSGを行います。最もシンプルで堅牢な修正です。すべてのルートがディスク上の実際のHTMLになります。
- はい → サーバーサイドレンダリング(SSR) を使用して、各リクエストがルート固有のHTMLを返すようにします。
3. 今すぐSSRやプリレンダリングができない場合(レガシーな手作りのSPA)?
- 動的レンダリングを一時的な橋渡しとして使用します。クローラーにレンダリング済みのスナップショットを提供します。SSR/SSGへの移行を計画し、これを最終的な目的地と見なさないでください。
4. どの方法を選んでも、ルートごとに次の項目を確認します:
- History APIによる実際のURL(
#!フラグメントなし)。 - レンダリング後のDOMに固有のtitle、meta description、canonicalがある。
- 生のシェルに、より制限的なシグナル(
noindexや誤ったcanonical)が埋め込まれていない。 - ルート間のリンクが本物の
<a href>である。 - エラールートが実際のエラーステータスまたはレンダリング後の
noindexを返す(ソフトエラーがない)。 - サイトマップ内のすべてのルートが、それぞれ実際のHTMLを返す。
SPA SEOチェックリスト
アドレス可能性
- 各ビューがHistory APIを介した実際のURL(
/products)を持ち、フラグメント(/#/products)ではない。 - ルート間のリンクが実際の
<a href>アンカーであり、<div onClick>ハンドラーではない。
コンテンツの可用性
- すべてのルートが、クローラーがJSを実行しなくても実際の一意のHTMLを返す(SSR、プリレンダリング/SSG、またはメタフレームワーク)。
- ルートコンテンツがレンダリングウィンドウが閉じる前に読み込まれる(定型文のみの重複を避ける)。
ルートごとのインデックス可能性
- ルートごとに一意の
<title>とメタディスクリプションが、レンダリングされたDOMに存在する。 - ルートごとに一意の
rel=canonicalが、レンダリングされたDOMに存在する。 - 生のシェルに、より制限的なシグナルが固定されうるような、迷子の
noindexやプレースホルダーのcanonicalがない。
エラー
- 「Not found」ルートは、サーバーが実際のエラーステータスを返すURLへリダイレクトするか、JavaScriptで追加された
noindexを持つ(200のソフトエラーがない)。 -
noindexを使う場合は、最初の描画から存在するのではなく、アプリがルートを無効と判断した後に追加されることを確認する。初期状態のnoindexは、Googleがページのレンダリングをスキップする原因になり得る。
サイトマップ
- リストされた各ルートが、独立して実際のHTMLに解決される。
- 動的ルート(
/product/:id)は、アプリのデータソースから列挙され、手動で管理されていない。
検証
- 生のHTMLが複数のルートにわたってチェックされ(curl)、URLごとに一意のコンテンツが確認されている。
- URL Inspectionが、ルートごとにレンダリングされたコンテンツ、タイトル、ディスクリプション、canonicalを確認している。
メンタルモデル
1. 混同してはいけない2つの側面。 アドレス可能性(History API、一意のURL、フラグメントなし)とコンテンツの可用性(SSR、プリレンダリング、またはメタフレームワーク)は独立している。ルートごとのHTMLがないきれいなURL = 空のシェルの整ったサイトマップ。両方が必要。
2. 「サーバーは、新しく何を返すか?」
SPAの問題全体は、ブラウザのビューがサーバーのレスポンスから乖離すること。任意のルートについて、JS実行なしのcurlが何を取得するかを問う。それがシェルなら、クローラーもシェルを見るかもしれない。
3. ステータスコードのデフォルトは200 — エラーも含めて。
クライアントサイドルーターは元の200を維持する。意図的に実際のエラーステータスまたはレンダリングされたnoindexを返すようにしない限り、すべての「エラー」ビューはソフト404と想定する。
4. 最も制限的なディレクティブの罠。
Googleは、より制限的なシグナルを採用することで、生のHTMLとレンダリングされたHTMLを調整する。シェル内のnoindexや誤ったcanonicalは、JSの「修正」を上書きできる。レンダリングされたDOMだけでなく、生のHTMLを監査する。
5. 新規ビルドにはメタフレームワークを優先。 クライアントサイドルーティングを手作りすることは、これらの問題のすべてを自分で抱えることを意味する。SSR/SSGとルートごとのメタデータを行うフレームワークを選ぶことは、それらを持たないことを選ぶこと。
SPA SEO チートシート
ルーティング
| アプローチ | URL例 | サーバーに到達するか? | Googleセーフか? |
|---|---|---|---|
| History API | /products | はい | はい(推奨) |
| ハッシュルーティング | /#/products | いいえ(フラグメントは送信されない) | いいえ — 2015年に非推奨 |
ハッシュバング(#!) | /#!/products | いいえ | いいえ — レガシーAJAXスキーム、非推奨 |
レンダリング戦略
| 戦略 | クローラーはJSを実行せずに実際のHTMLを取得できるか? | 最適な用途 |
|---|---|---|
| SSR | はい | ユーザーごと / リクエストごとのコンテンツ |
| プリレンダリング / SSG | はい | ほぼ静的なコンテンツ |
| メタフレームワーク(Next/Nuxtなど) | はい(組み込み) | 新規ビルド |
| 動的レンダリング | はい、ボットのみ | レガシーSPAの一時的な橋渡し |
| ベアCSR(クライアントのみ) | いいえ | 確実にインデックスさせたいものには不向き |
ルートごとの必須事項(すべてレンダリングされたDOM内)
- 一意のURL(History API)・一意の
<title>・一意のメタディスクリプション・一意のrel=canonical・実際の<a href>リンク・実際のステータスまたはnoindexを持つエラールート。
要点
#以降のフラグメントはサーバーへ送信されません。これがハッシュルーティングが失敗する理由です。- クライアントサイドルーターは「not found」を含むすべてで
200を維持します → ソフトエラーになります。 - Googleは生のHTMLとレンダリング後のHTMLを照合し、より制限的なシグナルを採用します。
- SPA専用のサイトマップ形式はありません。掲載した各ルートが実際のHTMLを返す必要があります。
SPA SEO アンチパターン
ベアCSR SPAを配信し、完全なサイトマップを提出する。 サイトマップには500ルートがリストされているが、新しいフェッチでは500すべてが同じシェルを返す。サイトマップはコンテンツを存在させるわけではない — レンダリングが存在させる。
History APIを追加して完了とする。 きれいなURLはアドレス可能性を修正するが、コンテンツは修正しない。SSR/プリレンダリングなしでは、サーバーはすべてのルートに対してシェルを返し続ける。
ハッシュ / ハッシュバングルーティングを「フォールバック」として使う。 フラグメントはサーバーに届かないため、サーバーはルートごとに応答を変えられません。2015年以降非推奨。フォールバックではなく、行き止まりです。
<div onClick>でナビゲーションし、<a href>を使わない。
Googleはhrefを抽出してURLを発見します。クリックハンドラーによるナビゲーションはリンク抽出から見えないため、そのルートが発見されない可能性があります。
生のシェルに「JS が上書きする」というプレースホルダーの noindex や canonical を置く。
Google は生のHTMLとレンダリング後のHTMLのうち、より制限的な方を採用します。シェルのディレクティブが勝って、ルートを静かに抑制する可能性があります。
「見つかりません」ビューで200を返す。
ソフトエラーによって、内容の薄いページや空のページがインデックス登録されます。実際のエラーステータスへリダイレクトするか、レンダリング後のnoindexを追加してください。
ボイラープレートの後に、ルートのコンテンツをゆっくり読み込む。 レンダリングのタイムアウトにより共有シェルのみが残り、すべてのルートが他のルートの重複に陥ります(Illyes)。中心となるコンテンツを最初に読み込みましょう。
クライアント状態に依存してナビゲーション間でコンテンツを保持する。 レンダラーはページ読み込みのたびに Local/Session Storage と Cookie をクリアします。クライアント状態にのみ存在するコンテンツは、Google が次のルートをレンダリングするときには存在しません。
一般的な SPA インデックス問題
すべてのルートが同じ HTML を返す
症状: /products と /about はブラウザでは異なるビューですが、生のレスポンスは同一です。考えられる原因: History API ルーティングは、SSR やプリレンダリングなしでアドレス可能性を提供します。修正: ルート固有の HTML を生成し、各パスへの直接リクエストに独自の見出し、本文コピー、メタデータが含まれることを確認してください。
存在しないルートが成功ページとして表示される
症状: 存在しないルートが 200 を返しながら「見つかりません」ビューを表示します。考えられる原因: サーバーが成功シェルを送信した後、クライアントルーターがエラーを処理します。修正: 正しいサーバーステータスを返してください。それがまだ配信できない場合は、実際のエラーステータスの URL にリダイレクトするか、noindex をレンダリングしてください。アプリ内ナビゲーションではなく、新しい直接リクエストで確認してください。
レンダリング後にルートが重複に陥る
症状: 検索エンジンが異なる URL をクラスタリングするか、共有ナビゲーションのみを保持します。考えられる原因: ルートコンテンツの読み込みが遅すぎて、レンダリングがボイラープレートを取得します。修正: サーバーレスポンスまたは最も早いレンダリングパスで主要コンテンツを優先してください。オプションのスクリプトが完了する前に、複数のルートが一意のコンテンツを公開していることを確認してください。
サーバーがルートごとに実際に返すものをテストする
SPA の罠は、ルートがブラウザでは異なるが、ワイヤー上では異ならないことです。1つだけでなく、複数のルートを生の HTML レベルで確認してください。
ルートごとに生のHTMLを取得する(macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
done2つのルートの生の HTML を差分比較する(同じシェルか?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"生のレスポンスでルートごとのタイトルと canonical を確認する(grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'「見つかりません」ルートのHTTPステータスを確認する(ソフト404検出)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existブラウザのDevToolsコンソールで — レンダリングされたtitle/canonicalを検査する
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);ブックマークレット — 実際のアンカーではないクリックハンドラーの「リンク」をフラグする
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath — クローラーが解決できないフラグメント/ハッシュリンクを見つける(DevToolsコンソールに貼り付け)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. SPA監査のためのツール
- URL Inspection (Google Search Console) — ルートのクロール済みHTMLとレンダリング済みHTMLを確認し、ルートごとのタイトル、説明、正規URLが実際に反映されていることを確認します。
- リッチリザルトテスト / URL Inspectionの「クロール済みページを表示」 — 単一URLに対するGoogle自身のレンダリングで、コンテンツがレンダリング後も残っているかを確認するのに役立ちます。
- Googlebot UAを使った
curl— 複数のルートの生のHTMLを比較し、共有シェルを検出する最速の方法です。 - Screaming Frog SEO Spider(JavaScriptレンダリングモード) — レンダリングありとなしでサイトをクロールし、すべてのルートで生のコンテンツとレンダリング済みコンテンツ、タイトルを比較します。
- Ahrefs Site Audit — ルート全体でインデックス可能性の問題、欠落・重複したタイトルと正規URL、ソフト404のようなパターンを明らかにします。
- DebugBear — SPA向けのパフォーマンス監視。レンダリングタイミングが重要です。遅いルートはタイムアウトして定型の重複になる可能性があるためです。
- Bing Webmaster Tools — URL Inspection — bingbotはGooglebotほど一貫してJSをレンダリングしないため、Bing側でもルートを確認してください。
SPAルートが独立してインデックス可能であることを証明する
直接アクセス時のHTML同等性をテストする
実行するテスト: 新しいセッションで代表的なルートを開き、同じURLをcurlで取得します。期待される結果: 各URLが、事前のアプリ状態なしで独自の主要コンテンツとヘッドシグナルを返します。失敗の解釈: ルートがクライアントナビゲーションまたは保存された状態に依存しています。監視期間: 即時。ロールバックのトリガー: ルートがホームページ経由でのみ機能する場合。
エラーステータスの処理をテストする
実行するテスト: 既知の無効なルートを直接リクエストし、ステータスとレンダリング後のディレクティブを確認します。期待される結果: 実際のエラーステータス、または文書化されたフォールバックであるレンダリング後のnoindexやエラーレスポンスへのリダイレクト。失敗の解釈: SPAがソフトエラーを生成しています。監視期間: 即時。ロールバックの条件: 無効なパスがインデックス可能な200ページとして配信される。
メタデータの分離をテストする
実行するテスト: 少なくとも3つのルートで、生のタイトルとレンダリングされたタイトル、robotsタグ、正規URLを比較します。期待される結果: 各ルートに、意図された一貫性のあるセットが1つあること。失敗の解釈: 共有シェルがルート間でメタデータを漏らしているか、JSが遅すぎて上書きしています。監視期間: ローカルでは即時、URL Inspectionでの再クロール後も確認。ロールバックのトリガー: いずれかのルートが別のルートの正規URLまたは制限的なシェルディレクティブを継承する場合。
自分でテスト: SPA SEO
シングルページアプリケーションをクロール可能かつインデックス可能にするための5つの簡単な質問です。各質問に回答を選び、確認してください。
時間をかける価値のあるリソース
関連記事
- JavaScript SEOの課題とベストプラクティス — 私の完全なJS-SEOガイド。この記事が基盤としている「URLにフラグメントを使わない」やアプリシェル/重複コンテンツのセクションを含みます。
- 技術的SEOの初心者向けガイド — レンダリングとクロールが全体像の中でどのように位置づけられるか。
講演資料
- 検索の仕組み (SlideShare) — クロール、レンダリング、インデックス、ランキングの流れを解説したもので、SPAが生き残らなければならないパイプラインです。(私の常套の免責事項が適用されます:「これは私のシステム理解に基づくもので…100%完全または正確ではありません。」)
業界からの情報
- Google — JavaScript SEOの基本を理解する — アプリシェルモデルと「フラグメントの代わりにHistory APIを使用する」。
- Google — 検索関連のJavaScriptの問題を修正する — SPAのソフト404セクションとHistory APIの推奨事項。
- Google — AJAXクロールスキームの非推奨化(2015年) — ハッシュバングクロールの正式な終了。
- Bing — bingbotシリーズ:JavaScript、動的レンダリング、クローキング。なんと! — bingbotのJSレンダリングに関する見解。
- SPA開発で空白のHTMLページにプリレンダリングサービスを使用することはクローキングではない (Search Engine Roundtable、2015年) — SPAのプリレンダリングがクローキングではないというGary Illyesの発言の報道。 (SERによるIllyesの発言の要約、2015年 — 二次情報。)
- シングルページアプリケーションのSEO (Nuxt SEO) — 同じ問題に対するフレームワーク側の解説。
- SEOのためにシングルページアプリケーションを最適化する方法 (DebugBear) — SPAのインデックス可能性におけるレンダリング/パフォーマンスの観点。
- SPA(用語集) (MDN) — シングルページアプリケーションパターンの中立的な定義。
動画
- Google Search Central (YouTube) — Martin SplittのJavaScript SEOシリーズでは、レンダリング、History API、ここで説明されているSPAの失敗モードについて解説しています。 チャンネル
変更履歴
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月20日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年8月3日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。
2026年7月18日に更新。
編集概要と記録された変更の詳細。変更の詳細
-
変更の詳細な注記は現在英語でのみ提供されています。
-
変更の詳細な注記は現在英語でのみ提供されています。
-
変更の詳細な注記は現在英語でのみ提供されています。
-
変更の詳細な注記は現在英語でのみ提供されています。
-
変更の詳細な注記は現在英語でのみ提供されています。
完全な比較は利用できません — この改訂の以前のスナップショットがアーカイブされていません。