📚

「Google 検索セントラル」から学ぶ、ページネーションを実装する前に検討しておくこと

2024/07/22に公開

プロジェクトでページネーションまわりの改修を担当した時に「Google 検索セントラル」やその他の記事を読んで、知ったことや考えたことをまとめてみました。
「どのようなライブラリを使うおうか」と調べ始める前に、知っておくべき前提知識を収集しておくのは大切だなと気づいたので、この記事がその一助となれば嬉しいです。

ページネーションのUI

最初と最後のリンクを常に表示する

各ページに最初のページへ戻るリンクを設定し、Google に対して一連のページの始点を示すこともおすすめします。これにより、ページ列中で最初のページが他のページよりもリンク先ページとして適しているというヒントを Google に与えることができます。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

UIに関しては、最初と最後のリンクを常に表示することだけでした。

ページネーションのUIを調べると色々なパターンが出てきますが、最初と最後のリンクを置いているデザインは少ない気がします。
とはいえ、Googleも「示すこともおすすめします」のようにゆるい推奨って感じですし、Google検索もそういうUIになっていなかったです。

UIを決めるときの判断材料の一つとして使えればいい、くらいのニュアンスで捉えておくくらいでいいかもしれません。

ページネーションのHTML

<a href>をページの読み込み時に出力する

Googleにリンクをクロールさせたい場合は<a href>で記述しておく必要があります。

Google は、サイトをクロールしてインデックス登録するページを見つける際に、HTML の <a href> タグでマークアップされたページリンクのみをたどります。Google のクローラは(<a href> でマークアップされている場合を除き)ボタンをたどらず、また JavaScript を実行して現在のページ コンテンツを更新することも行いません。
検索エンジンにページ分けされたコンテンツのページ間の関係を認識させるには、<a href> タグを使用して各ページに次のページへのリンクを追加します。そうすることで、Googlebot(Google のウェブクローラ)が次のページを見つけやすくなります。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

通常、リンクが href 属性を持つ <a> HTML 要素(アンカー要素とも呼ばれます)である場合のみ、Google はリンクをクロールできます。その他のフォーマットのリンクのほとんどは解析されず、Google クローラーにより抽出されません。href 属性のない <a> 要素や、スクリプト イベントによりリンクとして機能するタグから URL を信頼できる形で抽出することはできません。以下に、Google が解析できるリンク、解析できないリンクの例を示します。

Google の SEO リンクに関するベスト プラクティス | Google 検索セントラル  |  ドキュメント  |  Google for Developers

また、JavaScriptを使って動的にHTMLを出力するとクローリングされません。
クローラーはクリックもスクロールもせず、ページ内で発見したリンクをもとにサイトマップなどを参照しながらそのURLに対してクロールするからです。

ページの読み込み時にdisplay: none;visibility: hidden;といった方法で隠しているコンテンツはインデックスされたり評価対象に含まれるのかも気になるところです。
いくつかの記事を探してみましたが、次のように捉えておくのがよさそうです。

  • CSSで非表示になったコンテンツであっても、Googleはインデックスしていることが確認された
    • CSSのプロパティに関係なく、コンテンツを隠しているかの観点で捉えればいい
  • レスポンシブではUIを調整するために一時的に隠すことは一般的なので、それ自体がスパムとみなされる可能性は低い
  • 評価を不当に高めようとする行為に対してスパムとみなされる(基準は不明)
  • 隠された時点でそのコンテンツの評価は低くみなされる可能性があるので、重要なコンテンツは隠さずに表示しておく

こちらの記事を参考にしました。

最終的に次のような点に注意してHTMLとCSSで実装する必要があります。

  • buttonタグではなく<a href>を使う
  • ページの読み込み時点でHTMLとして出力しておく
  • CSSで非表示にしていてもいいが、評価が下がりやすいので最初から表示しておく

タイトルとディスクリプションは同じでも問題ない

