カスタムフィールドでデータを一覧する、または入力する際にまとめてやってしまいたいことがあります。
ここでは投稿に独自のpriceメタを保存する一覧画面を作ります。有料記事サービスや決済システムの販売価格を変更する機能ではありません。既存サービスが同名メタを使う場合は、この例をそのまま導入しないでください。

本記事ではその作り方を紹介いたします。
ちなみにいろいろな技術記事を書いていますので、よかったら見てくださいませ。
WordPressの管理画面にカスタムフィールドの一覧・編集機能を作成する方法
まずはカスタムフィールドを用意しましょう。
今回は価格(price)というカスタムフィールドを作成します。
いつもであればACFかSCFのプラグインでカスタムフィールドを作成するのですが、一つであればfunctions.phpで書いてしまいます。
カスタムフィールドの作成
導入前にファイルとデータベースをバックアップしてください。子テーマのfunctions.phpかCode Snippetsのどちらか一方へ追加します。両方へ重複登録しないでください。最初のコードは共通処理も含むため、一覧画面を使う場合にも必要です。追加場所の選び方はWordPressにカスタムコードを追加する方法を参照してください。
functions.phpに下記のコードを挿入します。
// 投稿画面にカスタムフィールドエリアを追加
add_action('add_meta_boxes_post', function () {
add_meta_box(
'custom_price_box',
'価格',
'render_custom_price_field',
'post',
'side',
'default'
);
});
// 未入力は削除、0以上の整数は保存。それ以外は保存しない。
function waza_custom_price_value($raw)
{
if (!is_string($raw)) {
return new WP_Error('invalid_price', '価格は0以上の整数で入力してください。');
}
$value = trim(wp_unslash($raw));
if ($value !== '' && !preg_match('/\A[0-9]+\z/', $value)) {
return new WP_Error('invalid_price', '価格は0以上の整数で入力してください。');
}
return $value;
}
// 対象投稿のpriceだけを変更し、保存後の値を照合する。
function waza_custom_price_write($post_id, $value)
{
if ($value === '') {
delete_post_meta($post_id, 'price');
return !metadata_exists('post', $post_id, 'price');
}
update_post_meta($post_id, 'price', $value);
return get_post_meta($post_id, 'price', true) === $value;
}
function render_custom_price_field($post)
{
if (!current_user_can('edit_post', $post->ID)) {
return;
}
$price = get_post_meta($post->ID, 'price', true);
if (!is_scalar($price)) {
echo '<p>複合値のため、この入力欄では編集できません。</p>';
return;
}
wp_nonce_field('save_custom_price', 'custom_price_nonce');
echo '<label for="price">価格(円)</label>';
echo '<input type="number" min="0" step="1" id="price" name="price" value="' . esc_attr($price) . '" style="width:100%;" />';
}
add_action('save_post', function ($post_id) {
if ((defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
|| wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)
|| get_post_type($post_id) !== 'post'
|| (is_multisite() && ms_is_switched())
|| !current_user_can('edit_post', $post_id)) {
return;
}
if (!isset($_POST['custom_price_nonce'])
|| !is_string($_POST['custom_price_nonce'])
|| !wp_verify_nonce(wp_unslash($_POST['custom_price_nonce']), 'save_custom_price')
|| !isset($_POST['price'])) {
return;
}
// 既存の複合値を誤って空欄扱いにしない。
if (!is_scalar(get_post_meta($post_id, 'price', true))) {
return;
}
$value = waza_custom_price_value($_POST['price']);
if (!is_wp_error($value)) {
waza_custom_price_write($post_id, $value);
}
});
// 一覧の保存処理。表示対象と投稿単位の権限を再確認する。
function waza_custom_price_bulk_save($posts)
{
if (!current_user_can('manage_options')
|| !isset($_POST['custom_price_list_nonce'])
|| !is_string($_POST['custom_price_list_nonce'])
|| !wp_verify_nonce(wp_unslash($_POST['custom_price_list_nonce']), 'save_custom_price_list')) {
return new WP_Error('invalid_request', '権限または確認情報が無効です。再表示してください。');
}
if (!isset($_POST['price']) || !is_array($_POST['price'])) {
return new WP_Error('invalid_input', '価格の入力形式が不正です。');
}
$allowed_ids = array_map('intval', wp_list_pluck($posts, 'ID'));
$updates = [];
foreach ($_POST['price'] as $key => $raw) {
if (!preg_match('/\A[1-9][0-9]*\z/', (string) $key)) {
return new WP_Error('invalid_id', '対象投稿が不正です。');
}
$post_id = (int) $key;
if (!in_array($post_id, $allowed_ids, true)
|| get_post_type($post_id) !== 'post'
|| get_post_status($post_id) !== 'publish'
|| !current_user_can('edit_post', $post_id)
|| !is_scalar(get_post_meta($post_id, 'price', true))) {
return new WP_Error('invalid_target', '編集できない投稿が含まれています。');
}
$value = waza_custom_price_value($raw);
if (is_wp_error($value)) {
return $value;
}
$updates[$post_id] = $value;
}
// 入力全体が適切な場合だけ書き込む。DB障害時の一括ロールバックは行わない。
foreach ($updates as $post_id => $value) {
if (!waza_custom_price_write($post_id, $value)) {
return new WP_Error('save_failed', '一部を保存できませんでした。再表示して値を確認してください。');
}
}
return count($updates);
}
カスタムフィールドの表示場所
コードにsideと設定した場合はサイドバーにカスタムフィールドが設置されます。

