「最近見た記事」を表示する機能は、読者の回遊率向上に役立ちます。この記事では、プラグインを使わずに実装する方法を、ページキャッシュ環境でも確実に動作する設計で解説します。
この記事でわかること
- JavaScript+
localStorageを使ったキャッシュ完全対応の実装 - PHPセッション方式がキャッシュ環境に向かない理由
- 重複除外・保存件数の上限・簡単なXSS対策
実装方針:なぜPHPセッションではなくlocalStorageを使うか
PHPセッション($_SESSION)方式が向かない2つの理由
①ページキャッシュと非互換:WP Super Cache・WP Rocket・Cocoonの高速化設定などでページが静的HTMLとして配信されると、そのリクエストではPHPが実行されないため閲覧記録も表示もまったく動作しません。②既定の有効期限が短い:PHPのセッションは既定で約24分(
①ページキャッシュと非互換: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・サムネイル」という、誰が見ても同じ値です。ページ全体がキャッシュされて配信されても、このスクリプトタグごと配信されるだけなので支障はありません。閲覧履歴という利用者ごとに異なるデータは、ブラウザ内の
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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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_Queryのpost__inパラメーターで実装する方法もあります。Q保存件数の上限や重複除外はどう実装しますか?
AJavaScript側で
unshift() で先頭に追加した後、同じ記事IDのものを filter() で除外し、slice(0, MAX_ITEMS) で上限件数に切り詰めます。記事末尾のコード例では上限を8件にしています。Qサムネイル画像がない記事はどう表示されますか?
A
get_the_post_thumbnail_url() はアイキャッチが無い場合 false を返すため、コード例では空文字に変換して保存し、表示側で thumb が空なら<img> タグ自体を出力しないようにしています。まとめ
- 推奨:JavaScript+
localStorage(ページキャッシュと完全互換) - 避ける:PHPセッション(キャッシュ非互換・約24分で消える)
- 実装のポイント:ページ自身の情報だけをサーバーが出力し、閲覧履歴はブラウザ側で管理
- 忘れずに:
wp_json_encode()(サーバー側)とescapeHtml()(クライアント側)でエスケープ
ページキャッシュを使う現代のWordPress環境では、閲覧履歴のようなユーザーごとに異なるデータはブラウザ側で管理するのが基本方針です。
