WordPress Custom Fields – Mastering Advanced WordPress Development Series

WordPress Hooks and Filters – Mastering Advanced WordPress Development Series

Every WordPress post carries more than its title and body. Behind each one sits a slot for extra data you define: an event date, a subtitle, a product code, a “featured until” timestamp. WordPress calls this data post meta, and the older UI calls it custom fields. Same thing.

This entry in the Advanced WordPress Development series walks through building custom fields by hand, with PHP and the core meta functions. You’ll end up with a real editor box, a save routine that’s actually safe, and output on the front end. We’ll also point you at the plugins worth reaching for when hand-coding isn’t the right trade.

Prerequisites

You’ll want to be comfortable reading PHP and dropping code into a plugin or a theme’s functions file. A running WordPress install to test against helps too. Nothing here is exotic, but we won’t stop to explain what a hook is.

Creating Custom Fields in WordPress

Under the hood, a custom field is a row in the wp_postmeta table tied to a post ID. You read it with get_post_meta() and write it with update_post_meta(). That’s the whole data model.

What takes the work is the editor experience. WordPress won’t build a nice input for you, so we use add_meta_box() to add our own panel to the edit screen, then wire up saving ourselves.

Step 1: Create a New Meta Box

Start by registering the box. This tells WordPress to render a panel on the post and page edit screens:

PHP
<?php
/**
 * Add a custom meta box to the post editor screen.
 */
function myplugin_add_custom_box() {
    $screens = array( 'post', 'page' );
    foreach ( $screens as $screen ) {
        add_meta_box(
            'myplugin_box_id',                 // Unique ID
            'My Custom Box',                   // Box title
            'myplugin_custom_box_html',        // Content callback
            $screen                            // Post type
        );
    }
}
add_action( 'add_meta_boxes', 'myplugin_add_custom_box' );

The four arguments to add_meta_box() we’re passing here:

  • myplugin_box_id: a unique ID for the box. Any string works, as long as nothing else uses it.
  • My Custom Box: the heading shown on the edit screen.
  • myplugin_custom_box_html: the callback that prints the box contents. We write it next.
  • $screen: the post type to attach to. We loop over both post and page.

Step 2: Create the Meta Box Content

Now the callback. It reads any saved value and prints a text input. It also drops in a nonce, which we’ll check on save:

PHP
/**
 * Output the HTML for the custom meta box.
 */
function myplugin_custom_box_html( $post ) {
    wp_nonce_field( 'myplugin_save_custom_box', 'myplugin_custom_box_nonce' );
    $value = get_post_meta( $post->ID, '_myplugin_custom_field', true );
    ?>
    <label for="myplugin_custom_field">Custom Field</label>
    <input type="text" id="myplugin_custom_field" name="myplugin_custom_field" value="<?php echo esc_attr( $value ); ?>">
    <?php
}

wp_nonce_field() writes a hidden token into the form so we can confirm the save request really came from this screen. get_post_meta() with a true third argument pulls back a single value instead of an array. And notice we escape the stored value with esc_attr() before it lands in the input; escape on output, always, even for data you put there yourself.

One detail worth knowing: the meta key here starts with an underscore, _myplugin_custom_field. WordPress treats underscore-prefixed keys as protected, so they won’t show up in the generic Custom Fields panel. That’s usually what you want for plugin-managed data.

Step 3: Save the Custom Field

The box exists and it renders. Now we catch the save and store the input. This is the part people get wrong, so read the guards carefully:

PHP
<?php
/**
 * Save the custom meta box data.
 */
function myplugin_save_custom_box( $post_id ) {
    if ( ! isset( $_POST['myplugin_custom_box_nonce'] ) ) {
        return;
    }
    if ( ! wp_verify_nonce( $_POST['myplugin_custom_box_nonce'], 'myplugin_save_custom_box' ) ) {
        return;
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }
    if ( ! isset( $_POST['myplugin_custom_field'] ) ) {
        return;
    }
    $data = sanitize_text_field( $_POST['myplugin_custom_field'] );
    update_post_meta( $post_id, '_myplugin_custom_field', $data );
}
add_action( 'save_post', 'myplugin_save_custom_box' );

Every check earns its place. We bail if the nonce is missing or fails, so a forged request goes nowhere. We bail during autosave, because that fires without our form data and would wipe the field. We bail if the current user can’t edit this post, since a valid nonce is not the same as permission. Only after all of that do we sanitize with sanitize_text_field() and write with update_post_meta().

That trio, verify a nonce, check the capability, sanitize the input, is the non-negotiable pattern for any save handler in WordPress. Skip one and you’ve opened a hole.

Step 4: Display the Custom Field on the Frontend

Storing the value is only useful if you show it. Here we append it to the post content:

PHP
<?php
/**
 * Display the custom field on the frontend.
 */
function myplugin_display_custom_field() {
    global $post;
    $value = get_post_meta( $post->ID, '_myplugin_custom_field', true );
    if ( $value ) {
        echo '<p>Custom Field: ' . esc_html( $value ) . '</p>';
    }
}
add_action( 'the_content', 'myplugin_display_custom_field' );

Hooking the_content tacks our output onto the end of the post body. We read the value, and if it’s set, print it through esc_html(). In real projects you’ll more often call get_post_meta() directly inside a template than filter the content, but the escaping rule holds either way.

A note on the block editor

The meta box above still works, but it’s a classic-editor construct. If you’re building for the block editor, register your meta with register_post_meta() and set show_in_rest to true. That exposes the field over the REST API so a block, a sidebar panel, or the site editor can read and write it. The core meta functions underneath don’t change; you’re just giving Gutenberg a supported way in.

Recommended Plugins

Hand-coding is the right call when you want zero dependencies and full control. For anything with many fields, or a non-developer maintaining it, a plugin saves real time:

  • Advanced Custom Fields: the long-standing favorite for building fields through a UI, with field types like date pickers and repeaters. WordPress.org also hosts Secure Custom Fields, a maintained fork of ACF adopted in late 2024, if you’d rather stay on a community-run version.
  • Custom Post Type UI: registers custom post types and taxonomies through a form, which pairs well with custom fields when you’re modeling something more structured.
  • Pods: covers post types, taxonomies, and fields in one visual tool, with extras like relationship and file fields.

Wrapping up

Custom fields are how you make WordPress hold the data your project actually needs, beyond the built-in title and body. You’ve now got the full loop by hand: register a box, render an input, save it safely, and show it. The guarded save handler is the piece to internalize, because that same shape protects every form you’ll write.

Before you ship, test the save and delete paths on a staging site, and keep your code commented so the next person (often you, months later) can follow it. When the field count grows or a client needs to manage them, reach for ACF, SCF, or Pods and skip the boilerplate. Match the tool to the job, not the other way around.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top