Creating meta boxes is a crucial part of WordPress theme/plugin development. It's a way to add an appealing editor to the post screen and avoids forcing users to rely on custom fields. If you've ever created a custom post type in WordPress, you've probably wanted to add some sort of additional data to it. Sure, you could use custom fields, but that's ugly. Getting started with custom meta boxes is easy, so let's dive in!
What Are Custom Meta Boxes?
A custom meta (or write) box is incredibly simple in theory. It allows you to add a custom piece of data to a post or page in WordPress.
Imagine that you're working on a theme for a client that wants to catalog his extensive collection of concert posters. You immediately start looking to the core WordPress functionality to see how you might organize the theme: Every post will represent a poster, which is perfect for adding an image, title and description. We can also use the categories and tags system inside of WordPress to organize the posters. But what if we wanted to add a new type of meta data for the "artist" of each poster? Hmph. WordPress doesn't quite have anything for that right out of the box... which brings us to custom meta boxes.
A custom meta (or write) box is incredibly simple in theory. It allows you to add a custom piece of data to a post or page in WordPress - what's better is that it can fit directly into most of the default pages inside WP, so you can easily place it inside the Post-Editor for easy use by non-technical types. As I said in the intro, you can add this same kind of "meta data" to your post using the built in custom fields for a post or page. There's nothing wrong with this persay, but it's not a very graceful or user-friendly.
Instead, you want to create a custom meta box that contains fields for all of your data and saves all of that stuff right when the post is published. That's what we're covering here. This is broken down into three big steps:
- Adding the meta box
- Rendering the meta box
- Saving the data (the right way - yes, there is a wrong way)
It's worth noting that a lot of this information can also be used inside the custom post types API (we'll get to that point later!), but for the sake of keeping things focused today, we're going to add this directly to the default post editor.
For the advanced readers in the audience: Yes, custom post types is where we'll be going with this eventually, but it's important to setup some fundamentals first. Plus, as you can use the custom meta boxes in all sorts of places, it's good for anyone to know.
Anything in this tutorial will work in a theme's
functions.php
file. That is not the correct place for it, however. If you're adding data to a post, chances are you want it there regardless of your front end design. As such, you should place this code some place that isn't dependent on your design: a plugin file.Step 1 Adding the Meta Box
Conveniently, WordPress provides a function for adding meta boxes to a given admin screen: add_meta_box.
The codex entry is well done for this function, but here's a brief overview. Its prototype:
1 | <?php add_meta_box( $id , $title , $callback , $page , $context , $priority , $callback_args ); ?> |
$id
is the html ID attribute of the box. This is useful if you're loading custom CSS or Javascript on the editing page to handle the options. Otherwise, it doesn't really matter all that much.$title
is displayed at the top of the meta box.$callback
is the function that actually renders the meta box. We'll outline this in step 2.$page
is where you want the meta box to be displayed. This should be a string with 'post' or 'page' or 'some_custom_post_type'.$context
is where you want the meta box displayed. 'normal' puts it below the post editor. 'side' moves the meta box to editing screen's right sidebar (by the categories and tags, etc). 'advanced' also put the box in the same column as the post editor, but further down.$priority
tells wordpress where to place the meta box in the context. 'high', 'default' or 'low' puts the box closer to the top, in its normal position, or towards the bottom respectively. Since all meta boxes are drag-able, $priority
is not a huge deal.Finally
$callback_args
lets you pass data to your $callback
function in the form of an array. We're not going to use this here, but it could be useful for passing some data to the meta box. Say, if your plugin had several options that influenced what was displayed in the meta box. You could pass those options values through the $callback_args
array.So, our
add_meta_box
call will look like this:1 | <?php add_meta_box( 'my-meta-box-id' , 'My First Meta Box' , 'cd_meta_box_cb' , 'post' , 'normal' , 'high' ); ?> |
We can't just pop this into our plugin file alone. Doing so will result in the white screen of death, and PHP fatal error: call to undefined function. Why? Because we called
add_meta_box
function before WordPress was loaded. So we need to use a WordPress hook, which is part of the plugin api. Basically, functions get hooked into a given WordPress action or filter hook, then those functions are fired when that hook loads. By wrapping our add_meta_box call in a function, then hooking that function into the add_meta_boxes
action hook, we avoid the fatal error.Our code to add the meta box to our post screen would look like this:
1 2 3 4 5 6 7 | <?php add_action( 'add_meta_boxes' , 'cd_meta_box_add' ); function cd_meta_box_add() { add_meta_box( 'my-meta-box-id' , 'My First Meta Box' , 'cd_meta_box_cb' , 'post' , 'normal' , 'high' ); } ?> |
Step 2 Rendering the Meta Box
The above code is enough to add the meta box, but now we have to render the thing and actually add fields. This is just an HTML form code mixed in with a bit of PHP to display the saved data. We don't need to include the form tags as WordPress does that for us.
Remember the string we passed as the
$callback
in add_meta_box
? We're now going to create a function with the same name. This function will take care of all the display inside the meta box.1 2 3 4 5 6 | <?php function cd_meta_box_cb() { echo 'What you put here, show\'s up in the meta box' ; } ?> |
We're going to add several fields to our meta box: a text input, a drop down menu, and a check-box. Let's start with the text input.
Adding the Text Input
1 2 3 4 5 6 7 8 9 | <?php function cd_meta_box_cb() { ?> <label for = "my_meta_box_text" >Text Label</label> <input type= "text" name= "my_meta_box_text" id= "my_meta_box_text" /> <?php } ?> |
But what about actually displaying the data? Well, as you'll see in step 3, we'll store this data in the wp_postmeta table using the update_post_meta function. That function has two sister functions called get_post_meta and get_post_custom, which grab data from wp_postmeta.
get_post_meta
only grabs data from one key, while get_post_custom
grabs all of it. Because we're only really using one field at this point, let's use get_post_meta
.Also note that the
add_meta_box
function passes one variable to our callback: $post, which is a post object.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | <?php function cd_meta_box_cb( $post ) { $values = get_post_custom( $post ->ID ); $text = isset( $values [ 'my_meta_box_text' ] ) ? esc_attr( $values [ 'my_meta_box_text' ][0] ) : ”; $selected = isset( $values [ 'my_meta_box_select' ] ) ? esc_attr( $values [ 'my_meta_box_select' ][0] ) : ”; $check = isset( $values [ 'my_meta_box_check' ] ) ? esc_attr( $values [ 'my_meta_box_check' ][0] ) : ”; ?> <p> <label for = "my_meta_box_text" >Text Label</label> <input type= "text" name= "my_meta_box_text" id= "my_meta_box_text" value= "<?php echo $text; ?>" /> </p> <?php } ?> |
Adding The Drop Down
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php function cd_meta_box_cb( $post ) { $values = get_post_custom( $post ->ID ); $text = isset( $values [ 'my_meta_box_text' ] ) ? esc_attr( $values [ 'my_meta_box_text' ][0] ) : ”; $selected = isset( $values [ 'my_meta_box_select' ] ) ? esc_attr( $values [ 'my_meta_box_select' ][0] ) : ”; $check = isset( $values [ 'my_meta_box_check' ] ) ? esc_attr( $values [ 'my_meta_box_check' ][0] ) : ”; ?> <p> <label for = "my_meta_box_text" >Text Label</label> <input type= "text" name= "my_meta_box_text" id= "my_meta_box_text" value= "<?php echo $text; ?>" /> </p> <p> <label for = "my_meta_box_select" >Color</label> <select name= "my_meta_box_select" id= "my_meta_box_select" > <option value= "red" <?php selected( $selected , 'red' ); ?>>Red</option> <option value= "blue" <?php selected( $selected , 'blue' ); ?>>Blue</option> </select> </p> <?php } ?> |
With the addition of a second field, we changed or
get_post_meta
call to get_post_custom
, which returns an associative array of all the post's custom keys and values. We then just access our fields via their names. The ternary statements keep our code from throwing PHP warnings (undefined indices and such). We'll cover the esc_attr function in step three.In the drop down, we're going to use one of WordPress's most handy functions: selected. This compares the first value, the data we saved, with the second, the
<option>
's value attribute. If they're the same, the function will echo selected="selected"
, which makes that value display on the drop down. Pretty sweet, and it saves us from writing a bunch of if or ternary statements. You can also use the selected()
function with radio buttons.Adding the Check-box
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php function cd_meta_box_cb() { // $post is already set, and contains an object: the WordPress post global $post ; $values = get_post_custom( $post ->ID ); $text = isset( $values [ 'my_meta_box_text' ] ) ? $values [ 'my_meta_box_text' ] : '' ; $selected = isset( $values [ 'my_meta_box_select' ] ) ? esc_attr( $values [ 'my_meta_box_select' ] ) : '' ; $check = isset( $values [ 'my_meta_box_check' ] ) ? esc_attr( $values [ 'my_meta_box_check' ] ) : '' ; // We'll use this nonce field later on when saving. wp_nonce_field( 'my_meta_box_nonce' , 'meta_box_nonce' ); ?> <p> <label for = "my_meta_box_text" >Text Label</label> <input type= "text" name= "my_meta_box_text" id= "my_meta_box_text" value= "<?php echo $text; ?>" /> </p> <p> <label for = "my_meta_box_select" >Color</label> <select name= "my_meta_box_select" id= "my_meta_box_select" > <option value= "red" <?php selected( $selected , 'red' ); ?>>Red</option> <option value= "blue" <?php selected( $selected , 'blue' ); ?>>Blue</option> </select> </p> <p> <input type= "checkbox" id= "my_meta_box_check" name= "my_meta_box_check" <?php checked( $check , 'on' ); ?> /> <label for = "my_meta_box_check" >Do not check this</label> </p> <?php } ?> |
Again WordPress provides the handy function checked(). It works just like
selected()
comparing the first value (our saved data) to the second and echoing out checked="checked"
if they're the same.wp_nonce_field
adds two hidden fields to our meta box. One of them is a nonce. These are random strings of numbers that are valid on per user per blog basis for 24 hours. Nonces are a way of verifying intention, and they make sure that WordPress doesn't do anything unless the request came from a very specific place. In other words, we don't want to accidentally update our data by somehow running our save function (see step 3) in another location other than the save_post hook, so we check to make sure the nonce is valid before doing anything.Step 3 Saving the Data
The number one rule when putting anything into your database or on your site isdon't trust the user. Even if that user is you.
To save our data, we're going to rely on another WordPress hook:
save_post
. This works just like our action hook above:1 | <?php add_action( 'save_post' , 'cd_meta_box_save' ); ?> |
The
cd_meta_box_save
function will receive one argument, the post id, and take care of cleaning and saving all of our data. The save_post
hook fires after the update or save draft button has been hit. So we have access to all the $_POST
data, which includes our meta box fields, within our saving function. Before we can do anything, however, we have to do three things: check if the post is auto saving, verify the nonce value we created earlier, and check to make sure the current user can actually edit the post.01 02 03 04 05 06 07 08 09 10 11 12 13 14 | <?php add_action( 'save_post' , 'cd_meta_box_save' ); function cd_meta_box_save( $post_id ) { // Bail if we're doing an auto save if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return ; // if our nonce isn't there, or we can't verify it, bail if ( !isset( $_POST [ 'meta_box_nonce' ] ) || !wp_verify_nonce( $_POST [ 'meta_box_nonce' ], 'my_meta_box_nonce' ) ) return ; // if our current user can't edit this post, bail if ( !current_user_can( 'edit_post' ) ) return ; } ?> |
Now the fun stuff: actually saving our data. The number one rule when putting anything into your database or on your site is don't trust the user. Even if that user is you. To that end, before we save any data, we want to make sure there's nothing malicious in there. Fortunately WordPress provides a bunch of functions for data validation.
You already saw
esc_attr()
above (step 2). This one encodes ' " and < > into HTML entities. Why use this? So users couldn't type a <script> into your meta box. If you'd like to allow certain HTML tags in, but strip others, wp_kses
can do that. It takes two arguments, the first of which is the string you'd like to check and the second is an associative array of allowed tags. WordPress provides many more data validation tools, just don't be afraid to use them.We're going to use the
update_post_meta
function to date care of saving our data. It takes three arguments: a post ID, the meta key, and the value.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <?php add_action( 'save_post' , 'cd_meta_box_save' ); function cd_meta_box_save( $post_id ) { // Bail if we're doing an auto save if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return ; // if our nonce isn't there, or we can't verify it, bail if ( !isset( $_POST [ 'meta_box_nonce' ] ) || !wp_verify_nonce( $_POST [ 'meta_box_nonce' ], 'my_meta_box_nonce' ) ) return ; // if our current user can't edit this post, bail if ( !current_user_can( 'edit_post' ) ) return ; // now we can actually save the data $allowed = array ( 'a' => array ( // on allow a tags 'href' => array () // and those anchors can only have href attribute ) ); // Make sure your data is set before trying to save it if ( isset( $_POST [ 'my_meta_box_text' ] ) ) update_post_meta( $post_id , 'my_meta_box_text' , wp_kses( $_POST [ 'my_meta_box_text' ], $allowed ) ); if ( isset( $_POST [ 'my_meta_box_select' ] ) ) update_post_meta( $post_id , 'my_meta_box_select' , esc_attr( $_POST [ 'my_meta_box_select' ] ) ); // This is purely my personal preference for saving check-boxes $chk = isset( $_POST [ 'my_meta_box_check' ] ) && $_POST [ 'my_meta_box_select' ] ? 'on' : 'off' ; update_post_meta( $post_id , 'my_meta_box_check' , $chk ); } ?> |
Wrap Up
That's it! You should have a fully working meta box. Other examples you may find around the web loop through a bunch of fields without really cleaning the data. This is the wrong approach. Always use the built in data validation functions; different fields/values may require different data validation.
To use these custom fields on the front end of your site, use the
get_post_meta
or get_post_custom
functions (see step 2).