Your support helps keep this blog running! Secure payments via Paypal and Stripe.
For my work, I often receive requests to do customizations for my clients. One of the most common requests is adding new custom fields to posts or pages. The benefit of using custom fields is for organizing and grouping data.
There are two common ways to add custom fields to your WordPress site. One is using a plugin, and the other is using the WordPress API. In this post, I will share both methods.
Add Custom Fields Using the Plugin
This method is the easy and fast way to add custom fields to your posts, pages, or custom post types.
- Advanced Custom Fields:
- The plugin provides both a free and paid version. Their documentation provides details and is easy to follow. I personally recommend this plugin.
- Meta Box – WordPress Custom Fields Framework:
- This plugin provides both free and paid versions. The free version doesn’t provide any UI (User Interface) for creating custom fields. Instead, it provides the online generator from their website that allows you to generate the custom fields and setting page code, then you will copy that generated code and paste it into the functions.php of your current theme (child theme). If you prefer the UI (User Interface), you need to pay for the premium version. However, the plugin provides some free extensions that may be useful, such as Elementor integrator, Beaver themer integrator, Rest API, Custom taxonomy UI, Custom post type UI, Yoast SEO support, and more.
Add Custom Fields Without a Plugin
If you want to manage the code by yourself, you will create the custom fields manually without any help from the plugins. We will use the add_meta_box function from WordPress API for this purpose.
Create a Child Theme – Recommended
For this tutorial, I recommend that you create the child theme before continuing. Follow this link to create the child theme correctly. Once the child theme is created, create a new functions.php under your child theme folder.
Create Project Post Type
In my tutorial, I create the project post type, which is a custom post type. If you don’t know how to create the custom post type, you can add the code below to the functions.php of your child theme. The code will create a new project custom post type for your WordPress.
Note
I recommend you create the custom post type through your own plugin rather than the functions.php. In this tutorial, I try to keep the content as short as possible. Follow this link to create the custom post type through your own plugin.
/**
* ## Strongly recommend to add the custom post type and custom taxonomy in your own plugin rather than functions.php.
* ## Because if you change the theme, the stored content will be disappear since the snippet code is in the functions.php.
* Create custom post type names "mms_project_cpt"
*/
// # custom post type
add_action('init', 'project_post_type');
function project_post_type()
{
// custom post type name is 20 maximum.
register_post_type(
'mms_project_cpt',
array(
'labels' => array(
'name' => __('Projects', 'your-textdomain'),
'singular_name' => __('Project', 'your-textdomain'),
'add_new' => __('Add New Project', 'your-textdomain'),
'add_new_item' => __('Add New Project', 'your-textdomain'),
'edit' => __('Edit', 'your-textdomain'),
'edit_item' => __('Edit Project', 'your-textdomain'),
'new_item' => __('New Project', 'your-textdomain'),
'view' => __('View', 'your-textdomain'),
'view_item' => __('View Project', 'your-textdomain'),
'search_items' => __('Search Project', 'your-textdomain'),
'not_found' => __('No Project found', 'your-textdomain'),
'not_found_in_trash' => __('No Project found in Trash', 'your-textdomain'),
),
'public' => true,
'show_ui' => true,
'menu_position' => 2,
'supports' => array('title'),
'rewrite' => array('slug' => __('projects', 'your-textdomain')),
'has_archive' => true, // If not set, you can not use the archive project page.
'menu_icon' => 'dashicons-superhero'
)
);
}After you save the functions.php and reload the WordPress back end, you will see the new project menu as shown below.

