閉じる

技術ブログ

【WordPress】プレビューにカスタムフィールドが反映されない問題を根本から解決する

2026.04.10

WordPress 実践シリーズ #05
プレビューにカスタムフィールドが
反映されない問題を根本から解決する
「編集中の値がプレビューに出ない」──
WordPressの長年の課題に、Coreの仕組みから向き合う完全ガイド

固定ページのカスタムフィールドを編集して「プレビュー」ボタンを押す。
本文のテキストは確かに反映されているのに、カスタムフィールドだけは古い値のまま
「保存してからじゃないと確認できないの?」
そんなモヤモヤを感じたことはありませんか?

さらにもう一つ。「この固定ページをトップページ用テンプレート(front-page.php)で見たらどうなるか確認したい」
WordPressの標準機能では、これも一筋縄ではいきません。

この記事では、WordPress Coreのプレビュー機構・リビジョンシステム・テンプレート階層という3つの仕組みを理解したうえで、実運用に耐える解決策を提示します。


1. 問題の確認 ── 何が起きているのか

まずは問題の全体像を確認しましょう。社内のWordPress担当者から寄せられた質問は、次のようなものでした。

寄せられた質問

① 固定ページをプレビューする際、未保存(編集中)のカスタムフィールドの内容を表示する方法はありますか。

② また、トップページ用のテンプレートを使用して固定ページをプレビューした場合でも、該当ページのカスタムフィールドの内容を表示することは可能でしょうか。

この2つの問いは、実は深く関係しています。「プレビュー」という一見単純な機能の裏側で、WordPressはどんなことをしているのか。そして、なぜ本文は反映されるのにカスタムフィールドは反映されないのか。順を追って見ていきましょう。

再現手順

  1. 固定ページに ACF や add_meta_box() でカスタムフィールドを追加
  2. 編集画面でカスタムフィールドの値を書き換える(まだ「更新」は押さない)
  3. 「プレビュー」ボタンをクリック
  4. 別タブで表示されるが、カスタムフィールドの値は前回保存時のまま
プレビューされない
  • カスタムフィールド(post_meta)
  • ACF(Free版)の値
  • タームメタ / ユーザーメタ
  • WP_Query の meta_query 結果
プレビューされる
  • タイトル(post_title)
  • 本文(post_content)
  • 抜粋(post_excerpt)
  • ステータス / スラッグ

「反映される / されない」を分ける基準は明確です。wp_posts テーブルのカラムに存在するものは反映される。post_meta として別テーブルに保存されているものは反映されないのです。


2. なぜ反映されないのか ── WordPressリビジョンの仕様

WordPressの「プレビュー」は、実はとても巧妙な仕組みで動いています。「編集中の値を一時的に表示する」と言いながら、内部ではリビジョン(revision)という実在のレコードを作っているのです。

プレビューの正体は「autosaveリビジョン」

プレビューボタンを押すと、編集中の内容は wp_posts テーブルに新しい行として INSERT されます。post_type は revision、post_status は inherit、post_parent に編集中のページIDが入ります。

— プレビューを押すと wp_posts にこんなレコードが増える
SELECT ID, post_type, post_status, post_parent, post_title
FROM wp_posts
WHERE post_parent = 123
AND post_type = ‘revision’
ORDER BY ID DESC;

+—–+———–+————-+————-+———————-+
| ID | post_type | post_status | post_parent | post_title |
+—–+———–+————-+————-+———————-+
| 456 | revision | inherit | 123 | 123-autosave-v1 |
+—–+———–+————-+————-+———————-+

プレビュー URL(?p=123&preview=true&preview_nonce=xxx)を開くと、WordPress は親投稿(ID=123)の post_title / post_content / post_excerpt を、このリビジョン(ID=456)の値でその場で差し替えて表示します。DBに書き戻すわけではなく、あくまで表示時の上書きです。

ではなぜ post_meta は差し替わらないのか

答えはシンプルで、WordPress Coreの _wp_put_post_revision() という関数は、リビジョンを作る際に post_meta をコピーしないからです。

⚠️ WordPressの設計仕様

