Why Custom Fields and Meta Matter
You're building a real estate listing plugin and need to store property details like square footage, number of bedrooms, and listing price alongside the standard post content. WordPress's post meta system gives you a structured way to attach arbitrary data to posts, users, and taxonomies without hacking the database or creating custom tables. Custom fields and meta: a WordPress guide for developers covers the tools and patterns that let you extend post types with exactly the fields your business logic demands.
Whether you're using the REST API to power a headless site, building a complex admin interface, or storing data for calculations and sorting, understanding how to register, store, and retrieve meta safely is foundational. We'll walk through the modern approach—starting with register_post_meta for type safety, then moving through the classic meta box UI, block editor integration, and when to reach for a tool like Advanced Custom Fields.
Registering Meta with register_post_meta
The first step is to declare your meta field. In WordPress 5.3+, register_post_meta lets you define a field's type, sanitization callback, and whether it appears in the REST API. This replaces the old pattern of just calling add_post_meta with a string key and hoping nothing collides.
Here's a practical example. Imagine you're building an e-commerce site and need to store a product's supplier cost on post objects:
add_action( 'init', function() {
register_post_meta(
'post',
'_supplier_cost',
array(
'type' => 'number',
'description' => 'Cost paid to supplier',
'single' => true,
'sanitize_callback' => function( $value ) {
return (float) $value;
},
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
},
'show_in_rest' => true,
)
);
} );Key points here:
- type: Tells WordPress (and REST clients) that this is a number, not a string. Use 'string', 'number', 'integer', or 'boolean'.
- single: Set to true if there's only one value per post. False if you allow repeating values (though you'll retrieve differently).
- sanitize_callback: Always sanitize on input. Here we cast to float to prevent injection or corruption.
- auth_callback: Control who can read or modify this field via REST. Essential for security.
- show_in_rest: Expose the field to the REST API. Omit or set to false if it's admin-only.
Once registered, retrieve the value with get_post_meta( $post_id, '_supplier_cost', true ) and update it with update_post_meta( $post_id, '_supplier_cost', $new_value ). WordPress handles serialization and sanitization automatically.
Building Meta Boxes in the Classic Editor
Meta boxes are the gray panels in the WordPress editor sidebar and below the editor. They're the traditional way to let editors input custom field values. Use add_meta_box to register one, then hook into save_post_* to persist the data.
Here's a complete example for our supplier cost field:
add_action( 'add_meta_boxes', function() {
add_meta_box(
'supplier_details',
'Supplier Information',
function( $post ) {
$cost = get_post_meta( $post->ID, '_supplier_cost', true );
wp_nonce_field( 'save_supplier_details', 'supplier_nonce' );
?>
Cost (USD):
A few critical practices:
- Always use
wp_nonce_fieldand verify it in the save hook. This prevents accidental or malicious bulk actions. - Check the save hook is specific:
save_post_postfires only for posts, not pages or custom types. - Sanitize on input with functions like
sanitize_text_field, even though you registered a callback. - Escape on output:
esc_attrfor attributes,esc_htmlfor text.
This approach works well for simple, admin-focused fields. For more complex UIs or when you need full block editor support, move to the next pattern.
Custom Fields in the Block Editor
The block editor (Gutenberg) changed how we interact with the editor. Meta boxes still work, but they appear in a legacy panel. For a modern experience, create a sidebar plugin that integrates directly with the editor's post settings panel.
Here's a simple React component registered as a block editor plugin:
add_action( 'enqueue_block_editor_assets', function() {
wp_enqueue_script(
'supplier-sidebar',
get_template_directory_uri() . '/blocks/supplier-sidebar.js',
array( 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data' ),
filemtime( get_template_directory() . '/blocks/supplier-sidebar.js' )
);
} );And the JavaScript:
wp.plugins.registerPlugin( 'supplier-sidebar', {
render: function() {
const { PluginDocumentSettingPanel } = wp.editPost;
const { useSelect, useDispatch } = wp.data;
const { TextControl } = wp.components;
const cost = useSelect( ( select ) =>
select( 'core/editor' ).getEditedPostAttribute( 'meta' )._supplier_cost
);
const { editPost } = useDispatch( 'core/editor' );
return (
editPost( { meta: { _supplier_cost: value } } )
}
type="number"
step="0.01"
/>
);
},
} );This approach:
- Reads and writes directly to post meta via the REST API and block editor data store.
- Requires that your meta field is registered with
show_in_rest => true. - Feels native to the block editor experience.
- Can include richer controls: sliders, color pickers, repeaters, etc.
For teams already using React and webpack, this is the modern standard. For simpler needs or less familiar codebases, the meta box approach is still valid.
Advanced Custom Fields for Field-Heavy Projects
If you're managing dozens of custom fields, building repeating field groups, or working with complex conditional logic, Advanced Custom Fields (ACF) accelerates development significantly. ACF handles registration, sanitization, UI, and REST exposure behind a declarative API.
A real-world scenario: you're building a travel booking site and need to store flexible trip itinerary data—multiple day entries, each with activities, notes, and images.
With ACF, you register a field group once:
acf_add_local_field_group( array(
'key' => 'group_trip_itinerary',
'title' => 'Trip Itinerary',
'fields' => array(
array(
'key' => 'field_trip_days',
'name' => 'days',
'label' => 'Days',
'type' => 'repeater',
'sub_fields' => array(
array(
'key' => 'field_day_number',
'name' => 'day_number',
'label' => 'Day Number',
'type' => 'number',
),
array(
'key' => 'field_activities',
'name' => 'activities',
'label' => 'Activities',
'type' => 'textarea',
),
array(
'key' => 'field_day_image',
'name' => 'day_image',
'label' => 'Cover Image',
'type' => 'image',
'return_format' => 'id',
),
),
),
),
'location' => array(
array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'trip',
),
),
),
'show_in_rest' => true,
) );Retrieving and rendering is clean:
$days = get_field( 'days', $post_id );
if ( $days ) {
foreach ( $days as $day ) {
echo 'Day ' . esc_html( $day['day_number'] ) . ': ';
echo esc_html( $day['activities'] );
}
}Benefits:
- Repeating fields and flexible content out of the box.
- Built-in conditional field logic (show/hide based on other field values).
- REST API support with proper type schema.
- No custom JavaScript or React required—works in classic or block editor.
- Database storage is optimized for performance (JSON when appropriate).
The trade-off: ACF is a dependency you manage and keep updated. For simple projects with one or two meta fields, the native WordPress tools are enough. For CMS-heavy work or client projects with scope creep, ACF pays for itself in developer time.
Common Mistakes
Not sanitizing on input or escaping on output: If a meta field accepts user input, sanitize it when saving. If you display it on the frontend, escape it. This is the top source of security issues in WordPress plugins.
Forgetting to register meta before using it: Old code called add_post_meta directly without registration. While this still works, it bypasses type checking and REST API integration. Always use register_post_meta for new code.
Using option names instead of meta for post-specific data: If data belongs to a post, use post meta, not options. Options are global; meta is scoped. Mixing them leads to bugs in multi-site or when copying posts.
Not verifying nonces in meta box save hooks: A nonce prevents CSRF attacks. Always include one in your form and verify it in the save hook, even for internal tools.
Ignoring performance with large meta queries: If you're querying many posts by meta value (e.g., all posts where _supplier_cost > 100), index that meta key or use custom tables for high-volume scenarios.
Key Takeaways
- Use
register_post_metato declare fields with type, sanitization, and REST visibility. This is foundational for modern WordPress development. - Meta boxes via
add_meta_boxare the classic UI. They're reliable but less modern than block editor plugins; use them for simple admin fields or legacy sites. - Block editor sidebar plugins integrate naturally with Gutenberg. They require
show_in_rest => trueon your meta and some React knowledge, but feel native to editors. - ACF (Advanced Custom Fields) is worth adopting for projects with many fields, repeaters, or complex conditional logic. It reduces custom code and speeds up delivery.
- Always sanitize on save, escape on output, and verify nonces. Security is non-negotiable, even for internal tools.
- Choose the simplest tool for your use case. Don't reach for ACF if native meta and a single meta box suffice.






Comments
Loading comments…