通常は、各ウェブページを区別できるように、別々のタイトルを付けることをおすすめしていますが、ページ分けされた一連のページについては、これに従う必要はありません。一連のページでは同じタイトルと説明を使用できます。Google は順番に続いているページを認識し、それに応じてページをインデックスに登録します。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

タイトルに「(2ページ目)」のように指定することもあると思いますが「指定がなくても関係性を把握してインデックスするよ」という意味なのかなと思います。
なので、「補足情報として載せられるなら載せておきたい」とか、「ブラウザのタブに表示させる」などの別の意図があるならそのまま指定しておくのもいいかもしれません。

rel="canonical"はページごとに設定する

ページ分けされたページ列の最初のページを正規ページとして使用しないでください。代わりに、固有の正規 URL を各ページに付与してください。

たとえばhttps://example.com/articles?page=2というURLの場合に<link rel="canonical" href="https://example.com/articles">と記述することが多いと思いますが、<link rel="canonical" href="https://example.com/articles?page=2">のようにcanonicalを設定する必要があります。
後述しますが、Googleはクエリパラメーターを含めて別のURLだと判断するためです。

canonicalをクエリパラメーターなしで記述する理由としては、内容の薄いページを増やしてサイト評価を下げたくない、あるいは最初のページに評価を集約させたいといったものがあると思います。
しかし、「使用しないでください」と書かれているので、URLとcanonicalは同じにしておくのがいいかもしれません。Googleとしては効率的にクローリングしたいはずなので、それに沿ったやり方をして評価が不当に下げられるようにはしないと思います。

ただ、page=1はクエリパラメーターなしの状態と同じ情報なので、canonicalをクエリパラメーターなしで統一しておくのがいいと思います。

内部リンク、サイトマップ ファイル、<link rel="canonical"> タグで同じ URL を使用します。たとえば、一連のぺージネーションの 1 ページ目がデフォルトのページで、クエリ パラメータを使用して最初のページへのリンクを設置する場合、URL に ?page=1 を含めるかどうかをサイト全体でどちらかに統一します。

EC サイトの URL 構造 ベスト プラクティス | Google 検索セントラル  |  ドキュメント  |  Google for Developers

サイト内検索に関してはサイト側が事前にURLをコントロールできるものではないので、canonicalをhttps://example.com/searchなどに固定したほうがいいかもしれません。

rel="next"rel="prev"は必須ではない

これまで、Google は <link rel="next" href="..."> と <link rel="prev" href="..."> を使用して次のページや前のページという関係を認識していましたが、今後はこれらのタグを使用しません。ただし、他の検索エンジンでは引き続き使用する可能性があります。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

「あくまでもGoogleは」という前提ですが、rel="next"rel="prev"を使用していないとうことなので、SEOとしての優先度は低くなっていると思います。

ユーザビリティとアクセシビリティの観点では記述しておくメリットはあるのかもしれません。調べても具体的な例が見つからなかったので、rel="next"rel="prev"を使用する具体的なメリットがご存知の方がいらっしゃったら教えていただけると嬉しいです。
ただし、適切なマークアップをしておかないとユーザーを混乱させる原因になってしまうので注意は必要そうです。

パラメーター

ハッシュではなくクエリを使う

