技術ブログ
【WordPress】プレビューにカスタムフィールドが反映されない問題を根本から解決する
固定ページのカスタムフィールドを編集して「プレビュー」ボタンを押す。
本文のテキストは確かに反映されているのに、カスタムフィールドだけは古い値のまま。
「保存してからじゃないと確認できないの?」
そんなモヤモヤを感じたことはありませんか?
さらにもう一つ。「この固定ページをトップページ用テンプレート(front-page.php)で見たらどうなるか確認したい」。
WordPressの標準機能では、これも一筋縄ではいきません。
この記事では、WordPress Coreのプレビュー機構・リビジョンシステム・テンプレート階層という3つの仕組みを理解したうえで、実運用に耐える解決策を提示します。
1. 問題の確認 ── 何が起きているのか
まずは問題の全体像を確認しましょう。社内のWordPress担当者から寄せられた質問は、次のようなものでした。
① 固定ページをプレビューする際、未保存(編集中)のカスタムフィールドの内容を表示する方法はありますか。
② また、トップページ用のテンプレートを使用して固定ページをプレビューした場合でも、該当ページのカスタムフィールドの内容を表示することは可能でしょうか。
この2つの問いは、実は深く関係しています。「プレビュー」という一見単純な機能の裏側で、WordPressはどんなことをしているのか。そして、なぜ本文は反映されるのにカスタムフィールドは反映されないのか。順を追って見ていきましょう。
再現手順
- 固定ページに ACF や
add_meta_box()でカスタムフィールドを追加 - 編集画面でカスタムフィールドの値を書き換える(まだ「更新」は押さない)
- 「プレビュー」ボタンをクリック
- 別タブで表示されるが、カスタムフィールドの値は前回保存時のまま
- カスタムフィールド(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が入ります。
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 をコピーしないからです。
_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で追う
解決策を考える前に、プレビューが内部でどう動いているのかを押さえておきましょう。ここを理解していないと、フックを仕込む場所を間違えます。
このフローで重要なのは、プレビュー表示は「別リクエスト」であるという点です。プレビューボタンを押した瞬間と、実際にブラウザでプレビューを表示する瞬間は、サーバから見ると別々のリクエスト。そのため、$_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 ];
}
ポイント解説
_wp_put_post_revision アクションで $_POST が使えるのか?$_POST が生きています。プレビュー表示(別リクエスト)の時点では $_POST は空ですが、保存側ではこのタイミングで拾える、というのがポイントです。remove_filter → get_post_meta → add_filter の3行が必要なのか?get_post_meta() を呼ぶと、同じフィルターが再度トリガーされて無限ループになります。一度外してから呼び、終わったら再登録するのが定番のイディオムです。これを忘れると白画面+PHPメモリ枯渇で泣きます。wp_get_post_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 を有効にしている案件で解決策A/Bを仕込むと、両方が get_post_metadata を差し替えて競合します。動作が不安定になったり、意図しない値が返ったりするので、どちらか一方に統一しましょう。
推奨: ACF Pro 案件 → ACF の標準機能に任せる / ACF Free または非ACF案件 → 解決策A/Bを使う
acf/load_value でピンポイントに差し替える
ACF Pro でも特定のフィールドだけ挙動を変えたい場合は acf/load_value フィルターが使えます。
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 が読み込まれます。
https://example.com/- 設定 → 表示設定 → 固定ページを指定
https://example.com/?page_id=123https://example.com/about/- 通常の固定ページURL
したがって、編集中の固定ページを プレビューURL(?page_id=123&preview=true) で開いても、WordPressは「これはトップじゃない」と判断して front-page.php を選びません。通常は page-{slug}.php → page.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;
}
編集画面にプレビューボタンを追加する
毎回クエリを手打ちするのは面倒なので、編集画面の「公開」ボックスにボタンを追加しておきましょう。
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 のプレビュー対応改修例
/**
* 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_page や updated_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の体系化)
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 で読み込み |
これで案件ごとに「どのメタをプレビュー対応させるか」「どのテンプレートに切り替えを許可するか」だけ設定すれば済む形になります。
参考リンク
- WordPress Developer:
wp_get_post_autosave() - WordPress Developer:
get_post_metadataフック - WordPress Developer:
_wp_post_revision_fieldsフック - WordPress Developer:
template_includeフック - WordPress Theme Handbook: Template Hierarchy
- WordPress Trac: #20299 Revisions for post meta
- WordPress Trac: #16847 Custom fields in preview
- ACF Documentation: ACF Revisions
2026-04-10 作成