normalを指定すると、投稿編集画面の本文下の領域に配置します。エディターや画面設定により見え方は異なります。

専用管理画面の作成
一覧は絞り込み付き版とシンプル版のどちらか一方だけを使います。同じ関数名を含むため、両方を登録するとエラーになります。対象は編集権限のある公開投稿で、取得するのは最大100件です。全投稿の一覧やページ送りではありません。
価格は0以上の整数で入力します。0は保存し、空欄はその投稿のpriceメタを削除します。負数・小数・配列等の不正な入力は保存しません。一覧では全入力を確認してから書き込みますが、データベース障害時の一括取り消しは行いません。複合値のメタはこの画面の編集対象外です。1投稿につきpriceメタ1件を前提とする例です。同名メタが複数あると更新・削除が複数行へ作用するため、その構成では使わないでください。
下記のコードをfunctions.phpに記載するだけです。
カテゴリーとタグの絞り込みを用意しています。両方指定すると両方に一致する投稿が対象です。
以下のコードです。
名称は好きに変更しましょう。
この例には、画面表示後の他ユーザーによる変更を検知する機能はありません。保存時には送信した全行が書き込まれるため、同時編集を避け、作業前に再表示してください。単一投稿画面では不正入力を保存せず従来値を残しますが、独自のエラー通知は出しません。保存後は値を再確認してください。
add_action('admin_menu', function () {
add_menu_page(
'価格一覧',
'価格一覧',
'manage_options',
'custom-price-list',
'render_custom_price_list',
'dashicons-money-alt',
20
);
});
function render_custom_price_list()
{
if (!current_user_can('manage_options')) {
wp_die('この画面を表示する権限がありません。');
}
// カテゴリーとタグの取得
$categories = get_terms(['taxonomy' => 'category', 'hide_empty' => false]);
$tags = get_terms(['taxonomy' => 'post_tag', 'hide_empty' => false]);
// 選択されたID
if (is_wp_error($categories) || is_wp_error($tags)) {
echo '<div class="notice notice-error"><p>分類を取得できませんでした。</p></div>';
return;
}
$selected_cat = isset($_GET['cat_id']) && is_string($_GET['cat_id']) ? absint(wp_unslash($_GET['cat_id'])) : 0;
$selected_tag = isset($_GET['tag_id']) && is_string($_GET['tag_id']) ? absint(wp_unslash($_GET['tag_id'])) : 0;
// 投稿を取得
$args = [
'post_type' => 'post',
'posts_per_page' => 100,
'post_status' => 'publish',
'tax_query' => [],
];
if ($selected_cat) {
$args['tax_query'][] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $selected_cat,
];
}
if ($selected_tag) {
$args['tax_query'][] = [
'taxonomy' => 'post_tag',
'field' => 'term_id',
'terms' => $selected_tag,
];
}
if (count($args['tax_query']) > 1) {
$args['tax_query']['relation'] = 'AND'; // 両方一致した投稿に限定
}
$posts = get_posts($args);
// 保存処理
$posts = array_values(array_filter($posts, function ($post) {
return current_user_can('edit_post', $post->ID);
}));
if (isset($_POST['custom_price_save'])) {
$result = waza_custom_price_bulk_save($posts);
if (is_wp_error($result)) {
echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
} else {
echo '<div class="notice notice-success"><p>' . esc_html((string) $result) . '件の価格を保存しました。</p></div>';
}
}
echo '<div class="wrap">';
echo '<h1>投稿の価格一覧</h1>';
// ▼絞り込みフォーム(カテゴリ + タグ)
echo '<form method="get" style="margin-bottom: 1em;">';
echo '<input type="hidden" name="page" value="custom-price-list">';
echo '<label for="cat_id">カテゴリー:</label>';
echo '<select name="cat_id" id="cat_id">';
echo '<option value="0">すべて</option>';
foreach ($categories as $cat) {
$selected = selected($selected_cat, $cat->term_id, false);
echo '<option value="' . esc_attr((string) $cat->term_id) . '" ' . $selected . '>' . esc_html($cat->name) . '</option>';
}
echo '</select> ';
echo '<label for="tag_id">タグ:</label>';
echo '<select name="tag_id" id="tag_id">';
echo '<option value="0">すべて</option>';
foreach ($tags as $tag) {
$selected = selected($selected_tag, $tag->term_id, false);
echo '<option value="' . esc_attr((string) $tag->term_id) . '" ' . $selected . '>' . esc_html($tag->name) . '</option>';
}
echo '</select> ';
echo '<button type="submit" class="button">絞り込み</button>';
echo '</form>';
// ▼価格編集テーブル
echo '<form method="post">';
wp_nonce_field('save_custom_price_list', 'custom_price_list_nonce');
echo '<table class="widefat fixed striped">';
echo '<thead><tr><th style="max-width:400px;width:400px;">記事タイトル</th><th>価格</th></tr></thead><tbody>';
foreach ($posts as $post) {
$price = get_post_meta($post->ID, 'price', true);
if (!is_scalar($price)) {
echo '<tr><td>' . esc_html($post->post_title) . '</td><td>複合値のため編集対象外</td></tr>';
continue;
}
echo '<tr>';
echo '<td style="max-width:400px;width:400px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">';
echo '<a href="' . esc_url(get_edit_post_link($post->ID, 'raw')) . '">' . esc_html($post->post_title) . '</a>';
echo '</td>';
echo '<td><input type="number" min="0" step="1" name="price[' . $post->ID . ']" value="' . esc_attr($price) . '" /></td>';
echo '</tr>';
}
echo '</tbody></table>';
submit_button('価格を保存する', 'primary', 'custom_price_save');
echo '</form>';
echo '</div>';
}
以上です。
あとは管理画面から確認しましょう。