_wp_post_revision_fields() がリビジョン対象として返すのは、post_title / post_content / post_excerpt など wp_posts テーブルのカラムのみです。

post_meta(wp_postmeta テーブル)は「投稿本体の付属情報」という扱いで、リビジョンの履歴管理の対象外になっています。これは設計仕様であり、バグではありません。

この仕様は WordPress の Trac(バグトラッカー)でも長年議論されてきました:

  • #20299 Revisions for post meta
  • #11049 Revisions should include custom fields
  • #16847 Custom fields in preview

いずれも「現状の仕様では対応できないので、プラグインやテーマ側で拡張してほしい」という結論になっています。つまり、カスタムフィールドをプレビューに反映させるのは、テーマ/プラグイン開発者の責任というのが Core チームのスタンスなのです。


3. プレビュー処理の流れをCoreで追う

解決策を考える前に、プレビューが内部でどう動いているのかを押さえておきましょう。ここを理解していないと、フックを仕込む場所を間違えます。

WORDPRESS PREVIEW FLOW
STEP 1 ── ADMIN
編集画面で「プレビュー」ボタンを押す
wp-admin/post.php?action=preview にPOST送信
STEP 2 ── CORE
post_preview() → wp_create_post_autosave()
wp-admin/includes/post.php で autosaveリビジョンを作成
STEP 3 ── REVISION
_wp_put_post_revision( $post, $autosave = true )
wp_postsに新しいrevisionレコードを挿入。★post_metaはコピーされない★
STEP 4 ── REDIRECT
/?p=123&preview=true&preview_id=123&preview_nonce=xxx
プレビューURLへリダイレクト(別リクエストとしてフロント側を表示)
STEP 5 ── FRONT
_show_post_preview() → add_filter( ‘the_posts’, ‘_set_preview’ )
wp-includes/revision.php でフィルターを仕込む
STEP 6 ── OVERRIDE
_set_preview( $posts )
wp_get_post_autosave()で最新リビジョンを取得し、$postのtitle/content/excerptを上書き
STEP 7 ── TEMPLATE
テンプレートで get_post_meta() を呼ぶ
wp_postmetaテーブルから★親投稿(123)の保存済み値★がそのまま返る

このフローで重要なのは、プレビュー表示は「別リクエスト」であるという点です。プレビューボタンを押した瞬間と、実際にブラウザでプレビューを表示する瞬間は、サーバから見ると別々のリクエスト。そのため、$_POST に入っていた編集中の値は、プレビュー表示の時点では失われています。

したがって、「編集中の値」をどこかに保存して、プレビュー表示時にそれを読み取るという橋渡しが必要になります。一番手近な保存場所が、先ほど見たautosaveリビジョンです。

Core 関数・フックの早見表

場所 関数 / フック 役割
wp-includes/revision.php _wp_put_post_revision() リビジョン作成。post_metaはコピーしない
wp-includes/revision.php _wp_post_revision_fields フィルター リビジョンに保存するフィールド定義
wp-includes/revision.php _show_post_preview() プレビュー時に the_posts にフィルタを仕込む
wp-includes/revision.php wp_get_post_autosave() 現在ユーザーの最新autosaveを取得
wp-includes/meta.php get_post_metadata フィルター get_post_meta() の返り値を差し替える
wp-includes/template-loader.php template_include フィルター 読み込むテンプレートファイルを差し替える

4. 解決策A: get_post_metadataフィルターで差し替える

まず最もシンプルな方法から。考え方はこうです。

作戦
① autoupsaveリビジョンが作られる瞬間に、$_POST から編集中のカスタムフィールド値を拾って、リビジョン側の post_meta として保存する。
② プレビュー表示時に get_post_meta() が呼ばれたら、get_post_metadata フィルターでリビジョン側の値を返すように差し替える。

コード全文です。このまま functions.php または自作プラグインに貼れば動きます。

/**
* プレビュー時に未保存のカスタムフィールドを表示する(最小構成版)
*
* 対象メタキーをホワイトリストで管理し、
* autosaveリビジョン作成時に $_POST から値を取り出してリビジョン側へ保存する。
*/

