2026年5月26日

2026年5月26日

WordPressでパーソナライズを実装する方法(ページキャッシュと両立させる設計)

WordPress でパーソナライズ(訪問者ごとの出し分け)を実装するときは、「誰に何を変えるか」と同じくらい「ページキャッシュとどう両立させるか」が重要です。ログインユーザー向けは PHP で、未ログインの訪問者向けは共通の HTML に JavaScript と REST API で個別部分を後から差し込む、という分け方にすると、表示速度を落とさずに実装できます。この記事では、それぞれの実装例と、キャッシュ・プライバシーで注意する点を説明します。

出し分けの方式は3つ

パーソナライズの実装場所は、大きく次の3つに分かれます。

  • サーバー側(PHP): テンプレートやショートコードの中で、ユーザー情報を見て出力を変える。JavaScript に頼らず表示できるが、ページキャッシュと衝突しやすい
  • クライアント側(JavaScript): HTML は全員共通にし、ページの読み込み後に個別の部分だけを取得・表示する。キャッシュと両立しやすい
  • エッジ側(CDN): CDN が国や端末の種類などでキャッシュを分け、それぞれの版を配信する。設定は CDN 依存になる

どれか1つを選ぶというより、「ログインユーザーの会員情報はサーバー側」「閲覧履歴に応じたおすすめはクライアント側」のように、出し分ける内容ごとに使い分けるのが現実的です。

ページキャッシュとの衝突を理解する

WP Super Cache や LiteSpeed Cache などのページキャッシュは、生成した HTML を保存して次の訪問者にもそのまま返します。ここで PHP が「〇〇さん、こんにちは」や「前回見た商品」を HTML に埋め込むと、最初の訪問者向けの内容がキャッシュされ、別の人にも表示されてしまいます。表示の不具合で済めばよいのですが、会員情報が他人に見える事故につながることもあります。

多くのキャッシュプラグインは、WordPress のログイン Cookie を持つリクエストをキャッシュの対象外にしています。そのため「ログインユーザーだけを PHP で出し分ける」設計は比較的安全です。反対に、未ログインの訪問者を Cookie や国の情報で PHP 側で出し分けると、キャッシュの設定を変えない限り混ざります。

未ログインの訪問者をサーバー側で出し分けたい場合の選択肢は、次のどちらかです。

  • 出し分けに使う Cookie を持つリクエストをキャッシュの対象外にする(キャッシュプラグインの「除外する Cookie」の設定など)
  • Cookie の値ごとにキャッシュを分ける(Vary ヘッダーやキャッシュキーの設定)

どちらもキャッシュの効率が下がるため、パターンは2〜3種類に抑えます。Vary: Cookie を付けると、Cookie の内容が少しでも違うだけで別のキャッシュになり、CDN ではほとんどキャッシュが効かなくなる点にも注意してください。

ログインユーザー向けにサーバー側で出し分ける

ログイン中のユーザーにだけ表示する案内を、ショートコードで作る例です。

add_shortcode( 'my_member_notice', function () {
	if ( ! is_user_logged_in() ) {
		return '<p>会員登録すると、続きから読める機能を使えます。</p>';
	}

	$user    = wp_get_current_user();
	$last_id = (int) get_user_meta( $user->ID, 'my_last_read_post', true );

	$html = sprintf( '<p>%s さん、おかえりなさい。</p>', esc_html( $user->display_name ) );

	if ( $last_id && 'publish' === get_post_status( $last_id ) ) {
		$html .= sprintf(
			'<p><a href="%s">前回の続き: %s</a></p>',
			esc_url( get_permalink( $last_id ) ),
			esc_html( get_the_title( $last_id ) )
		);
	}
	return $html;
} );

// 記事を開いたときに「最後に読んだ記事」を保存する
add_action( 'template_redirect', function () {
	if ( is_user_logged_in() && is_singular( 'post' ) ) {
		update_user_meta( get_current_user_id(), 'my_last_read_post', get_queried_object_id() );
	}
} );

ユーザー名やタイトルなど、データベースから取り出した値は esc_html() や esc_url() でエスケープしてから出力します。ユーザーが自分で編集できる表示名も、そのまま出力すると表示崩れやスクリプト混入の原因になります。

キャッシュプラグインの中には、ログインユーザー向けにもキャッシュを作る設定を持つものがあります。その設定を有効にしている場合は、ユーザーごとに別のキャッシュになっているかを、2つのアカウントで交互に表示して確認してください。

未ログインの訪問者にはJavaScriptとREST APIで差し込む

HTML は全員共通にしておき、ページ表示後に JavaScript で個別部分を取得する方法なら、ページキャッシュはそのまま使えます。ここでは「最近読んだカテゴリーのおすすめ記事」を表示する例を紹介します。閲覧したカテゴリーIDはブラウザの localStorage に保存し、記事の取得には WordPress 標準の REST API(/wp/v2/posts)を使うので、独自のエンドポイントもサーバー側での個人の記録も要りません。

まず、テーマで JavaScript を読み込み、現在の記事のカテゴリーと REST API の URL を渡します。

add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script(
		'my-personalize',
		get_theme_file_uri( 'assets/js/personalize.js' ),
		array(),
		'1.0.0',
		array( 'in_footer' => true, 'strategy' => 'defer' )
	);

	$data = array(
		'postsUrl'   => esc_url_raw( rest_url( 'wp/v2/posts' ) ),
		'categories' => is_singular( 'post' ) ? wp_get_post_categories( get_queried_object_id() ) : array(),
		'currentId'  => is_singular( 'post' ) ? get_queried_object_id() : 0,
	);
	wp_add_inline_script( 'my-personalize', 'window.myPersonalize = ' . wp_json_encode( $data ) . ';', 'before' );
} );