シンプル版
絞り込みなしのシンプル版も作成しました。

add_action('admin_menu', function () {
add_menu_page(
'価格一覧',
'価格一覧',
'manage_options',
'custom-price-list',
'render_custom_price_list',
'dashicons-money-alt',
20
);
});
function render_custom_price_list()
{
if (!current_user_can('manage_options')) {
wp_die('この画面を表示する権限がありません。');
}
// 投稿を取得(100件まで表示)
$posts = get_posts([
'post_type' => 'post',
'posts_per_page' => 100,
'post_status' => 'publish',
]);
// 保存処理
$posts = array_values(array_filter($posts, function ($post) {
return current_user_can('edit_post', $post->ID);
}));
if (isset($_POST['custom_price_save'])) {
$result = waza_custom_price_bulk_save($posts);
if (is_wp_error($result)) {
echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
} else {
echo '<div class="notice notice-success"><p>' . esc_html((string) $result) . '件の価格を保存しました。</p></div>';
}
}
echo '<div class="wrap">';
echo '<h1>投稿の価格一覧</h1>';
echo '<form method="post">';
wp_nonce_field('save_custom_price_list', 'custom_price_list_nonce');
echo '<table class="widefat fixed striped">';
echo '<thead><tr><th>記事タイトル</th><th>価格</th></tr></thead><tbody>';
foreach ($posts as $post) {
$price = get_post_meta($post->ID, 'price', true);
if (!is_scalar($price)) {
echo '<tr><td>' . esc_html($post->post_title) . '</td><td>複合値のため編集対象外</td></tr>';
continue;
}
echo '<tr>';
echo '<td><a href="' . esc_url(get_edit_post_link($post->ID, 'raw')) . '">' . esc_html($post->post_title) . '</a></td>';
echo '<td><input type="number" min="0" step="1" name="price[' . $post->ID . ']" value="' . esc_attr($price) . '" /></td>';
echo '</tr>';
}
echo '</tbody></table>';
submit_button('価格を保存する', 'primary', 'custom_price_save');
echo '</form>';
echo '</div>';
}
この記事で紹介しているコードは、GitHubのMOTOKI-LLC/WAZA-codeにもまとめています。
今回の修正は公開コードとWordPress公式資料の静的確認に基づきます。修正後コードの実機保存、エディター別の配置、権限ごとの操作、併用プラグイン、マルチサイトは未検証です。既存画像は配置例であり、修正後コードの動作確認画像ではありません。GitHub側の同時更新も確認していません。
停止するときは追加したスニペットを無効化するか、子テーマへ追加した該当コードを取り除きます。停止だけで保存済みpriceメタは消えません。価格を一括削除するコードは追加しないでください。
価格一覧と価格メタボックスのLocal表示例
2026年10月13日に、新規Local環境(WordPress 7.1.3、PHP 8.1.23、Twenty Twenty-Five 1.5)へ、このページに掲載中の共通処理と絞り込み付き一覧のコードだけを追加して撮影しました。シンプル版は同時に追加していません。投稿タイトル・価格はすべて撮影用の架空データです。本番サイトへのコード導入や価格変更は行っていません。
管理メニューの「価格一覧」にはカテゴリー・タグの選択欄、記事タイトル・価格の列、価格を保存するボタンが表示されました。投稿の編集画面では右側に「価格」メタボックスが表示されました。配置はこのエディター・テーマでの例で、すべての構成で同じ位置になるとは限りません。
今回は画面の表示確認と撮影のみです。絞り込み・価格入力・保存・投稿更新は操作していません。0や空欄を含む価格は撮影前に用意した見本であり、画面からの保存や削除が成功した記録ではありません。権限別の操作・複合値・マルチサイト・決済との連携は未検証です。





