Tips・WordPress custom fields (ACF / SCF)

Working with custom fields using get_post_meta

How to work with custom fields without a plugin. Covers what get_post_meta's third argument means, the update and delete functions, how to reveal the input panel in the block editor, and how this relates to ACF.

You can work with custom fields without a plugin. Reading takes one line of get_post_meta().

<?php $price = get_post_meta(get_the_ID(), 'price', true); ?>
<?php if ($price !== '') : ?>
  <p class="price">$<?php echo esc_html(number_format((int) $price)); ?></p>
<?php endif; ?>

The true in the third argument is the first hurdle. Understand that and the rest is variations.

What the third argument (single) means

Post meta is built so that the same key can hold several values, which gives it two ways to return things.

get_post_meta(get_the_ID(), 'price', true);   // '3300' (the value itself)
get_post_meta(get_the_ID(), 'price', false);  // ['3300'] (an array)
get_post_meta(get_the_ID(), 'price');         // omitted behaves like false = an array

You normally want one value, so add true. Forgetting it and seeing Array on the page is the classic stumble.

Omit the key too and you get every meta value on that post as an array. Handy for debugging.

<?php var_dump(get_post_meta(get_the_ID())); ?>

Revealing the input panel (the standard custom fields panel)

In the block editor, the standard custom fields panel is hidden at first.

  1. Open ”” at the top right of the editing screen → “Preferences
  2. Turn on “Custom fields” under “Panels” (the screen reloads)
  3. A “Custom Fields” panel appears below the editor
  4. Use “Add New” to enter name price and value 3300

To use this panel on a custom post type, include 'custom-fields' in supports when calling register_post_type().

TipsHow to create custom post types (register_post_type)How to create custom post types

Saving, updating and deleting

These three are how you write values from code.

update_post_meta($post_id, 'price', 3300);   // add if missing, update if present (use this by default)
add_post_meta($post_id, 'tag', 'sale');      // add another value under the same key (becomes multi-valued)
delete_post_meta($post_id, 'price');         // delete

When in doubt, update_post_meta(). Since it’s “update if present, add if not,” you never get accidental duplicates.

A note on using values as numbers

Post meta values are stored as strings, without distinguishing types. Pin down the type when you use them for arithmetic or comparison.

<?php $price = (int) get_post_meta(get_the_ID(), 'price', true); ?>
<p>Including tax: $<?php echo esc_html(number_format((int) round($price * 1.1))); ?></p>

Filtering or sorting a list by price needs a type specified for the same reason.

TipsFiltering and sorting by custom field with meta_queryHow to filter and sort with meta_query

The trap when you want to display “0”

<?php if (get_post_meta(get_the_ID(), 'stock', true)) : ?>   <!-- ❌ 0 is treated as "not there" -->
<?php if (get_post_meta(get_the_ID(), 'stock', true) !== '') : ?>   <!-- ✅ -->

On a site where you want to show zero stock or a price of zero, compare against an empty string instead. In PHP, both 0 and '0' are falsy in a condition.

Keys starting with an underscore

Meta keys starting with _ (like _thumbnail_id) are treated as protected meta and don’t appear in the standard custom fields panel.

  • Values you want people to edit on screen → ordinary names like price
  • Internal values for code only → underscore-prefixed names like _my_internal_flag

The internal data WordPress and plugins use is stored in this form too.

Register it if you’ll use the block editor or the REST API

To read and write meta from the block editor’s sidebar or through the REST API, declare it with register_post_meta().

add_action('init', function () {
  register_post_meta('shohin', 'price', [
    'type'          => 'number',
    'single'        => true,
    'show_in_rest'  => true,
    'auth_callback' => function () {
      return current_user_can('edit_posts');
    },
  ]);
});

Setting show_in_rest makes the value usable from the block editor (the JavaScript side) as well.

How this relates to ACF (SCF)

ACF stores things as the same post meta internally, so get_post_meta() can read them. There are differences, though.

get_post_meta()get_field() (ACF/SCF)
What comes backThe raw stored valueA value formatted according to its type
Image fieldThe ID as a stringID, URL or array, depending on the setting
Repeaters and groupsRaw row counts and internal keysA usable array

If ACF is installed, get_field() is the reliable choice. get_post_meta() earns its keep when you’re building a plugin-free setup, or a theme that doesn’t break when the plugin is deactivated.

TipsACF field types: a cheat sheet for displaying each oneHow to write each field type when you are using ACF

Building your own input panel (add_meta_box)

The standard “name and value” panel is hard for the people running the site to understand. To build a dedicated input, use add_meta_box().

add_action('add_meta_boxes', function () {
  add_meta_box('shohin_price', 'Price', function ($post) {
    $value = get_post_meta($post->ID, 'price', true);
    wp_nonce_field('shohin_price_save', 'shohin_price_nonce');
    echo '<input type="number" name="price" value="' . esc_attr($value) . '">';
  }, 'shohin', 'side');
});

add_action('save_post_shohin', function ($post_id) {
  if (!isset($_POST['shohin_price_nonce'])
      || !wp_verify_nonce($_POST['shohin_price_nonce'], 'shohin_price_save')) {
    return;
  }
  if (!current_user_can('edit_post', $post_id)) {
    return;
  }
  update_post_meta($post_id, 'price', (int) ($_POST['price'] ?? 0));
});

The save routine must include nonce verification and a capability check. Skip those and you’ve left a hole that lets values be written from outside.

TipsChoosing the right escaping function (esc_html, esc_attr, esc_url)Choosing between escaping functions, and how nonces fit in

Summary

  1. Read with get_post_meta($id, 'key', true). The true says “take it as a single value”
  2. Write with update_post_meta() by default (adds if missing, updates if present)
  3. Values are stored as strings, so pin numbers down with (int). To display 0, compare against an empty string
  4. If ACF is installed, get_field() is reliable. Core features suit plugin-independent themes

Once you know core can handle this, “we can’t build it without a plugin” stops being a thing.

FAQ

What is the true in get_post_meta's third argument?
It says to retrieve a single value. With true you get the value itself (a string); with false or omitted you get an array. You normally want one value, so you add true.
The custom fields panel doesn't appear in the block editor.
Turn it on via "⋮" at the top right of the editing screen → "Preferences" → "Custom fields" under Panels (the screen reloads). For custom post types you also need 'custom-fields' in supports.
Keys starting with an underscore don't appear in the editing screen.
Meta keys starting with _ are treated as "protected meta" and don't appear in the standard custom fields panel. They're used for internal values read and written by code.
Can get_post_meta read values entered through ACF?
Yes, because ACF (SCF) also stores them as post meta. But ACF's own formatting—image field return value conversion and so on—doesn't apply, so get_field() is normally the more reliable choice.