Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 010 / 100

Custom fields and meta: a WordPress guide for developers

Store extra data for posts.

Expona Admin7 min read

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:

php
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:

php
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_field and verify it in the save hook. This prevents accidental or malicious bulk actions.
  • Check the save hook is specific: save_post_post fires 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_attr for attributes, esc_html for 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:

php
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:

php
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:

php
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:

php
$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_meta to declare fields with type, sanitization, and REST visibility. This is foundational for modern WordPress development.
  • Meta boxes via add_meta_box are 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 => true on 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles