iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Meta Fields

Custom meta fields attach extra structured data to posts, pages, users, terms, or comments. Use register_post_meta for typed, REST-exposed fields that work in the block editor; reach for ACF / Meta Box / Pods only when you outgrow the basics.

register_post_meta, REST, ACF, queries

EXAMPLE
<?php
// 1) Register a post meta field — the modern way (5.5+)
add_action( 'init', function () {
    register_post_meta(
        'post',                                            // post type ('' = all)
        'subtitle',
        array(
            'type'         => 'string',
            'description'  => 'Subtitle displayed below the title',
            'single'       => true,                          // one value, not array
            'default'      => '',
            'show_in_rest' => true,                          // expose via REST + block editor
            'auth_callback' => function () {
                return current_user_can( 'edit_posts' );
            },
            'sanitize_callback' => 'sanitize_text_field',
        )
    );
} );

// 2) Get / update / delete
$value = get_post_meta( $post_id, 'subtitle', true );        // single
$all   = get_post_meta( $post_id, 'subtitle', false );      // array (when single=false)

update_post_meta( $post_id, 'subtitle', 'Modern WordPress development' );
delete_post_meta( $post_id, 'subtitle' );

// 3) REST exposure
// With show_in_rest => true, the field appears in:
// GET  /wp-json/wp/v2/posts/{id}        → "meta": { "subtitle": "..." }
// POST /wp-json/wp/v2/posts/{id}       
//     body: { "meta": { "subtitle": "New value" } }

// Block editor automatically supports binding ( useEntityProp ):
import { useEntityProp } from '@wordpress/core-data';
const [ meta, setMeta ] = useEntityProp( 'postType', 'post', 'meta' );
<input value={meta.subtitle ?? ''} onChange={(e) => setMeta({ ...meta, subtitle: e.target.value })} />

// 4) Rich types (object/array)
register_post_meta( 'product', 'specs', array(
    'type'         => 'object',
    'single'       => true,
    'show_in_rest' => array(
        'schema' => array(
            'type'       => 'object',
            'properties' => array(
                'weight_g'    => array( 'type' => 'integer' ),
                'dimensions'  => array( 'type' => 'string'  ),
                'color'       => array( 'type' => 'string'  ),
            ),
        ),
    ),
) );

update_post_meta( $post_id, 'specs', array(
    'weight_g'   => 250,
    'dimensions' => '20×10×5 cm',
    'color'      => 'red',
) );

// 5) Query posts by meta
$query = new WP_Query( array(
    'post_type'   => 'product',
    'meta_query'  => array(
        'relation' => 'AND',
        array(
            'key'     => 'in_stock',
            'value'   => '1',
            'compare' => '=',
        ),
        array(
            'key'     => 'price_cents',
            'value'   => array( 1000, 5000 ),
            'compare' => 'BETWEEN',
            'type'    => 'NUMERIC',
        ),
    ),
    'orderby'     => 'meta_value_num',
    'meta_key'    => 'price_cents',
    'order'       => 'ASC',
) );

// CAREFUL: meta_query joins are SLOW at scale. Index meta keys with custom DB indexes or move
// high-cardinality fields to a custom table.

// 6) Show in templates
<?php
if ( $subtitle = get_post_meta( get_the_ID(), 'subtitle', true ) ) :
?>
    <p class="entry-subtitle"><?php echo esc_html( $subtitle ); ?></p>
<?php endif; ?>

// 7) User meta
register_meta( 'user', 'twitter_handle', array(
    'type'         => 'string',
    'show_in_rest' => true,
    'single'       => true,
) );

get_user_meta( $user_id, 'twitter_handle', true );
update_user_meta( $user_id, 'twitter_handle', '@example' );

// Display + save in user profile screen:
add_action( 'show_user_profile', 'add_twitter_field' );
add_action( 'edit_user_profile', 'add_twitter_field' );
add_action( 'personal_options_update', 'save_twitter_field' );
add_action( 'edit_user_profile_update', 'save_twitter_field' );

function add_twitter_field( $user ) {
    $handle = esc_attr( get_user_meta( $user->ID, 'twitter_handle', true ) );
    echo "<table class='form-table'><tr><th><label for='twitter_handle'>Twitter</label></th>
               <td><input name='twitter_handle' id='twitter_handle' value='{$handle}' /></td></tr></table>";
}
function save_twitter_field( $user_id ) {
    if ( ! current_user_can( 'edit_user', $user_id ) ) return;
    update_user_meta( $user_id, 'twitter_handle', sanitize_text_field( $_POST['twitter_handle'] ?? '' ) );
}

// 8) Term meta
register_term_meta( 'category', 'banner_image_id', array(
    'type'         => 'integer',
    'single'       => true,
    'show_in_rest' => true,
) );
update_term_meta( $term_id, 'banner_image_id', $attachment_id );
$image_id = get_term_meta( $term_id, 'banner_image_id', true );

// 9) Hidden meta (won't show in custom field UI)
register_post_meta( 'post', '_internal_id', array(
    'type'   => 'string',
    'single' => true,
) );
// Prefixing the key with '_' makes it hidden from the legacy Custom Fields metabox.

// 10) ACF / Meta Box / Pods
// Plugins offer richer UI: repeaters, flexible content, conditional fields, REST API integration.
// • ACF Pro — most popular; great UX; commercial license
// • Meta Box — generous free version; developer-friendly
// • Pods    — custom post types + content fields with relationships
//
// They all store data in postmeta by default (compatible with WP_Query),
// or in custom tables (ACF Pro option, Meta Box premium) for performance.

// 11) Custom table for high-cardinality meta
// When meta queries dominate your slow log, model the data as a custom table:
//   wp_my_products_specs (product_id, weight_g, dimensions, color, …)
// Query via $wpdb->prepare( 'SELECT … WHERE product_id IN (…)' )
// Index the relevant columns; orders of magnitude faster than postmeta joins.

// 12) Validation + sanitisation
register_post_meta( 'post', 'price_cents', array(
    'type'         => 'integer',
    'show_in_rest' => true,
    'sanitize_callback' => function ( $v ) {
        $v = (int) $v;
        return $v < 0 ? 0 : $v;
    },
    'auth_callback' => function () { return current_user_can( 'edit_posts' ); },
) );

// 13) Common bugs
// • show_in_rest=true but field not appearing in block editor → register on 'init', not 'wp_loaded'
// • Meta queries that JOIN multiple meta tables → exponential slowdown; index or move to custom table
// • Storing big blobs in meta → serialised PHP arrays balloon postmeta; use a custom table
// • Forgetting auth_callback → anonymous REST callers can write meta
// • Slug typos between register + get → silent missing field
// • Storing dates as strings → can't sort or range-query; store as UNIX timestamps or ISO strings
// • Pre-WP 5.5: meta wasn't typed; use register_post_meta with schema for safety
// • Querying with type='NUMERIC' but value is string '0050' → padding may break comparisons
// • ACF + register_post_meta colliding on same key → drift; choose one source

Why it matters

register_post_meta with show_in_rest and an explicit schema is the modern way to add typed structured data — the block editor and REST API just work. Promote to a plugin (ACF, Meta Box) for repeaters and rich UI; promote to a custom table when meta_query joins start dominating your slow query log.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
update_post_meta($post_id, 'isbn', '978-…');
get_post_meta($post_id, 'isbn', true);
Try it Yourself »

Discussion

Loading…