// ① プレビュー対象にしたいカスタムフィールドのキー一覧
function sb_preview_meta_keys() {
return [
‘subtitle’,
‘hero_image_id’,
‘cta_text’,
‘custom_css’,
];
}

// ② autosaveリビジョン作成時に $_POST からメタをリビジョンへ保存
add_action( ‘_wp_put_post_revision’, ‘sb_save_preview_meta_to_revision’ );
function sb_save_preview_meta_to_revision( $revision_id ) {
if ( empty( $_POST ) ) {
return;
}
foreach ( sb_preview_meta_keys() as $key ) {
if ( isset( $_POST[ $key ] ) ) {
$value = wp_unslash( $_POST[ $key ] );
update_metadata( ‘post’, $revision_id, $key, $value );
}
}
}

// ③ プレビュー表示時に get_post_meta() を差し替える
add_filter( ‘get_post_metadata’, ‘sb_preview_post_meta_filter’, 10, 4 );
function sb_preview_post_meta_filter( $value, $post_id, $meta_key, $single ) {
if ( ! is_preview() ) {
return $value;
}
if ( ! in_array( $meta_key, sb_preview_meta_keys(), true ) ) {
return $value;
}

// 現在ユーザーの最新autosaveリビジョンを取得
$preview = wp_get_post_autosave( $post_id );
if ( ! is_object( $preview ) ) {
return $value;
}

// 再帰を防ぐため一度フィルターを外して取得
remove_filter( ‘get_post_metadata’, ‘sb_preview_post_meta_filter’, 10 );
$preview_value = get_post_meta( $preview->ID, $meta_key, $single );
add_filter( ‘get_post_metadata’, ‘sb_preview_post_meta_filter’, 10, 4 );

if ( === $preview_value || null === $preview_value ) {
return $value;
}
return $single ? $preview_value : [ $preview_value ];
}

ポイント解説

Q. なぜ _wp_put_post_revision アクションで $_POST が使えるのか?
このアクションは「プレビューボタンを押した直後のリクエスト」内部で発火するため、まだ $_POST が生きています。プレビュー表示(別リクエスト)の時点では $_POST は空ですが、保存側ではこのタイミングで拾える、というのがポイントです。
Q. なぜ remove_filterget_post_metaadd_filter の3行が必要なのか?
フィルター内で get_post_meta() を呼ぶと、同じフィルターが再度トリガーされて無限ループになります。一度外してから呼び、終わったら再登録するのが定番のイディオムです。これを忘れると白画面+PHPメモリ枯渇で泣きます。
Q. wp_get_post_autosave() は誰のautosaveを返すのか?
第二引数を省略すると現在ログイン中のユーザーのautosaveを返します。複数人で同じページを編集しているケースでは、他人のautosaveは見えません(プレビューは「自分が編集中の内容を見る」ためのものなので、これは正しい挙動)。
⚠️ ホワイトリストは必須
対象キーを絞らず全メタを差し替えるコードを書くと、_edit_lock(同時編集ロック)や _wp_page_template(テンプレート選択)など、WordPress内部が使っているメタまで壊してしまう恐れがあります。必ずホワイトリストで対象を明示しましょう。

5. 解決策B: リビジョン対応のmetaを体系化する

解決策Aは「プレビューだけ対応」の最小構成でした。本番運用では、もう一歩進めて「カスタムフィールドもリビジョンの一部として扱う」方向で体系化するのが望ましいです。これなら:

  • リビジョン比較画面(「リビジョン」→「前のバージョンと比較」)でカスタムフィールドの差分が見られる
  • リビジョン復元(「この版に戻す」)でカスタムフィールドも一緒に復元される
  • プレビュー時の挙動は解決策Aと同じ

コードはやや長くなりますが、いったん仕込めば多数の案件で使い回せます。

/**
* カスタムフィールドをリビジョン対応させる完全実装
* (プレビュー表示 + 差分比較 + 復元まで対応)
*/

function sb_revisioned_meta_keys() {
return [ ‘subtitle’, ‘hero_image_id’, ‘cta_text’ ];
}