各ページに一意の URL を割り当ててください。たとえば、ページ分けされたページ列内の各 URL に「?page=n」というクエリ パラメータを付加すると、Google は各 URL を別々のページの URL として扱います。
ページ番号に URL フラグメント識別子(URL で「#」の後に続くテキスト)を使用しないでください。Google はフラグメント識別子を無視します。Googlebot は、次のページへの URL を認識しても「#」の後のテキストしか異なっていない場合は、取得済みのページだと判断してリンクをたどらない可能性があります。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

ほとんどの場合で#を使うことはないと思うので、page=2のようなクエリパラメーターを引き続き使います。

重要なのは「Google はフラグメント識別子を無視します」の部分ですね。
ハッシュ(フラグメント識別子)を使う場面としてページ内リンクが思い浮かびますが、そういった用途がクローリングに影響しなかったことから無視をする判断になっていったのかなと考えています。

調べてわかった違いとしては、クエリパラメータはサーバーにリクエストする URL にクエリパラメータの値も含めるが、ハッシュパラメータだと含まれないということでした。

[Frontend] クエリパラメータとハッシュの区別

と書かれた記事もあり、サーバー側では使用できないハッシュの使用を避けている可能性もありそうです。

無効な値が入力された時のハンドリングをする

これはGoogleの公式ドキュメントに記載されている内容ではありませんが、事前に決めておく必要があると思います。

具体的にはpage=0のようなシステムとして表示できない値や、page=999999のようなデータとして存在しない値が入力された場合です(数値以外も考慮が必要ですが、今回は割愛します)。
考えられる方法として2つありそうです。

  1. 404ページを表示する
  2. 表示可能なページを表示する

1. 404ページを表示する

システムとして適切な挙動です。

ただ、ユーザーはなぜページが正常に表示されていないか判断できない可能性があります。
「なぜ表示されていないのか」「どのようなことを試すと解消されるのか」といった情報を404ページに出しておくのが大切そうです。
たとえば以下のようにDocument.referrerからURLを取得して、パスに応じてヒントを出し分ける実装が考えられます。

'use client';

import { useEffect, useState } from 'react';

export default function ReferrerDisplay() {
  const [referrer, setReferrer] = useState('');

  useEffect(() => {
    setReferrer(document.referrer);
  }, []);

  const reason = (referrer: string) => {
    // `referrer`が`/items/`を含む場合にヒントを出す
    if (referrer.includes('/items/')) {
      return (
        <ul>
          <li>表示できなかった理由①</li>
          <li>表示できなかった理由②</li>
          <li>表示できなかった理由③</li>
        </ul>
      );
    }
    return null;
  };

  return referrer ? (
    <>
      <p>アクセスしようとしたページは表示できませんでした。</p>
      {reason(referrer)}
    </>
  ) : null;
}

2. 表示可能なページを表示する

404ページのような無効なページではなく、コンテンツが表示できるページを見せたい場合もあるかもしれません。
ただし、誤った値で有効なページが表示されることにユーザーが気づけなくなることもありそうです。
条件としては次の2つが考えられます。

  1. 1ページ目を表示する
  2. 値が0以下なら1ページ目、表示可能な最大ページ数を超えていたら最後のページを表示する

1.の「1ページ目を表示する」の場合は次のような実装が考えられます。

import { redirect } from 'next/navigation';

type PageData = {
  totalCount: number;
  perPage: number;
  currentPage: number;
  basePath: string;
};

/**
 * 現在のページが有効な範囲内にあるかをチェックして、無効な場合は1ページ目にリダイレクトします。
 * @param {PageData} data - ページネーションに関するデータ
 * @param {number} [data.totalCount] - 総アイテム数
 * @param {number} data.perPage - 1ページあたりのアイテム数
 * @param {number} data.currentPage - 現在のページ番号
 * @param {string} data.basePath - ベースとなるURLパス
 * @example
 * paginationGuard({
 *   totalCount: 100,
 *   perPage: 10,
 *   currentPage: 2,
 *   basePath: '/articles',
 * });
 * @example
 * // `page`以外のパラメーターがある場合
 * paginationGuard({
 *   totalCount: 100,
 *   perPage: 10,
 *   currentPage: 2,
 *   basePath: `/search?q=${encodeURIComponent(keyword)}`,
 * });
 */
export function paginationGuard(data: PageData) {
  const { totalCount, perPage, currentPage, basePath } = data;

  // ページネーションで表示可能な最大ページ数
  const maxPage = Math.ceil(totalCount / perPage);

  // アクセスしようとしているページが無効な場合
  if (currentPage <= 0 || currentPage > maxPage) {
    // クエリパラメーターを追加・上書きできるようにする
    const url = new URL(basePath, 'http://dummy.com');
    // 1ページ目で設定する
    url.searchParams.set('page', '1');
    // パスとクエリパラメーターだけを取り出してリダイレクトする
    redirect(url.pathname + url.search);
  }
}

リダイレクトさせているのは「ソフト404エラー」にならないようにするためです。

ソフト404エラーは存在しないURLとして、404エラーのページを表示するにも関わらず、サーバーのリクエストが正常に行われていることを表す200のステータスコードで返答してしまうことを指します。
このソフト404の大きな問題点が正常に200のステータスコードとして扱われるため、正常なサイトとしてクローリングされることです。本来はクローリングされないページをクローリングしているので、サイトの評価に影響してしまいます。

404 not found(404エラー)とは?原因と解決方法・効果的な404エラーページの作り方| 株式会社PLAN-B

インデックス

クロール可能な範囲でインデックスさせる

ページネーションとは何だったのか、SEO的なベストプラクティスとは #DemandLive SEO5つの論点より | アユダンテ株式会社

上記の記事にある「ページネーションの2ページ目はsitemapに入れたほうがいいのか」という質問に対して、次のような回答があります。

A:一度は重要なページを全て評価してもらったほうがいいので、ページネーションは全てsitemapに入れてもらうようにお願いしています。逆に絞り込みはブロックしたり制御することが多いです。

室屋さん:すべてを評価させるのがベストではあるけど、サイトの規模によって調整が必要。私が以前担当していた大規模サイトでは、sitemap だけでクロールバジェットを使い果たしてしまう状況でした。仕方なく優先度の高いものだけを sitemap に記述する運用に切り替えました。

canonicalもそうでしたが、より多くのURLをクロールしてもらうことがSEOでは大切なようです。

ただし記事に書かれているように、サイトごとにクロールしてくれる上限(クロールバジェット)が存在するので、URLの総数やSearch Consoleの登録状況を考慮しながら設定していく必要がありそうです。

絞り込みや並び替えのURLはインデックスさせない

フィルタや並べ替えのある URL がインデックスに登録されないようにする
サイト上の長い検索結果リストに対してフィルタや並べ替えをサポートすることができます。たとえば、URL で「?order=price」をサポートして、同じリストを価格順で返すなどです。

同じ検索結果リストのバリエーションをインデックスに登録しないようにするには、noindex robots meta タグを使用して不要な URL がインデックスに登録されないようにブロックするか、robots.txt ファイルを使用して特定の URL パターンがクロールされないようにします。

ページネーション、無限スクロールや「もっと見る」ボタン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

効率的にクロールしてもらうために、とくにURL数の多いサイトはやっておくとよさそうです。

ページにnoindexを指定する

name="googlebot"もありますが、基本的にはname="robots"で問題ないと思います。

<meta name="robots" content="noindex">

HTTP レスポンス ヘッダーで設定する方法もあります。

HTTP/1.1 200 OK
(...)
X-Robots-Tag: noindex
(...)

noindex を使用してコンテンツをインデックスから除外する | Google 検索セントラル  |  ドキュメント  |  Google for Developers

robots.txtファイルで除外する

robots.txtファイルのほうが一括で管理できるので保守性が高く保てそうです。
たとえば?order=priceのようなクエリを除外する場合は次のように指定するとよさそうです(未検証です)。

User-agent: *
Disallow: /*?*order=
  • User-agent: *:すべての検索エンジンボットに対してルールを適用します
  • Disallow: /*?*order=:URLに「order=」が含まれるすべてのページをクロールから除外します

Google による robots.txt の指定の解釈 | Google 検索セントラル  |  ドキュメント  |  Google for Developers

chot Inc. tech blog

Discussion