Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Custom fields

A book has a price and a page count, an event a date and a place, a job opening a salary. Such data does not fit into the title and the text. WordPress keeps it in metadata — key–value pairs attached to a post.

Reading and writing

update_post_meta($postId, 'book_price', 18);
 
$price = get_post_meta($postId, 'book_price', true);
 
delete_post_meta($postId, 'book_price');

The third argument of get_post_meta() — true — returns a single value. Without it the function returns an array: several values may be stored under one key.

Printing in a template

<?php
// single-book.php
 
$price = get_post_meta(get_the_ID(), 'book_price', true);
$pages = get_post_meta(get_the_ID(), 'book_pages', true);
 
if ($price !== '') {
    echo '<p class="price">$' . esc_html(number_format((float) $price, 2)) . '</p>';
}
 
if ($pages !== '') {
    echo '<p>Pages: ' . (int) $pages . '</p>';
}

If the field does not exist, the function returns an empty string.

A field in the editor

So that an editor can enter the price, a box is added to the post form:

// books.php
 
add_action('add_meta_boxes', function () {
    add_meta_box('book_details', 'Book details', 'books_render_details', 'book', 'side');
});
 
function books_render_details(WP_Post $post): void
{
    wp_nonce_field('book_details', 'book_details_nonce');
 
    $price = get_post_meta($post->ID, 'book_price', true);
    ?>
    <label for="book_price">Price, $</label>
    <input type="number" id="book_price" name="book_price" value="<?php echo esc_attr($price); ?>" min="0">
    <?php
}

wp_nonce_field() adds a hidden field with a token — it is needed when saving.

Saving

// books.php
 
add_action('save_post_book', function (int $postId) {
    if (!isset($_POST['book_details_nonce'])
        || !wp_verify_nonce($_POST['book_details_nonce'], 'book_details')) {
        return;
    }
 
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
 
    if (!current_user_can('edit_post', $postId)) {
        return;
    }
 
    update_post_meta($postId, 'book_price', absint($_POST['book_price'] ?? 0));
});

Three checks: the token is genuine, this is not an autosave, the user has the right to change the post. absint() casts the value to a whole number.

Fields in the REST API

For a field to be available to the block editor and the API, it is registered:

add_action('init', function () {
    register_post_meta('book', 'book_price', [
        'type' => 'integer',
        'single' => true,
        'show_in_rest' => true,
        'sanitize_callback' => 'absint',
    ]);
});

A ready-made plugin

Sites with dozens of fields use the Advanced Custom Fields plugin: fields are created in the dashboard and read in a template with get_field('book_price'). The data still lives in the same metadata.

Searching by fields

Posts can be selected and ordered by a field's value — the next lesson, on WP_Query, covers that.

Common mistake

A save_post handler without checks is a vulnerability: any request with the right field changes the data of someone else's post. Always verify the token with wp_verify_nonce(), the rights with current_user_can(), and clean the value before writing — absint(), sanitize_text_field(), sanitize_email().

Thanks for reading!

Donate to the site — we work hard for you.