JavaScript SEO の基本を理解する

JavaScript には、ウェブを強力なアプリケーション プラットフォームに変える多くの機能が用意されており、ウェブ プラットフォームを支える重要な要素となっています。JavaScript を利用するウェブアプリを Google 検索で見つかりやすいようにすることで、ウェブアプリによるコンテンツを検索するユーザーを新たに開拓し、既存のユーザーにもあらためてアプローチできるようになります。 Google 検索では、JavaScript を最新バージョンの Chromium で実行しますが、最適化できる項目がいくつかあります。

このガイドでは、Google 検索で JavaScript が処理される仕組みについて説明し、Google 検索に対して JavaScript ベースのウェブアプリを最適化するためのヒントを紹介します。

Googlebot が JavaScript を処理する仕組み

Googlebot は、JavaScript ベースのウェブアプリを 3 つの主要なフェーズで処理します。

  1. クロール
  2. レンダリング
  3. インデックス登録

Googlebot はページのクロール、レンダリング、インデックス登録を行います。

Googlebot はクロールキューから URL を取り出し、URL をクロールして処理ステージに渡します。処理ステージでは、リンクを抽出してクロールキューに戻し、ページをレンダリング キューに入れます。ページはレンダリング キューからレンダラに渡され、レンダラはレンダリングした HTML を処理ステージに戻します。処理ステージではコンテンツをインデックスに登録し、リンクを抽出してクロールキューに入れます。

Googlebot は、HTTP リクエストを発行してクロールキューから URL を取得すると、クロールが許可されているかどうかを最初に確認します。そのために Googlebot は robots.txt ファイルを読み取ります。URL がクロール禁止対象としてマークされている場合、Googlebot はその URL に対して HTTP リクエストを実行せず、URL をスキップします。

次に、Googlebot は HTML リンクの href 属性内の他の URL についてレスポンスを解析し、URL をクロールキューに追加します。リンクの検出を回避するには、nofollow メカニズムを使用します。

URL のクロールと HTML レスポンスの解析は、HTTP レスポンス内の HTML にすべてのコンテンツが含まれている従来型のウェブサイトやサーバー側でレンダリングされるページでは、問題なく実行できます。一方、一部の JavaScript サイトでは、App Shell モデルを使用していることがあります。この場合、最初は HTML に実際のコンテンツが含まれておらず、Googlebot が JavaScript の生成する実際のページ コンテンツを参照するには JavaScript を実行する必要があります。

robots メタタグまたはヘッダーによってページのインデックス登録が禁止されていなければ、Googlebot はすべてのページをレンダリングするためキューに入れます。ページはこのキューで数秒間(またはそれ以上)待機することがあります。Googlebot のリソースに空きができると、ヘッドレスの Chromium がページをレンダリングして JavaScript を実行します。Googlebot はレンダリングされた HTML のリンクを再度解析し、見つかった URL をクロールするためキューに入れます。また、Googlebot はレンダリングされた HTML を使用してページをインデックスに登録します。

ボットによっては JavaScript を実行できないものもあり、ユーザーやクローラに対してウェブサイトを高速化する効果もあるので、サーバー側でのレンダリング(プリレンダリング)も有効な方法として検討してみてください。

独自のタイトルとスニペットを持つページを作成する

タイトルを内容が伝わりやすい独自のものにし、わかりやすいメタ ディスクリプションを付けると、ユーザーが目的とする結果を見つけやすくなります。タイトルと説明の作成に関するガイドラインを参考にしてください。

タイトルとメタ ディスクリプションは、JavaScript で設定または変更できます。

互換性のあるコードを作成する

ブラウザでは多くの API が提供されています。また、JavaScript は急速に進化している言語であることから、Googlebot のサポートする API と JavaScript の機能には一定の制約があります。Googlebot と互換性があるコードを使用するには、JavaScript の問題のトラブルシューティングに関するガイドラインに従ってください。

有意な HTTP ステータス コードを使用する

Googlebot は、ページをクロールする際に問題が発生したかどうかを確認するために、HTTP ステータス コードを使用します。