You can find more parameters of the register_post_type function from this link.
Custom Fields We Will Create
For this tutorial, I want to add five custom fields for the project post type.
- project image (image field type)
- project description (textarea field type)
- time spent on the project (number field type)
- project website (URL field type)
- project published date (date field type)
Now we know how many fields we are gonna add. Next, we will start to add those fields to the project page at the back end.
Basically, We Will Do Four Steps
- Step 1 is creating the meta box and using the meta box with the project post type.
- Step 2 is handling the data validation
- Step 3 is saving the custom field into the database.
- Step 4 is retrieving the custom field and displaying it on the front end.
Step 1: Create the meta box and use the meta box with the project post type
We will register the meta box on the project page. To do that, add the code below to the functions.php of your child theme.
/**
* Register meta box(es).
*/
function wpar_register_meta_boxes() {
add_meta_box( 'proj-fields-group', __( 'Project Meta Box', 'your-textdomain' ), 'wpar_my_display_callback', 'mms_project_cpt' );
}
add_action( 'add_meta_boxes', 'wpar_register_meta_boxes' );Now, click on Add New Project menu, you will see the new Project Meta Box appearing as below. Notice that mms_project_cpt is our project post type that we registered earlier.

Customize WordPress media uploader (JS knowledge is requested)
For our image field, we will use the default image uploader (Media Library) from WordPress. To do that, we will modify the JS code from wp.media, which is a default JS function from WordPress.
- First, create a new JS folder under your child theme folder
- Secondly, create a new project_img_uploader.js under the JS folder
- Finally, add the code below to the project_img_uploader.js
// # Javascript Reference/wp-media
// https://codex.wordpress.org/Javascript_Reference/wp.media
jQuery(function ($) {
$('body').on('click', '.wpar_upload_image_button', function (e) {
e.preventDefault();
var button = $(this),
wpar_uploader = wp.media({
title: 'Custom image',
library: {
// # We scope to display the images that attach to our post ID only.
// You can remove the uploadedTo option if you want to display all images from the media library.
// uploadedTo: wp.media.view.settings.post.id,
// # You can change the file type to display. The file type is the same as Media Library support.
// For example, you can set the type as
// type: [ 'video', 'image' ]
type: 'image'
},
button: {
text: 'Use this image'
},
multiple: false
}).on('select', function () {
var attachment = wpar_uploader.state().get('selection').first().toJSON();
// # Change the #proj-image to your input ID
$('#proj-image').val(attachment.url);
})
.open();
});
});Next, we have to enqueue this project_img_uploader.js into our child theme. Add the code below to the functions.php of your child theme.
/**
* Customize WordPress media uploader for our project post type
* You want to customize the media uploader code from the link to meet your need.
* https://codex.wordpress.org/Javascript_Reference/wp.media
*/
function wpar_include_script() {
if ( ! did_action( 'wp_enqueue_media' ) ) {
// https://developer.wordpress.org/reference/functions/wp_enqueue_media/
wp_enqueue_media();
}
wp_enqueue_script( 'wpar_proj-img_uploader', get_stylesheet_directory_uri() . '/js/project_img_uploader.js', array('jquery'), null, false );
}
// https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/
add_action( 'admin_enqueue_scripts', 'wpar_include_script' );
Display the custom fields on the admin page
Next, we will add the callback function for displaying the custom fields in the project meta box that we create.
Add the code below to the functions.php.
/**
* Meta box display callback.
*
* @param WP_Post $post Current post object.
*/
function wpar_my_display_callback( $post ) {
// # Display code/markup goes here. Don't forget to include nonces!
// https://developer.wordpress.org/reference/functions/wp_nonce_field/
// We will check the nonce when we are saving the field value.
wp_nonce_field( basename( __FILE__ ), 'wpar_project_nonce' );
// # Display all custom fileds
echo '<table>';
// 1.project image field
echo '<tr>';
echo '<td style="vertical-align: top; background: #f6f6f6; padding: 5px 10px;">';
echo '<a href="#" class="button button-secondary wpar_upload_image_button">'. __( 'Upload Image', 'your-textdomain' ) .'</a>';
echo '</td>';
echo '<td>';
$proj_image = get_post_meta( $post->ID, '_proj-image', true );
echo '<input type="text" id="proj-image" name="proj-image" style="width:100%;" readonly="readonly">';
echo '<p style="font-size: 90%; color: #777777;">Upload the project image</p>';
if( strlen($proj_image) > 0 ) {
echo '<img src="'. esc_url($proj_image).'" style="width: 500px; border: 1px solid #999999;">';
}
echo '</td>';
echo '</tr>';
// 2.project description field
echo '<tr>';
echo '<td style="vertical-align: top; background: #f6f6f6; padding: 5px 10px;">';
echo '<label for="proj-website">' . __( 'Project Description', 'your-textdomain' ) . '</label> ';
echo '</td>';
echo '<td>';
$proj_desc = get_post_meta( $post->ID, '_proj-desc', true );
echo '<textarea name="proj-desc" id="proj-desc" cols="50" rows="3" placeholder="Your project description here..." style="width:100%;" maxlength="200">'. esc_html($proj_desc) .'</textarea>';
echo '</td>';
echo '</tr>';
// 3.number of days to spend on the project field
echo '<tr>';
echo '<td style="vertical-align: top; background: #f6f6f6; padding: 5px 10px;">';
echo '<label for="proj-days">' . __( 'Time spent on the project', 'your-textdomain' ) . '</label> ';
echo '</td>';
echo '<td>';
$proj_days = get_post_meta( $post->ID, '_proj-days', true );
if(strlen($proj_days) == 0){
$proj_days = 1;
}
echo '<input type="number" name="proj-days" id="proj-days" min="1" value="'. esc_html($proj_days) .'">' . ' ' . __( 'days', 'your-textdomain' );
echo '</td>';
echo '</tr>';
// 4.project website field
echo '<tr>';
echo '<td style="vertical-align: top; background: #f6f6f6; padding: 5px 10px;">';
echo '<label for="proj-website">' . __( 'Website', 'your-textdomain' ) . '</label> ';
echo '</td>';
echo '<td>';
$proj_website = get_post_meta( $post->ID, '_proj-website', true );
echo '<input type="url" name="proj-website" id="proj-website" value="'. esc_url($proj_website) .'" style="width:100%;">';
echo '</td>';
echo '</tr>';
// 4.project published date field
echo '<tr>';
echo '<td style="vertical-align: top; background: #f6f6f6; padding: 5px 10px;">';
echo '<label for="proj-published">' . __( 'Published date', 'your-textdomain' ) . '</label> ';
echo '</td>';
echo '<td>';
$proj_published = get_post_meta( $post->ID, '_proj-published', true );
echo '<input type="date" name="proj-published" id="proj-published" value="'. esc_html($proj_published) .'">';
echo '</td>';
echo '</tr>';
echo '</table>';
}As you can see, I added the table HTML to organize the label and input field on the meta box. You can add any HTML tag and style as you like. Also, I use data sanitization escaping for securing output. Escaping is the process of securing output by stripping out unwanted data, like malformed HTML or script tags, preventing this data from being seen as code. Escaping helps secure your data prior to rendering it for the end user and prevents XSS (Cross-site scripting) attacks.
The result from the code above is below.

When you add the value into the input fields and save. We will do the data validation in the next step.
Step 2: Handle data validation
In this step, we will do;
- security check
- data validation
- Save the data to the database
Add the code below to the functions.php of your child theme.
/**
* Save meta box content.
*
* @param int $post_id Post ID
*/
function wpar_save_meta_box( $post_id ) {
/**
* # security check
*
* We need to verify this came from the our screen and with proper authorization,
* because save_post can be triggered at other times.
*
* Don't forget to include nonce checks!
*/
// Check if our nonce is set.
if ( ! isset( $_POST['wpar_project_nonce'] ) ) {
return $post_id;
}
$nonce = $_POST['wpar_project_nonce'];
// Verify that the nonce is valid.
if ( ! wp_verify_nonce( $nonce, basename( __FILE__ ) ) ) {
return $post_id;
}
// If this is an autosave, our form has not been submitted,
// so we don't want to do anything.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return $post_id;
}
// Check the user's permissions.
if ( 'mms_project_cpt' == $_POST['post_type'] ) {
if ( ! current_user_can( 'edit_page', $post_id ) ) {
return $post_id;
}
} else {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return $post_id;
}
}
// # check data validation
// https://developer.wordpress.org/themes/theme-security/data-validation
// I use the HTML attributes for the data validation which works for the modern browsers.
// For the old browser, the data validation may not work.
// So you can integrate the jquery validation plugin(https://jqueryvalidation.org) or ajax validation to do the data validation instead.
// # Now, our data is safe for saving to the database
// ## Sanitize the user input.
// https://developer.wordpress.org/reference/functions/sanitize_text_field/
$title = sanitize_text_field($_POST['title']);
$proj_image = esc_url_raw($_POST['proj-image']);
$proj_desc = sanitize_text_field($_POST['proj-desc']);
$proj_days = sanitize_text_field($_POST['proj-days']);
$proj_website = esc_url_raw($_POST['proj-website']);
$proj_published = sanitize_text_field($_POST['proj-published']);
// ## Update the meta field(custom field).
// https://developer.wordpress.org/reference/functions/update_post_meta/
// If the meta field never been saved to the wp_postmeta table before,
// update_post_meta function will save the meta field for you.
update_post_meta( $post_id, 'title', $title );
update_post_meta( $post_id, '_proj-image', $proj_image );
update_post_meta( $post_id, '_proj-desc', $proj_desc );
update_post_meta( $post_id, '_proj-days', $proj_days );
update_post_meta( $post_id, '_proj-website', $proj_website );
update_post_meta( $post_id, '_proj-published', $proj_published );
// ## Delete the meta field(custom field)
// The meta field value will stay with the post ID until the post is deleted permanently.
// You don't need to delete the meta field manually.
}
// https://developer.wordpress.org/reference/hooks/save_post/
add_action( 'save_post', 'wpar_save_meta_box' );I added a useful comment to explain each snippet of code. Hope it makes it clear.
Step 3: Saving the data into the database
In step 2, I use the update_post_meta function. This function will save the data into the wp_postmeta table, which stores all postmeta values in WordPress. If our custom fields have never been saved in the wp_postmeta table before, this function will create a new entry in the wp_postmeta table for us. If our custom fields are already stored in the wp_postmeta table, the update_post_meta function will update the data. And that’s how we save the data into the database using the update_post_meta function.
Wp_postmeta table
Once the data is saved to the database, you can check the data from the wp_postmeta table. Each entry is saved with four fields, which are meta_id, post_id, meta_key, and meta_value.
You can use the SQL below to query the data that we just saved. I use phpMyAdmin for performing the query.
SELECT * FROM `wp_postmeta`
where `meta_key` IN ('_proj-image','_proj-desc', '_proj-desc', '_proj-days', '_proj-website', '_proj-published')

Below is how the project page looks after saving the data on the back end.


Step 4: Retrieve the data and display on the front end
Now we want to display our custom field value on the front end. Basically, you can display the custom field value anywhere on the site. But in this tutorial, I will create the project template(single and archive templates) and display our custom field value in there.
Flush WordPress Permalinks – Attention!
Whenever a new custom post type is added through functions.php or a plugin, usually WordPress permalinks need to be flushed. Follow these three steps below.
- At the WordPress admin panel, go to Settings>Permalinks
- Click on Save Changes. Just click on the button, even though you don’t change any settings there.
- Now permalinks and rewrite rules are flushed.
If you don’t flush the permalinks after we create the new custom post type, you may experience the 404 error(File Not Found) when you visit the custom post type page(single, archive, etc).
Create our single project page
In your child theme folder, create a new single-mms_project_cpt.php while mms_project_cpt is our project post type. More details can be found here.
Now, add the code below to the single-mms_project_cpt.php
<?php
/**
* The template for displaying single post type.
*
* Copy code from the singular parent theme file.
*
*/
get_header();
?>
<main id="site-content" role="main">
<article <?php post_class(); ?> id="post-<?php the_ID(); ?>">
<?php
// # display the title
get_template_part('template-parts/entry-header');
?>
<div class="post-inner <?php echo is_page_template('templates/template-full-width.php') ? '' : 'thin'; ?> ">
<div class="entry-content">
<?php
// # get the current post ID
$post_id = get_the_ID();
// # check empty value
if (!empty($post_id)) {
// # retrieve the custom field value
// https://developer.wordpress.org/reference/functions/get_post_type/
$proj_image = get_post_meta($post_id, '_proj-image', true);
$proj_desc = get_post_meta($post_id, '_proj-desc', true);
$proj_days = get_post_meta($post_id, '_proj-days', true);
$proj_website = get_post_meta($post_id, '_proj-website', true);
$proj_published = get_post_meta($post_id, '_proj-published', true);
// # securing output
// https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/#escaping-securing-output
if (!empty($proj_image))
$proj_image = esc_url($proj_image);
if (!empty($proj_desc))
$proj_desc = esc_textarea($proj_desc);
if (!empty($proj_days))
$proj_days = esc_html($proj_days);
if (!empty($proj_website))
$proj_website = esc_url($proj_website);
if (!empty($proj_published))
$proj_published = esc_html($proj_published);
// # display the custom field value
// project image
echo '<p><b>' . __('Project Image', 'your-textdomain') . ': </b></p>';
echo '<p padding: 10px;">' . '<img src="' . $proj_image . '" alt="" style="max-width:100%; border:1px solid #dddddd;">' . '</p>';
// project description
echo '<p><b>' . __('Project Description', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_desc . '</p>';
// days
echo '<p><b>' . __('Time spent on the project ', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_days . '</p>';
// website
echo '<p><b>' . __('Website', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_website . '</p>';
// published date
echo '<p><b>' . __('Published date', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_published . '</p>';
echo '<hr>';
}
?>
</div><!-- .entry-content -->
</div><!-- .post-inner -->
</article> <!-- .mms_project_cpt -->
</main><!-- #site-content -->
<?php get_template_part('template-parts/footer-menus-widgets'); ?>
<?php get_footer(); ?>I use the TwentyTwenty theme as the parent theme. What I did I just copy the code from the singular template from the parent theme and modify the code, as you can see from the code above. This way, you will be sure that you use the same style and settings from the parent theme.
Now, when you click the View menu at the project admin page, the new single-mms_project_cpt.php will be used. The result is below.

Create our archive projects page
We already created the single page for our project post type. We still need to create one more template for displaying all project items. It calls the archive page.
In your child theme folder, create a new archive-mms_project_cpt.php while mms_project_cpt is our project post type. More details can be found here.
Add the code below to the archive-mms_project_cpt.php.
<?php
/**
* The archive template file
*
* Copy code from the index file from TwentyTwenty theme
* Learn more about hierarchy template:
* https://developer.wordpress.org/files/2014/10/Screenshot-2019-01-23-00.20.04.png
*/
get_header();
?>
<main id="site-content" role="main">
<?php
// # query the custom field value
$loop = new WP_Query(array('post_type' => 'mms_project_cpt', 'posts_per_page' => 10));
while ($loop->have_posts()) : $loop->the_post();
?>
<article <?php post_class(); ?> id="post-<?php the_ID(); ?>">
<!-- # display the title -->
<header class="entry-header has-text-align-center">
<div class="entry-header-inner section-inner medium">
<?php the_title('<h2 class="entry-title heading-size-1"><a href="' . esc_url(get_permalink()) . '">', '</a></h2>'); ?>
</div>
</header>
<div class="post-inner <?php echo is_page_template('templates/template-full-width.php') ? '' : 'thin'; ?> ">
<div class="entry-content" data-rinquest="<?php echo get_the_ID() ?>">
<?php
// # get the current post ID
$post_id = get_the_ID();
// # check empty value
if (!empty($post_id)) {
// # retrieve the custom field value
// https://developer.wordpress.org/reference/functions/get_post_type/
$proj_image = get_post_meta($post_id, '_proj-image', true);
$proj_desc = get_post_meta($post_id, '_proj-desc', true);
$proj_days = get_post_meta($post_id, '_proj-days', true);
$proj_website = get_post_meta($post_id, '_proj-website', true);
$proj_published = get_post_meta($post_id, '_proj-published', true);
// # securing output
// https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/#escaping-securing-output
if (!empty($proj_image))
$proj_image = esc_url($proj_image);
if (!empty($proj_desc))
$proj_desc = esc_textarea($proj_desc);
if (!empty($proj_days))
$proj_days = esc_html($proj_days);
if (!empty($proj_website))
$proj_website = esc_url($proj_website);
if (!empty($proj_published))
$proj_published = esc_html($proj_published);
// # display the custom field value
// project image
echo '<p><b>' . __('Project Image', 'your-textdomain') . ': </b></p>';
echo '<p padding: 10px;">' . '<img src="' . $proj_image . '" alt="" style="max-width:100%; border:1px solid #dddddd;">' . '</p>';
// project description
echo '<p><b>' . __('Project Description', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_desc . '</p>';
// days
echo '<p><b>' . __('Time spent on the project ', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_days . '</p>';
// website
echo '<p><b>' . __('Website', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_website . '</p>';
// published date
echo '<p><b>' . __('Published date', 'your-textdomain') . ': </b></p>';
echo '<p style="background:#ffffff; padding: 10px;">' . $proj_published . '</p>';
echo '<hr>';
echo '<br><br>';
}
?>
</div><!-- .entry-content -->
</div><!-- .post-inner -->
</article> <!-- .mms_project_cpt -->
<?php endwhile; ?>
<!-- Destroys the previous query and sets up a new query. -->
<!-- https://developer.wordpress.org/reference/functions/wp_reset_query/ -->
<?php wp_reset_query(); ?>
</main><!-- #site-content -->
<?php get_template_part('template-parts/footer-menus-widgets'); ?>
<?php
get_footer();
When you visit the projects link (yourdomain.com/projects/), you will see all project items in this archive page as below.

I use the TwentyTwenty theme as my parent theme. So I copied the code from the index.php from the TwentyTwenty theme and modified it, and putit into our archive file.
Below is the theme and WordPress versions I use for creating this tutorial. Basically, the tutorial should work for all versions except for the action hooks or the JS library of WordPress changes.
- TwentyTwenty version 1.5
- WordPress version 5.5.1
Download the Full Source Code of the Child Theme
You can download the source code of the child theme of this tutorial from the link below.
Custom Fields Tutorial – source code child theme (1428 downloads )If you want to install this child theme on your site, I recommend downloading and installing fresh WordPress and making sure you have the TwentyTwenty theme installed. Then install this child theme and activate it on your WordPress site.
Download the Full Source Code Plugin
You can download the source code plugin from the link below. This source code is added to the custom plugin. Once you add this plugin to your demo site, just activate the plugin. It will work as same as the source code of the child theme above.
Please complete the form. Upon successful submission, the file download will begin automatically. Please allow a few seconds for the submission to complete.
Conclusion
Custom fields are a strong feature in WordPress that help you organize information on any page or post.
We looked at two ways to add them: using a plugin or doing it yourself (manual). Using a plugin is the easiest and fastest way. Building custom fields manually takes longer, and you need to know code (like JavaScript) if you want to add complex things like a button to upload pictures.
If you don’t code, you should use a plugin. If you are a developer, feel free to build them yourself. You can take the source code and change it to do exactly what you need.
Your support helps keep this blog running! Secure payments via Paypal and Stripe.