// ① リビジョン保存時に親投稿のmetaをコピー(プレビュー中は$_POSTから)
add_action( ‘save_post’, ‘sb_copy_meta_to_revision’, 10, 2 );
function sb_copy_meta_to_revision( $post_id, $post ) {
if ( ‘revision’ !== $post->post_type ) return;
$parent_id = $post->post_parent;
if ( ! $parent_id ) return;

foreach ( sb_revisioned_meta_keys() as $key ) {
if ( isset( $_POST[ $key ] ) ) {
$value = wp_unslash( $_POST[ $key ] );
} else {
$value = get_post_meta( $parent_id, $key, true );
}
if ( !== $value && null !== $value ) {
update_metadata( ‘post’, $post_id, $key, $value );
}
}
}

// ② リビジョン復元時に親のmetaを書き戻す
add_action( ‘wp_restore_post_revision’, ‘sb_restore_meta_from_revision’, 10, 2 );
function sb_restore_meta_from_revision( $post_id, $revision_id ) {
foreach ( sb_revisioned_meta_keys() as $key ) {
$value = get_metadata( ‘post’, $revision_id, $key, true );
if ( false === $value || === $value ) {
delete_post_meta( $post_id, $key );
} else {
update_post_meta( $post_id, $key, $value );
}
}
}

// ③ プレビュー時に get_post_meta() をリビジョン側に向ける
add_filter( ‘get_post_metadata’, ‘sb_preview_meta_from_revision’, 10, 4 );
function sb_preview_meta_from_revision( $value, $post_id, $meta_key, $single ) {
if ( ! is_preview() ) return $value;
if ( ! in_array( $meta_key, sb_revisioned_meta_keys(), true ) ) return $value;

$preview = wp_get_post_autosave( $post_id );
if ( ! $preview ) return $value;

remove_filter( ‘get_post_metadata’, ‘sb_preview_meta_from_revision’, 10 );
$preview_value = get_post_meta( $preview->ID, $meta_key, $single );
add_filter( ‘get_post_metadata’, ‘sb_preview_meta_from_revision’, 10, 4 );

return $preview_value;
}

// ④ リビジョン比較画面にカスタムフィールドを表示(任意)
add_filter( ‘_wp_post_revision_fields’, ‘sb_add_meta_to_revision_fields’ );
function sb_add_meta_to_revision_fields( $fields ) {
$fields[‘subtitle’] = ‘サブタイトル’;
$fields[‘cta_text’] = ‘CTAテキスト’;
return $fields;
}

add_filter( ‘_wp_post_revision_field_subtitle’, ‘sb_revision_field_value’, 10, 4 );
add_filter( ‘_wp_post_revision_field_cta_text’, ‘sb_revision_field_value’, 10, 4 );
function sb_revision_field_value( $value, $field, $post, $context = null ) {
return get_metadata( ‘post’, $post->ID, $field, true );
}

④のフィルターを仕込むと、リビジョン比較画面にこんな感じでカスタムフィールドが並びます。差分も Core の diff ビューアで見られるようになります。

💡 おすすめ構成
解決策Bをテーマの inc/preview-meta.php などに切り出しておき、sb_revisioned_meta_keys() の配列だけ案件ごとにカスタマイズする運用が便利です。共通テーマを運用しているなら function-preview.php のような専用ファイルとして切り出す形がおすすめ。

6. 解決策C: ACF Proのリビジョン機能に任せる

もし案件で ACF Pro を使っているなら、実は何もしなくてもプレビューが動きます。ACF Pro には「ACF Revisions」という機能が標準搭載されており、リビジョン作成時にACFフィールドの値を自動的にリビジョン側へ保存してくれるからです。

ACFバージョン プレビュー対応 備考
ACF Free(無料版) ✕ 非対応 公式フォーラムでも「自作フィルターで対応してくれ」という回答。解決策A/Bを使う
ACF Pro 5.0 〜 ○ 標準対応 ACF Revisions機能が自動的にmetaをリビジョン側に保存
ACF Pro 6.x(現行) ○ 安定動作 Gutenbergでも問題なく動く

ACF Pro使用時の注意点

ACF Pro と自作フィルターの併用は避ける

