How to add custom fields to your WordPress
Home » BLOG » WordPress » How to add custom fields to your WordPress

How to add custom fields to your WordPress

category:  WordPress

For my work, I am often requested to do the customization for my clients. One of the most requests are adding the new custom fields to the posts or pages. The benefits for creating the custom fields are for organizing the data and grouping them.

For adding the custom fields to your WordPress, there are two common ways to do. One is using the plugin and another one is using the WordPress API. In this post, I will share two ways.

Add custom fields using the plugin

This method is the easy and fast way for adding the custom fields to your posts, pages or custom post type.

Add custom fields without 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 add_meta_box function from WordPress API for this purpose.

Create a child theme – recommended

For this tutorial, I recommend you to create the child theme before continue. Follow this link for creating 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 into the functions.php of your child theme. The code will create a new project custom post type for your WordPress.

Note that, I recommend you to create the custom post type though your own plugin rather than the functions.php. In this tutorial, I try to keep the content short as possible. Follow this link for creating the custom post type though 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 below.

add new project custom post type
Figure: add new project custom post type

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.

  1. project image (image field type)
  2. project description (textarea field type)
  3. time spent on the project (number field type)
  4. project website (url field type)
  5. project published date (date field type)

Now we know how many fields we are gonna add. Next, we will start to add those fields into 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 display 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 into 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 register earlier.

add new project meta box
Figure: Add new project meta box
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 into 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 into 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 the callback function for displaying the custom fields in the project meta box that we create.

Add code below into 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 add 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 the 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.

add custom fields to meta box
Figure: add custom fields to meta box

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 into 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 add the useful comment to explain each snippet code. Hope it makes it clear.

Step 3: Saving the data into database

In the step 2, I use the update_post_meta function. This function will save the data into the wp_postmeta table which stores all postmeta value in WordPress. If our custom fields never saved in the wp_postmeta table before, this function will create new entry in the wp_postmeta table for us. If our custom fields 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 database using update_post_meta function.

Wp_postmeta table

Once the data is saved to the database, you can check the data from wp_postmeta table. Each entry is saved with four fields which are meta_id, post_id, meta_key and meta_value.

You can use this SQL below for query the data that we just save. 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')
wp_postmeta query result in phpMyadmin
Figure: wp_postmeta query result in phpMyadmin

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

Project admin page
Figure: Project admin page
Custom fields after saving
Figure: Custom fields after saving

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 plugin, usually WordPress permalinks needs 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 have an experience with the 404 error(File Not Found) when you visit the custom post type page(single, archive, etc).

Create our single project page

At your child theme folder, create a new single-mms_project_cpt.php while mms_project_cpt is our project post type. More detail can be found here.

Now add the code below into 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 TwentyTwenty theme as 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 View menu at the project admin page, the new single-mms_project_cpt.php will be used. The result is below.

Single Project page
Figure: a single project page
Create our archive projects page

We already created the single page for our project post type. We still need to create one more templates for displaying all project items. It calls the archive page.

At your child theme folder, create a new archive-mms_project_cpt.php while mms_project_cpt is our project post type. More detail can be found here.

Add the code below into 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.

archive projects page
Figure: Archive projects page

I use TwentyTwenty theme as my parent theme. So I copy the code from the index.php from the TwentyTwenty theme and modify it and put 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 the action hooks or JS library of WordPress changes.

  • TwentyTwenty version 1.5
  • WordPress version 5.5.1

Download a full source code of child theme

You can download the source code of child theme of this tutorial from the link below.

Custom Fields Tutorial - source code child theme (748 downloads )

If you want to install this child theme on your site, I recommend to download and install fresh WordPress and make sure you have the TwentyTwenty theme installed. Then install this child theme and activate it on your WordPress site.

Download a 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 child theme above.

Custom Fields Tutorial - source code plugin (5075 downloads )

Conclusion

Adding the custom fields to any post type are one of the powerful features from WordPress. Using the custom fields plugin is easy and fast while creating the custom fields manually will take more time and you need the JS knowledge in order to modify the media uploader library. For non-coder, I recommend to use the plugins. For developers, feel free to download the source code and modify it to fit your needs.

And that’s it for today.