How to Create Custom WordPress Write/Meta Boxes


Start a hosting plan from $3.92/mo and get a free year on Tuts+ (normally $180)

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

The Meta Box Title

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:

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

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:

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.

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

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.

Adding The Drop Down

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

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 is don'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:

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.

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.

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).