【WordPress】プラグインなしで「最近見た記事」機能を実装する方法|localStorageでキャッシュ完全対応

「最近見た記事」を表示する機能は、読者の回遊率向上に役立ちます。この記事では、プラグインを使わずに実装する方法を、ページキャッシュ環境でも確実に動作する設計で解説します。

この記事でわかること

  • JavaScript+localStorage を使ったキャッシュ完全対応の実装
  • PHPセッション方式がキャッシュ環境に向かない理由
  • 重複除外・保存件数の上限・簡単なXSS対策
スポンサーリンク

実装方針:なぜPHPセッションではなくlocalStorageを使うか

PHPセッション($_SESSION)方式が向かない2つの理由
ページキャッシュと非互換:WP Super Cache・WP Rocket・Cocoonの高速化設定などでページが静的HTMLとして配信されると、そのリクエストではPHPが実行されないため閲覧記録も表示もまったく動作しません。②既定の有効期限が短い:PHPのセッションは既定で約24分(session.gc_maxlifetime)で消えるため、「最近見た記事」として期待される数時間〜数日単位の振り返りには使えません。

そこで、閲覧記録と表示の両方をブラウザ側のlocalStorageで完結させます。サーバー側で出力するのは「今開いているページ自身の情報」だけなので、その部分はページごとに固定値=キャッシュしても問題ないという設計です。

各記事ページに閲覧記録を仕込む

個別記事のテンプレート(single.php)に、現在の記事の情報を localStorage に追記するスクリプトを出力します。PHPの値をJavaScriptへ埋め込む際は、wp_json_encode() を使うことでタイトルに含まれる引用符などによるエラーやXSSを防ぎます。

single.php:閲覧した記事をlocalStorageに記録
<script>
(function () {
  var STORAGE_KEY = 'recently_viewed_posts';
  var MAX_ITEMS = 8;
  var current = {
    id: <?php echo (int) get_the_ID(); ?>,
    title: <?php echo wp_json_encode( get_the_title() ); ?>,
    url: <?php echo wp_json_encode( get_permalink() ); ?>,
    thumb: <?php echo wp_json_encode( get_the_post_thumbnail_url( get_the_ID(), 'thumbnail' ) ?: '' ); ?>
  };

  var list;
  try {
    list = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
  } catch (e) {
    list = [];
  }
  list = list.filter(function (p) { return p.id !== current.id; }); // 同じ記事の重複を除外
  list.unshift(current); // 先頭に追加
  if (list.length > MAX_ITEMS) list = list.slice(0, MAX_ITEMS);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
})();
</script>
この部分はキャッシュされても問題ない
PHPが出力しているのは「このページ自身のタイトル・URL・サムネイル」という、誰が見ても同じ値です。ページ全体がキャッシュされて配信されても、このスクリプトタグごと配信されるだけなので支障はありません。閲覧履歴という利用者ごとに異なるデータは、ブラウザ内の localStorage にのみ保存され、サーバー側には一切残りません。

「最近見た記事」の一覧を表示する

サイドバーやフッターに、一覧を差し込むための空の入れ物を用意し、JavaScriptで localStorage から読み出して組み立てます。表示中の記事自身は一覧から除外します。

sidebar.php:表示位置に空の入れ物を用意
<div id="recently-viewed-posts"></div>
JavaScript:localStorageから読み出して一覧を描画
<script>
(function () {
  var STORAGE_KEY = 'recently_viewed_posts';
  var container = document.getElementById('recently-viewed-posts');
  if (!container) return;

  function escapeHtml(str) {
    return String(str).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }

  var list;
  try {
    list = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
  } catch (e) {
    list = [];
  }

  var currentUrl = location.href.split('#')[0].split('?')[0];
  list = list.filter(function (p) { return p.url !== currentUrl; }); // 表示中の記事自身は除外

  if (!list.length) {
    container.remove(); // 履歴が無ければ入れ物ごと消す
    return;
  }

  var html = '<ul class="recently-viewed-list">';
  list.forEach(function (p) {
    html += '<li><a href="' + escapeHtml(p.url) + '">'
      + (p.thumb ? '<img src="' + escapeHtml(p.thumb) + '" alt="">' : '')
      + '<span>' + escapeHtml(p.title) + '</span></a></li>';
  });
  html += '</ul>';
  container.innerHTML = html;
})();
</script>
escapeHtml() を必ず挟む
localStorage の中身は自分のスクリプトだけが書き込んでいるとはいえ、記事タイトルをそのまま innerHTML に差し込むのは安全ではありません。サーバー側で esc_html() を使うのと同じ考え方で、クライアント側でもHTML特殊文字をエスケープしてから描画してください。

関連するlocalStorageの使い方

よくある質問(FAQ)

QPHPのセッションやCookieを使う実装ではダメですか?
A動作はしますが、ページキャッシュを使っているサイトでは正しく機能しません。キャッシュされたHTMLが返される限りPHPは実行されないため、閲覧記録も一覧表示も更新されなくなります。キャッシュを一切使わない小規模サイトであればCookie+WP_Querypost__inパラメーターで実装する方法もあります。
Q保存件数の上限や重複除外はどう実装しますか?
AJavaScript側で unshift() で先頭に追加した後、同じ記事IDのものを filter() で除外し、slice(0, MAX_ITEMS) で上限件数に切り詰めます。記事末尾のコード例では上限を8件にしています。
Qサムネイル画像がない記事はどう表示されますか?
Aget_the_post_thumbnail_url() はアイキャッチが無い場合 false を返すため、コード例では空文字に変換して保存し、表示側で thumb が空なら<img> タグ自体を出力しないようにしています。

まとめ

  • 推奨:JavaScript+localStorage(ページキャッシュと完全互換)
  • 避ける:PHPセッション(キャッシュ非互換・約24分で消える)
  • 実装のポイント:ページ自身の情報だけをサーバーが出力し、閲覧履歴はブラウザ側で管理
  • 忘れずにwp_json_encode()(サーバー側)と escapeHtml()(クライアント側)でエスケープ

ページキャッシュを使う現代のWordPress環境では、閲覧履歴のようなユーザーごとに異なるデータはブラウザ側で管理するのが基本方針です。