ACF Pro を有効にしている案件で解決策A/Bを仕込むと、両方が get_post_metadata を差し替えて競合します。動作が不安定になったり、意図しない値が返ったりするので、どちらか一方に統一しましょう。

推奨: ACF Pro 案件 → ACF の標準機能に任せるACF Free または非ACF案件 → 解決策A/Bを使う

acf/load_value でピンポイントに差し替える

ACF Pro でも特定のフィールドだけ挙動を変えたい場合は acf/load_value フィルターが使えます。

add_filter( ‘acf/load_value/name=subtitle’, ‘sb_acf_subtitle_preview’, 10, 3 );
function sb_acf_subtitle_preview( $value, $post_id, $field ) {
if ( is_preview() ) {
// プレビュー中の特別処理をここに書く
}
return $value;
}

7. 論点2: 別テンプレートでプレビューする方法

ここからが2つ目の質問です。「この固定ページを front-page.php で見たらどうなるか確認したい」──この要望、実はWordPressの設計上、少しクセがあります。

前提: front-page.php はどのURLで使われるか

WordPress のテンプレート階層では、front-page.php「サイトのトップページURL(/)」でのみ自動適用されます。つまり、「設定 → 表示設定 → ホームページの表示」で「固定ページ」を選び、そのページとして指定されたページに トップURLでアクセスしたときだけ front-page.php が読み込まれます。

front-page.php が使われる
  • https://example.com/
  • 設定 → 表示設定 → 固定ページを指定
front-page.php は使われない
  • https://example.com/?page_id=123
  • https://example.com/about/
  • 通常の固定ページURL

したがって、編集中の固定ページを プレビューURL(?page_id=123&preview=true) で開いても、WordPressは「これはトップじゃない」と判断して front-page.php を選びません。通常は page-{slug}.phppage.php の順でテンプレートが選ばれます。

解決策: template_include フィルターで強制的に差し替える

やり方は「プレビューURLに ?preview_template=front-page のようなクエリを追加し、template_include フィルターで受け取って、指定のテンプレートファイルを返す」というシンプルな実装です。

/**
* プレビュー時に ?preview_template=xxx で任意のテンプレートを使えるようにする
*
* 使い方:
* 通常: /?page_id=123&preview=true&preview_nonce=xxx
* 別テンプレート: 上記URL + &preview_template=front-page
*/

add_filter( ‘template_include’, ‘sb_preview_with_alt_template’, 99 );
function sb_preview_with_alt_template( $template ) {
if ( ! is_preview() ) return $template;
if ( empty( $_GET[‘preview_template’] ) ) return $template;

$requested = sanitize_file_name( $_GET[‘preview_template’] );

// 許可テンプレートのホワイトリスト
$allowed = [ ‘front-page’, ‘page-landing’, ‘page-campaign’ ];
if ( ! in_array( $requested, $allowed, true ) ) {
return $template;
}

$found = locate_template( [ $requested . ‘.php’ ] );
return $found ? $found : $template;
}

編集画面にプレビューボタンを追加する

毎回クエリを手打ちするのは面倒なので、編集画面の「公開」ボックスにボタンを追加しておきましょう。

add_action( ‘post_submitbox_misc_actions’, ‘sb_add_alt_preview_link’ );
function sb_add_alt_preview_link( $post ) {
if ( ‘page’ !== $post->post_type ) return;

$url = add_query_arg(
[
‘preview’ => ‘true’,
‘preview_template’ => ‘front-page’,
],
get_preview_post_link( $post )
);
echo ‘<div class=”misc-pub-section”>’
. ‘<a class=”button” target=”_blank” href=”‘ . esc_url( $url ) . ‘”>’
. ‘front-page.php でプレビュー</a></div>’;
}

このテンプレート内でカスタムフィールドは取れるのか?

結論から言うと、取れます。ただし次の条件を満たす必要があります。

✓ 動くための前提条件

① 解決策A/B(または ACF Pro)を併用していること
template_include でテンプレートを差し替えただけでは、get_post_meta() は依然として「保存済みの値」を返します。プレビューへの反映は解決策A/Bのフィルターに依存します。

