2026年11月12日

2026年11月12日

WordPress Custom Fields Not Saving: save_post, Nonces, Block Editor

This article is a localized edition of the Japanese original. Read the Japanese original

This article explains why a custom field value you entered disappears after saving, why your own meta box values aren't stored, or why update_post_meta() seems to do nothing, and how to fix each case. It assumes WordPress with the block editor as of October 2026.

Put the code in your child theme's functions.php or in your own plugin. Back up before making changes and, if you can, test on a staging site first.

Common causes

Cause What happens
The nonce action or field name differs between the output side and the check side Verification stops the handler and nothing is saved
The save handler runs even on requests that don't include the input field The value is overwritten with an empty one, so it disappears after saving
The capability check is wrong Saving fails only for users who aren't administrators
The custom post type's supports doesn't include custom-fields The Custom Fields panel doesn't appear, and meta can't be handled through the REST API
register_post_meta() is missing required settings Saves from the block editor (REST API) are ignored

In the block editor, the save handler runs twice

When you click "Update" in the block editor, two requests are sent in order:

  1. A REST API request that saves the content, title and so on
  2. A request that sends the meta box inputs at the bottom of the screen to post.php (only when there are meta boxes)

The save_post action runs for both requests. The first request does not include your meta box input values, so if you write code like the following, the value is overwritten with an empty one every time.

<?php
// Bad example: overwrites with an empty string even when the field was not sent
add_action( 'save_post', function ( $post_id ) {
	update_post_meta( $post_id, 'my_custom_field', $_POST['my_custom_field'] ?? '' );
} );

Make the save handler run only when the input field and the nonce were actually sent. The code in the following steps does this.

Step 1: Output the meta box and the nonce

<?php
// Add a meta box to the post edit screen
add_action( 'add_meta_boxes', function () {
	add_meta_box(
		'my_meta_box',          // Meta box ID
		'Custom info',          // Heading
		'my_render_meta_box',   // Function that outputs the contents
		'post',                 // Target post type
		'normal',
		'high'
	);
} );

function my_render_meta_box( $post ) {
	// Output the nonce (use the same action and field name as the save side)
	wp_nonce_field( 'my_save_meta', 'my_meta_nonce' );

	$value = get_post_meta( $post->ID, '_my_custom_field', true );

	echo '<label for="my_custom_field">Custom value:</label>';
	echo '<input type="text" id="my_custom_field" name="my_custom_field" value="'
		. esc_attr( $value ) . '" style="width:100%">';
}

If the meta key starts with an underscore, like _my_custom_field, it is treated as "protected meta" and does not appear in the built-in Custom Fields panel. For values you want to edit only through your own meta box, this keeps the same value from being edited in two places.

Step 2: Save with save_post

<?php
// Save the meta box value
add_action( 'save_post', function ( $post_id ) {
	// Do nothing on autosave
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	// Don't save to revisions
	if ( wp_is_post_revision( $post_id ) ) {
		return;
	}

	// Do nothing if there is no nonce (= not a meta box submission) or it is invalid
	if ( ! isset( $_POST['my_meta_nonce'] )
		|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['my_meta_nonce'] ) ), 'my_save_meta' ) ) {
		return;
	}

	// Check edit permission
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	// Save only when the input field was sent
	if ( isset( $_POST['my_custom_field'] ) ) {
		update_post_meta(
			$post_id,
			'_my_custom_field',
			sanitize_text_field( wp_unslash( $_POST['my_custom_field'] ) )
		);
	}
} );

Check the following points:

  • Is the first argument of wp_nonce_field() (the action) the same as the second argument of wp_verify_nonce()?
  • Is the second argument of wp_nonce_field() (the field name) the same as the $_POST key?
  • Do you check the capability with the post ID, as in current_user_can( 'edit_post', $post_id )? (edit_post is a per-post capability, so it isn't evaluated correctly without the ID.)
  • Do you remove slashes from $_POST values with wp_unslash() before sanitizing them?

To target only one post type, use the save_post_post (save_post_{post type}) hook so you don't have to check the post type yourself.

Step 3: Show the Custom Fields panel in the block editor

The built-in Custom Fields panel (where you type meta keys and values directly) is hidden by default in the block editor. The block editor has no "Screen Options," so show it as follows:

  1. Click "⋮" ("Options") at the top right of the edit screen
  2. Open "Preferences"
  3. On the "General" tab, under "Advanced", turn on "Custom fields"
  4. Click "Show & Reload Page" (save your changes before reloading)

If the "Custom fields" option doesn't appear, the post type doesn't support custom-fields (see the next step). Meta keys that start with an underscore are not shown in this panel.

Step 4: Add custom-fields support to a custom post type

<?php
// Register a custom post type (include custom-fields in supports)
add_action( 'init', function () {
	register_post_type( 'my_type', array(
		'label'        => 'My Type',
		'public'       => true,
		'show_in_rest' => true, // Required for editing in the block editor
		'supports'     => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
	) );
} );

To add it to a post type that is already registered, run add_post_type_support( 'my_type', 'custom-fields' ); on init.

Step 5: Register meta that the block editor saves

When meta is edited from a block editor sidebar or a block (in JavaScript), the value is saved through the REST API. If the meta is not registered with register_post_meta() and show_in_rest, the value is not saved.

<?php
// Register meta that can be read and written through the REST API (block editor)
add_action( 'init', function () {
	register_post_meta( 'post', '_my_custom_field', array(
		'show_in_rest'      => true,
		'single'            => true,
		'type'              => 'string',
		'sanitize_callback' => 'sanitize_text_field',
		// Protected meta keys (starting with an underscore) can't be updated through the REST API without auth_callback
		'auth_callback'     => function ( $allowed, $meta_key, $post_id ) {
			return current_user_can( 'edit_post', $post_id );
		},
	) );
} );

Meta is available through the REST API only when the post type supports custom-fields. For custom post types, also check the setting in Step 4.

If you edit the value only through the meta box from Steps 1 and 2, this registration is not required, because meta box values are saved by the request to post.php, not through the REST API.

Step 6: Use a plugin such as ACF

When you have many fields, or need image or repeating inputs, a plugin such as Advanced Custom Fields (ACF) saves you from writing the save handler yourself.

# Install and activate ACF (free version) with WP-CLI
wp plugin install advanced-custom-fields --activate

In the admin screen, go to "ACF" → "Field Groups," create a new field group, add fields, and set the conditions (such as post type) under which the field group is shown. Repeating inputs (the Repeater field) are a feature of the paid ACF PRO.

If values don't save while using ACF, check that the field group's display conditions match the post you are editing, and that your own code isn't also saving a field with the same name.

Step 7: Check whether the save handler runs

When you set WP_DEBUG and WP_DEBUG_LOG to true in wp-config.php, output from error_log() is written to wp-content/debug.log.

<?php
// Temporary check: log which request the save handler ran on
add_action( 'save_post', function ( $post_id ) {
	error_log( sprintf(
		'save_post: id=%d rest=%s nonce=%s field=%s',
		$post_id,
		( defined( 'REST_REQUEST' ) && REST_REQUEST ) ? 'yes' : 'no',
		isset( $_POST['my_meta_nonce'] ) ? 'yes' : 'no',
		isset( $_POST['my_custom_field'] ) ? 'yes' : 'no'
	) );
}, 5 );

When you update a post in the block editor, you should see two lines: rest=yes (the REST API save) and rest=no nonce=yes (the meta box submission). If the second line is missing, the meta box isn't displayed or was added to a different post type. If you see nonce=yes but the value still isn't saved, review the nonce action and the capability check from Step 2.

When you are done, remove this code and set WP_DEBUG back.

Summary

  • In the block editor, save_post runs twice. Save only when the input field and nonce were sent, and never overwrite with an empty value
  • Use the same nonce action and field name on the output and save sides, and check permission with current_user_can( 'edit_post', $post_id )
  • Show the Custom Fields panel from "⋮" → "Preferences" → "General" → "Advanced" → "Custom fields"
  • Include custom-fields in a custom post type's supports
  • Register meta that the block editor saves from JavaScript with register_post_meta() and show_in_rest

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

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