WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I am trying to create a custom meta box where the user can add fields as needed. I followed this tutorial: http://wp.tutsplus.com/tutorials/reusable-custom-meta-boxes-part-1-intro-and-basic-fields/

I would like to expand on this tutorial and create a repeatable field that allows two inputs. For Example: Name and URL.

How could I achieve this?

I found this other question here: Create more Meta Boxes as needed

I was able to integrate the meta box example into my custom post type but I couldn't figure out how to output the meta values into my theme. Here the code I am working with:

    /* Define Metabox Fields

    $prefix = 'tz_';

    $custom_downloads = array(
         'id' => 'custom-downloads',
         'title' => __('Custom Downloads Downloads (Beta)'),
         'page' => 'categories-supported',
         'context' => 'normal',
         'priority' => 'high',
         'fields' => array(

            'name' => __('Add Product Downloads', 'framework'),
            'desc' => __('This meta box is under development', 'framework'),
            'id' => $prefix . 'custom-downloads',
            'type' => 'text',
            'std' => ''

    /* Add Meta Box to CPT screen 

    function tz_add_box() {
        global $custom_downloads;

        add_meta_box($custom_downloads['id'], $custom_downloads['title'], 'tz_show_box_custom_dwnld', $custom_downloads['page'], $custom_downloads['context'], $custom_downloads['priority']);



 add_action('admin_menu', 'tz_add_box');

    /* Callback function/show fields in meta box
    This is taken directly from: http://wordpress.stackexchange.com/questions/19838/create-more-meta-boxes-as-needed

    function tz_show_box_custom_dwnld() {
        global $custom_downloads, $post;
        // Use nonce for verification
        echo '<input type="hidden" name="tz_meta_box_nonce" value="', wp_create_nonce(basename(__FILE__)), '" />';

        <div id="meta_inner">

        //get the saved meta as an arry
        $songs = get_post_meta($post->ID,'songs',true);

        $c = 0;
        if ( count( $songs ) > 0 ) {
            foreach( $songs as $track ) {
                if ( isset( $track['title'] ) || isset( $track['track'] ) ) {
                    printf( '<p>Song Title <input type="text" name="songs[%1$s][title]" value="%2$s" /> -- Track number : <input type="text" name="songs[%1$s][track]" value="%3$s" /><span class="remove">%4$s</span></p>', $c, $track['title'], $track['track'], __( 'Remove Track' ) );
                    $c = $c +1;

        <span id="here"></span>
        <span class="add"><?php _e('Add Tracks'); ?></span>
        var $ =jQuery.noConflict();
        $(document).ready(function() {
        var count = <?php echo $c; ?>;
        $(".add").click(function() {
        count = count + 1;

        $('#here').append('<p> Song Title <input type="text" name="songs['+count+'][title]" value="" /> -- Track number : <input type="text" name="songs['+count+'][track]" value="" /><span class="remove">Remove Track</span></p>' );
        return false;
        $(".remove").live('click', function() {


/* Save data when post is edited

function tz_save_data($post_id) {
    global $meta_box, $meta_box_video, $meta_box_video_page, $meta_box_product_tabs, $meta_deployments, $meta_features, $meta_downloads;
    // verify nonce
    if (!wp_verify_nonce($_POST['tz_meta_box_nonce'], basename(__FILE__))) {
        return $post_id;
    // check autosave
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return $post_id;
    // check permissions
    if ('page' == $_POST['post_type']) {
        if (!current_user_can('edit_page', $post_id)) {
            return $post_id;
    } elseif (!current_user_can('edit_post', $post_id)) {
        return $post_id;

 $songs = $_POST['songs'];


add_action('save_post', 'tz_save_data');

Using The above code I am able to generate the dynamic meta box on my CPT edit screen and am able to save data in the fields successfully.

I'm a little embarrassed to admit it, but I don't know how to display the info from these fields in my theme. I have been able to successfully display other custom meta information stored in other fields by using

<?php $post_meta_data = get_post_custom($post->ID); ?>
<?php $custom_features = unserialize($post_meta_data['tz_features-repeat'][0]); ?>

<?php echo '<ul class="deployments">';
    foreach ($custom_deployments as $string) {
        echo '<li>'.$string.'</li>';
    echo '</ul>';

Any help would be greatly appreciated!

share|improve this question
Nice tutorial. But I suggest you to be more explicit, what part of the code is not working? Maybe you already have lots of code, but try to isolate the relevant parts and add it to your question. The 4th part of the tutorial deals with the output, what are you using? – brasofilo May 16 '12 at 23:26
@brasofilo I just added the relavent code. Thanks for your comment. – derekshirk May 17 '12 at 1:28
after looking at the database tables in phpMyAdmin it looks like the data is being stored in an array like this: a:2:{s:4:"%1$s";a:2:{s:5:"title";s:4:"%2$s";s:5:"track";s:4:"%3$s";}s:4:"%2$s";a‌​:2:{s:5:"title";s:5:"Title";s:5:"track";s:5:"Album";}} – derekshirk May 17 '12 at 2:49
up vote 0 down vote accepted

The serialized array your meta boxes are saving is deformed. I made a few slight changes to your code to get it to work. I created a new post and here is what was saved after being unserialized.

Note: WordPress will automatically unserialize an an array stored in post_meta. Use an online unserializer if you ever want to check the contents.

[1] => Array
[title] => Back in Black
[track] => 1
) [2] => Array
[title] => Hells Bells
[track] => 2
) [3] => Array
[title] => Hot for Teacher
[track] => 3
) [4] => Array
[title] => Rack City Bitch
[track] => 4

To output this on the front end you just call get_post_meta() and loop through the array.

add_filter( 'the_content', 'get_your_track_meta' );
    function get_your_track_meta( $content ) {
        global $post;
        $metadata = get_post_meta( $post->ID, 'songs', true );
        $content .= '<ul>';
        foreach ( (array) $metadata as $meta) {
            $content .= '<li>' . $meta['title'] . ' -- ' . $meta['track'] . '</li>';
        $content .= '</ul>';
        return $content;

On the front end this gives us a nice unordered list with the track and title:

  • Back in Black -- 1
  • Hells Bells -- 2
  • Hot for Teacher -- 3
  • Rack City Bitch -- 4

Code Changes:

I had to change your page field to post in you meta box definitions to get it to load on a post and I also moved them inside the add_meta_box function so we wouldn't have to deal with making them global. In your show box function I cast the get_post_meta variable to an array to get rid of the invalid argument supplied for foreach error I got in Xdebug and fixed your counter.

$songs = get_post_meta( $post->ID, 'songs', true );
        $c = 0;
        if ( count( $songs ) > 0 ) {
            foreach ( (array)$songs as $track ) {
                if ( isset( $track['title'] ) || isset( $track['track'] ) ) {
                    printf( '<p>Song Title <input type="text" name="songs[%1$s][title]" value="%2$s" /> -- Track number : <input type="text" name="songs[%1$s][track]" value="%3$s" /><span class="remove">%4$s</span></p>', $c, $track['title'], $track['track'], __( 'Remove Track' ) );
share|improve this answer
Thanks so much for the thorough response. I am going to take a closer look at this tonight. However my first attempt this morning did not work. The array does look a lot nicer in the database table but I am still unable to output the unordered list of values in my theme. I am using this on a CPT and I'm wondering if that has something to do with it. Would you mind posting a link to the full code you used? – derekshirk May 18 '12 at 0:25
I used your exact code and only chaged categories-supported to posts and moved the $custom_downloads array into the tz_add_meta_box function along with the changes outlined above. Could you run this code on your single.php or single-post_type.php inside the loop: $meta = get_post_meta( get_the_ID(), 'songs' true); var_dump( $meta ); load the post and see what you get. – Chris_O May 18 '12 at 0:33
Chris, Thank you so much for your help. Your solution did work. However, I did end up modifying how I output the data a bit.<?php $file = get_post_meta( $post->ID, 'file', true ); if ( count( $file ) > 0 ) { foreach ( (array)$file as $url ) { echo '<li><a href="'.$url['url'].'" target="_blank" >Download '.$url['name'].'</a></li>'; } } ?> – derekshirk Jun 4 '12 at 18:28

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.