② front-page.php 側で is_front_page() の分岐に注意すること
プレビューURL(?page_id=123&preview=true)では is_front_page() は false です。front-page.php の中に if ( is_front_page() ) のような分岐があると、意図どおりに動きません。

③ 独自クエリを使っている場合は get_queried_object() ベースに書き換える
front-page.php が new WP_Query([ 'page_id' => 固定値 ]) のような固定IDベースで書かれていると、編集中ページの情報を取りません。

front-page.php のプレビュー対応改修例

<?php
/**
* front-page.php
* プレビュー対応版
*/

get_header(); ?>

<?php
// プレビュー時は編集中ページ、通常時はトップ表示対象ページを取得
if ( is_preview() ) {
$page = get_queried_object();
} else {
$page = get_post( get_option( ‘page_on_front’ ) );
}
setup_postdata( $page );
$subtitle = get_post_meta( $page->ID, ‘subtitle’, true );
?>

<section class=“hero”>
<h1><?php echo esc_html( $page->post_title ); ?></h1>
<p class=“subtitle”><?php echo esc_html( $subtitle ); ?></p>
</section>

<?php wp_reset_postdata(); ?>
<?php get_footer(); ?>

これで、編集中ページのタイトル・サブタイトル(カスタムフィールド)が front-page.php のレイアウトで表示されます。解決策A/Bと組み合わせれば、未保存の値でプレビュー可能です。


8. 落とし穴・注意点まとめ

# 落とし穴 対策
1 フィルター内の get_post_meta() 再帰呼び出しで無限ループ remove_filter → 取得 → add_filter の3行イディオム必須
2 対象メタキーを絞らず _edit_lock まで差し替えてしまう 必ずホワイトリスト(in_array)で対象を明示
3 ACF Pro と自作フィルターの併用で競合 どちらか一方に統一。Pro案件ではACFに任せる
4 Gutenberg プレビューは REST API 経由で $_POST が空 rest_after_insert_pageupdated_postmeta で同期
5 front-page.php 内の is_front_page() 分岐で動かない is_preview() でバイパスガードを入れる
6 独自クエリ(固定IDベース)で編集中ページを取らない get_queried_object() ベースに書き換え
7 WP_Query の meta_query には効かない get_post_metadata フィルターは SQL 直接発行には効かない。プレビュー中の一覧絞り込みには使えない
8 キャッシュプラグインで前回のプレビューがキャッシュされる プレビューURLはクエリ付きで除外されるのが基本。W3TC等で設定確認
9 $_POST の値をサニタイズせずに update_metadata wp_unslash + 適切な sanitize_text_field / absint
10 別ユーザーの autosave が取れない これは仕様。複数人編集では実現困難

9. まとめ ── どの解決策を選ぶべきか

冒頭の2つの質問に対する最終回答です。

質問①: 未保存のカスタムフィールドをプレビューに表示するには?

1 ACF Pro を使っている案件 → ACF Revisions に任せる。追加実装なし。

2 ACF Free / 非ACFの案件で、プレビューだけ対応したい → 解決策A(get_post_metadataフィルター + _wp_put_post_revision アクション)

3 リビジョン比較・復元まで対応したい → 解決策B(リビジョン対応metaの体系化)

質問②: 別テンプレート(front-page.php)でプレビューするには?

1 template_include フィルターで ?preview_template=front-page を受けて差し替える
2 post_submitbox_misc_actions アクションで編集画面にボタンを追加
3 front-page.php 側を is_preview()get_queried_object() 対応に改修
4 カスタムフィールドの反映が必要なら質問①の解決策も併用必須

テーマへの組み込み例

親テーマや共通テーマに組み込む場合の推奨配置:

ファイル 役割
inc/function-preview.php 解決策Bのリビジョン対応コード。対象メタキーの配列は子テーマ側で上書き可能にする
inc/function-preview-template.php 論点2のテンプレート切替コード。許可テンプレートは子テーマで拡張
functions.php 上記2ファイルを require_once で読み込み

これで案件ごとに「どのメタをプレビュー対応させるか」「どのテンプレートに切り替えを許可するか」だけ設定すれば済む形になります。


参考リンク

Seeds Brains
2026-04-10 作成