表示先の要素は、テンプレートに <div id="my-recommend" style="min-height: 8rem"></div> のように置いておきます。高さを先に確保しておくと、後から中身が入ったときのレイアウトのずれ(CLS)を抑えられます。

// assets/js/personalize.js
( function () {
	var cfg = window.myPersonalize || {};
	var key = 'myViewedCategories';
	var viewed = [];

	try {
		viewed = JSON.parse( localStorage.getItem( key ) || '[]' );
	} catch ( e ) {}

	// 今見ている記事のカテゴリーを先頭に追加し、最大5件に保つ
	( cfg.categories || [] ).forEach( function ( id ) {
		viewed = [ id ].concat( viewed.filter( function ( v ) { return v !== id; } ) );
	} );
	viewed = viewed.slice( 0, 5 );
	try {
		localStorage.setItem( key, JSON.stringify( viewed ) );
	} catch ( e ) {}

	var box = document.getElementById( 'my-recommend' );
	if ( ! box || ! viewed.length ) {
		return;
	}

	// URL オブジェクトで組み立てると、パーマリンク設定が「基本」(?rest_route=)でも壊れない
	var url = new URL( cfg.postsUrl );
	url.searchParams.set( 'categories', viewed[ 0 ] );
	url.searchParams.set( 'per_page', 3 );
	url.searchParams.set( '_fields', 'title,link' );
	if ( cfg.currentId ) {
		url.searchParams.set( 'exclude', cfg.currentId );
	}

	fetch( url.toString() )
		.then( function ( res ) { return res.ok ? res.json() : []; } )
		.then( function ( posts ) {
			var ul = document.createElement( 'ul' );
			posts.forEach( function ( post ) {
				var li = document.createElement( 'li' );
				var a = document.createElement( 'a' );
				a.href = post.link;
				// rendered は HTML 文字列。DOMParser でタグと文字参照を解いてから文字として入れる
				a.textContent = new DOMParser().parseFromString( post.title.rendered, 'text/html' ).body.textContent;
				li.appendChild( a );
				ul.appendChild( li );
			} );
			box.appendChild( ul );
		} )
		.catch( function () {} );
} )();

タイトルは innerHTML ではなく textContent で入れています。REST API が返すタイトルは HTML として整形済みの文字列なので、innerHTML に入れる設計にするとスクリプト混入の余地が生まれるためです。JavaScript が動かない環境では何も表示されないだけなので、ページ本来の機能には影響しません。

ログインユーザーの情報をRESTで返す場合

会員向けの情報を JavaScript で取得する場合は、独自エンドポイントを作り、permission_callback でログインを確認します。Cookie 認証で REST API を呼ぶには X-WP-Nonce ヘッダーに wp_create_nonce( 'wp_rest' ) の値を付ける必要があり、nonce を付けない要求は未ログインとして扱われます。nonce はユーザーごとに異なり有効期限もあるため、キャッシュされる HTML に埋め込むと古い値が配られて失敗します。ログインユーザーの画面がキャッシュされていないことを確認してから使ってください。また、レスポンスには Cache-Control: private などを付け、CDN に保存されないようにします。

国や地域で出し分ける場合

CDN の中には、訪問者の国コードをリクエストヘッダーに付けて配信元に渡す機能を持つものがあります。PHP からは $_SERVER の該当キーで読めますが、次の点に気をつけてください。

  • ヘッダーの値は信用しすぎない。想定する国コードの一覧と照合し、一覧にない値は既定の表示にする
  • PHP で出し分けた HTML を CDN やページキャッシュに保存する場合は、国ごとにキャッシュを分ける設定が要る。設定がないと最初の国の版が全員に配られる
  • IP アドレスからの推定には誤差がある。VPN や企業ネットワーク経由では別の国と判定されることがあるため、言語や通貨の切り替えは訪問者が手動でも選べるようにしておく

ヘッダー名や設定方法は CDN ごとに異なるので、利用中の CDN のドキュメントで確認してください(2026年10月現在)。

プライバシーへの配慮

パーソナライズは訪問者のデータを使う以上、プライバシーの扱いを設計の段階で決めておきます。

  • 閲覧履歴や行動データを使う場合は、プライバシーポリシーに「何のために、どんな情報を、どこに保存するか」を記載する
  • 地域や用途によっては、端末への保存(Cookie や localStorage)や行動の追跡に事前の同意が求められる。同意管理の仕組みを使っている場合は、同意が得られるまで保存処理を動かさない
  • 健康・信条など配慮が必要な情報を推測して出し分ける設計は避ける
  • 訪問者が出し分けをやめられる手段(保存データの削除ボタンなど)を用意しておく

どの規則が適用されるかはサイトの対象地域や事業内容によって変わるため、必要に応じて専門家に確認してください。

まとめ

  • 出し分けはサーバー側・クライアント側・エッジ側の3方式。内容ごとに使い分ける
  • PHP で個別の HTML を作ると、ページキャッシュで他の訪問者に配られる危険がある
  • ログインユーザー向けは is_user_logged_in() とユーザーメタで出し分け、出力はエスケープする
  • 未ログインの訪問者向けは、共通の HTML に JavaScript と REST API で差し込み、キャッシュを活かす
  • 国や Cookie で PHP 側の出し分けをするなら、キャッシュの除外や分割をセットで設定する
  • 使うデータと保存先をプライバシーポリシーに書き、必要な同意を得てから動かす

お気軽にご相談ください

お見積りへ お問い合わせへ
▲