ページに対するクロールやインデックス登録を許可しない場合は、それを Googlebot に伝えるために、有意なステータス コードを使用する必要があります。たとえば、ページが見つからなかったことを示す 404 や、ログインできないことを示す 401 などのコードがあります。また、HTTP ステータス コードを使用して、ページが新しい URL に移動されたことを Googlebot に通知することもできます。そうすることで、インデックスが随時更新されます。

次のリストは、HTTP ステータス コードと、その用途をまとめたものです。

HTTP ステータス 用途
301 / 302 ページが新しい URL に移動されている。
401 / 403 権限の問題によりページを参照できない。
404 / 410 ページが利用不可になった。
5xx サーバー側で問題が発生した。

シングルページ アプリでソフト 404 エラーを回避する

クライアント側でレンダリングされたシングルページ アプリでは多くの場合、ルーティングはクライアント側ルーティングとして実装されます。 この場合、有意な HTTP ステータス コードの使用は不可能か、実用的でないことがあります。 クライアント側でレンダリングやルーティングを行う場合にソフト 404 エラーを回避するには、次のいずれかを実施します。

  • サーバーが 404 HTTP ステータス コード(/not-found など)を返す URL にリダイレクトする JavaScript を使用する。
  • JavaScript を使用してエラーページに <meta name="robots" content="noindex"> を追加する。

リダイレクトを行う場合のサンプルコードを次に示します。

fetch(`/api/products/${productId}`)
.then(response => response.json())
.then(product => {
  if(product.exists) {
    showProductDetails(product); // shows the product information on the page
  } else {
    // this product does not exist, so this is an error page.
    window.location.href = '/not-found'; // redirect to 404 page on the server.
  }
})

noindex を使用する場合のサンプルコードを次に示します。

fetch(`/api/products/${productId}`)
.then(response => response.json())
.then(product => {
  if(product.exists) {
    showProductDetails(product); // shows the product information on the page
  } else {
    // this product does not exist, so this is an error page.
    // Note: This example assumes there is no other meta robots tag present in the HTML.
    const metaRobots = document.createElement('meta');
    metaRobots.name = 'robots';
    metaRobots.content = 'noindex';
    document.head.appendChild(metaRobots);
  }
})

フラグメントの代わりに History API を使用する

Googlebot がページ内のリンクを探すときには、HTML リンクの href 属性内の URL だけが対象になります。

クライアント側ルーティングを使用するシングルページ アプリケーションでは、History API を使用して、ウェブアプリのビュー間にルーティングを実装します。Googlebot がリンクを確実に見つけられるように、フラグメントを使用して別のページ コンテンツを読み込むことは避けてください。 次に示す方法は、Googlebot がリンクをクロールしないため適切ではありません。

<nav>
  <ul>
    <li><a href="#/products">Our products</a></li>
    <li><a href="#/services">Our services</a></li>
  </ul>
</nav>

<h1>Welcome to example.com!</h1>
<div id="placeholder">
  <p>Learn more about <a href="#/products">our products</a> and <a href="#/services">our services</p>
</div>
<script>
window.addEventListener('hashchange', function goToPage() {
  // this function loads different content based on the current URL fragment
  const pageToLoad = window.location.hash.slice(1); // URL fragment
  document.getElementById('placeholder').innerHTML = load(pageToLoad);
});
</script>

代わりに History API を実装することで、Googlebot がリンク URL に確実にアクセスできるようになります。

<nav>
  <ul>
    <li><a href="/products">Our products</a></li>
    <li><a href="/services">Our services</a></li>
  </ul>
</nav>

<h1>Welcome to example.com!</h1>
<div id="placeholder">
  <p>Learn more about <a href="/products">our products</a> and <a href="/services">our services</p>
</div>
<script>
function goToPage(event) {
  event.preventDefault(); // stop the browser from navigating to the destination URL.
  const hrefUrl = event.target.getAttribute('href');
  const pageToLoad = hrefUrl.slice(1); // remove the leading slash
  document.getElementById('placeholder').innerHTML = load(pageToLoad);
  window.history.pushState({}, window.title, hrefUrl) // Update URL as well as browser history.
}

// Enable client-side routing for all links on the page
document.querySelectorAll('a').forEach(link => link.addEventListener('click', goToPage));

</script>

robots メタタグを使用する場合の注意点

robots メタタグは、Googlebot によるページのインデックス登録やリンクの参照を回避するために使用できます。 たとえば、ページの上部に次のメタタグを追加すると、Googlebot によるページのインデックス登録がブロックされます。

<!-- Googlebot won't index this page or follow links on this page -->
<meta name="robots" content="noindex, nofollow">

JavaScript を使用して、robots メタタグをページに追加したり、robots メタタグの内容を変更したりできます。次のコード例は、API 呼び出しでコンテンツが返されない場合は現在のページのインデックスが登録されないように、JavaScript で robots メタタグを変更する方法を示しています。

fetch('/api/products/' + productId)
  .then(function (response) { return response.json(); })
  .then(function (apiResponse) {
    if (apiResponse.isError) {
      // get the robots meta tag
      var metaRobots = document.querySelector('meta[name="robots"]');
      // if there was no robots meta tag, add one
      if (!metaRobots) {
        metaRobots = document.createElement('meta');
        metaRobots.setAttribute('name', 'robots');
        document.head.appendChild(metaRobots);
      }
      // tell Googlebot to exclude this page from the index
      metaRobots.setAttribute('content', 'noindex');
      // display an error message to the user
      errorMsg.textContent = 'This product is no longer available';
      return;
    }
    // display product information
    // ...
  });
    

Googlebot は、JavaScript 実行前に robots メタタグ内で noindex を見つけた場合、ページのレンダリングとインデックス登録を行いません。

長期保存キャッシュを使用する

Googlebot は、ネットワーク リクエストとリソース使用量を減らすために、積極的にキャッシュに保存しますが、WRS はキャッシュ ヘッダーを無視することがあります。このため、WRS は古い JavaScript や CSS リソースを使用するおそれがあります。この問題は、ファイル名の一部をコンテンツのフィンガープリントにすることで回避できます(main.2bb85551.js など)。フィンガープリントはファイル コンテンツによって異なるため、更新するたびに別のファイル名が生成されます。詳細については、長期保存キャッシュを使用する際の web.dev ガイドをご覧ください。

構造化データを使用する

ページで構造化データを使用する場合は、JavaScript を使用して必要な JSON-LD を生成し、ページに挿入できます。問題を避けるために、必ず実装をテストしてください。

ウェブ コンポーネントに関するおすすめの方法に準拠する

Googlebot はウェブ コンポーネントをサポートしています。Googlebot がページをレンダリングする際、Shadow DOM と Light DOM のコンテンツがフラット化されます。つまり、Googlebot はレンダリングされた HTML に表示されるコンテンツのみを認識できます。レンダリング後に Googlebot がコンテンツを引き続き認識できるようにするには、モバイル フレンドリー テストまたは URL 検査ツールを使用して、レンダリングされた HTML を確認します。

レンダリングされた HTML にコンテンツが表示されない場合は、Googlebot はそのコンテンツをインデックスに登録できません。

次の例は、Shadow DOM 内に Light DOM コンテンツを表示するウェブ コンポーネントを作成します。レンダリングされた HTML に Light DOM と Shadow DOM の両方のコンテンツが表示されるようにする 1 つの方法は、スロット要素を使用することです。

<script>
  class MyComponent extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
      let p = document.createElement('p');
      p.innerHTML = 'Hello World, this is shadow DOM content. Here comes the light DOM: <slot></slot>';
      this.shadowRoot.appendChild(p);
    }
  }

  window.customElements.define('my-component', MyComponent);
</script>

<my-component>
  <p>This is light DOM content. It's projected into the shadow DOM.</p>
  <p>WRS renders this content as well as the shadow DOM content.</p>
</my-component>
            

レンダリング後、Googlebot はこのコンテンツをインデックスに登録します。

<my-component>
  Hello World, this is shadow DOM content. Here comes the light DOM:
  <p>This is light DOM content. It's projected into the shadow DOM<p>
  <p>WRS renders this content as well as the shadow DOM content.</p>
</my-component>
    

画像と遅延読み込みされるコンテンツを修正する

画像があると、帯域幅とパフォーマンスにかなりの負荷がかかることがあります。ユーザーが画像を表示しようとしたときにだけ、画像を遅延読み込みする方法をおすすめします。遅延読み込みを実装して検索されやすくするには、遅延読み込みのガイドラインに従ってください。