Shortcodes

Everybody has seen them – those bracketed tags you put in your post or page that later transform in something fancy. They’ve been around since WordPress 2.5 and are pretty much everywhere these days. That’s why I’ll keep the introductory part short and instead I’ll concentrate on something interesting – a hard to produce by hand shortcode and a generator UI for easing the pain.

The Shortcode API

I’ll just summarize what you can find in the codex.

Three flavours of shortcodes

The most basic shortcode is this: [my_shortcode]. It has just a name and nothing else. But let’s change the name to something more meaningful. For example,

. We want the author shortcode to show some information about the current post’s author.But what if the post was co-authored? We have to add some way of selecting which author we want to display. Not a problem, the shortcode API allow us to have parameters:

.Hmm… You don’t think this author’s description fits the current post’s theme? Well, we can add some description in the shorcode’s content:

This is the most freaking awesome author you can find in the universe. What an honour that he has written a post about peanuts

It look really similar to BBCode – you have a name of the shortcode, some attributes, and you may have a content which you’d need to enclose in the shortcode in a way similar to what HTML looks like.

Implementing the author shortcode

For this part I’ll, uhm… “borrow” Greg’s design for an author box ;) I won’t provide a detailed explanation of it, so please check his tutorial if you have any problems with the HTML/CSS part. I’ll just change the way it’s displayed in posts – instead of forcing it before the_content(), it will be accessed via a shortcode – the one I used to show you the shortcode syntax.

We’ll start with a basic function that is pretty much the most common way to start implementing a shortcode.

001 function sc_author($atts, $content = null, $code) {
002         extract(shortcode_atts(array(
003                 'id' => get_the_author_meta('ID')
004         ), $atts));
005
006         $data = <<<HTML
007 HTML;
008
009         return $data;
010 }
011 add_shortcode('author', 'sc_author');

Several things happen here. First, the add_shortcode call – it takes two parameters – the name of the shortcode and the function that does the job. Three arguments will be passed to this function:

1. the first is an associative array of the attributes, starting at $atts[1]. $atts[0] may hold the matched shortcode regex if it’s different from the callback name. You don’t have to worry about the $atts array actually, there’s a function that processes it, so it can become useful for you.
1. the second argument is the content of the shortcode. If the shortcode doesn’t have any content – this argument will be null.
1. the third argument is the shortcode’s name – in our example this will be “author”. We don’t need this, but I wanted to show you that it’s there.

Now, in the function you see a call to shortcode_atts(). This function takes two arguments. The first argument is an associative array of the default attributes for the shortcode. In our case, we want this to be set to the ID of the author of the current post by default. The second argument are the attributes that are actually passed to the shortcode. If an attribute is present in $atts, it’s default will be substituted. It is important to note that you must list all attributes in the defaults array, otherwise they will be ignored.

The last part of the function is a heredoc string for $data. We then return $data – the shortcode will be substituted with what sc_author() returns. Do not echo anything, because the shortcode processing takes place before the content is printed, so an echo inside sc_author() will mess things up.

Now that we have the ID of the desired author, we can add the HTML:

001 function sc_author($atts, $content = null, $code) {
002         extract(shortcode_atts(array(
003                 'id' => get_the_author_meta('ID')
004         ), $atts));
005
006         $avatar = get_avatar( get_userdata($id)->user_email, '140', '' );
007         $description = is_null($content) ? get_userdata($id)->user_description : $content;
008
009         $data = <<<HTML
010 <div id=”author-box">
011     <div id=”author-avatar”>{$avatar}</div>
012     <div id=”author-content”>
013         <h3>About me</h3>
014         <p>{$description}</p>
015     </div>
016 </div>
017 HTML;
018
019         return $data;
020 }
021 add_shortcode('author', 'sc_author');

And that’s basically it. We get the avatar and the default description using the get_userdata() function. If our shortcode has any $content, we use this instead of the default.

Just add this css and you should have your author box working:

001 #author-box {
002     background-color: #f9f9f9;
003     -moz-box-shadow: 3px 3px 0 #cccccc;
004     -webkit-box-shadow: 3px 3px 0 #cccccc;
005     box-shadow: 3px 3px 0 #cccccc;
006     border: 1px solid #e0e0e0;
007     width: 600px;
008     height: 160px;
009 }
010 #author-avatar {
011     padding: 10px;
012     float: left;
013 }
014 #author-content {
015     padding: 10px 10px 10px 0;
016 }
017 #author-content h3 {
018     font-size: 22px;
019     color: #444444;
020     padding-bottom: 10px;
021 }
022
023 #author-content p {
024     font-size: 13px;
025     color: #606060;
026     line-height: 18px;
027 }

Something more serious – a pricing list

Now that we have started “borrowing” ;) ideas, we’ll try to create something like this pricing list: http://basecamphq.com/signup

Here are our requirements:

  • three to five plans
  • each plan has these options:
    • title
    • price
    • description
    • button link
    • button text
    • featured (boolean, only one plan should be “featured”)

Design

Since this is a shortcodes tutorial, I’ll skip the design part and concentrate on the back end. However, we need some minimal design, so I’ve created something that should work in modern browsers:

HTML:

001 <div class="pricing col-3 clearfix">
002     <div class="price">
003         <h2>small</h2>
004         <h3>2.99/month</h3>
005         <div class="description">some text here</div>
006         <a href="button link" title="button text" class="button">button text</a>
007     </div>
008     <div class="price featured">
009         <h2>medium</h2>
010         <h3>5.99/month</h3>
011         <div class="description">some text here</div>
012         <a href="button link" title="button text" class="button">button text</a>
013     </div>
014     <div class="price">
015         <h2>large</h2>
016         <h3>9.99/month</h3>
017         <div class="description">some text here</div>
018         <a href="button link" title="button text" class="button">button text</a>
019     </div>
020 </div>

CSS:

001 .pricing { margin-top: 30px }
002
003 .pricing.col-3 .price { width: 32%; }
004 .pricing.col-3 .price.featured { width: 36%; }
005 .pricing.col-4 .price { width: 24%; }
006 .pricing.col-4 .price.featured { width: 28%; }
007 .pricing.col-5 .price { width: 19%; }
008 .pricing.col-5 .price.featured { width: 24%; }
009
010 .pricing .price {
011     position: relative;
012     float: left;
013     text-align: center;
014     color: #444;
015     height: 250px;
016     border: 1px solid #444;
017     -moz-box-sizing: border-box;
018     -webkit-box-sizing: border-box;
019     box-sizing: border-box;
020     background: #f0f0f0;
021     z-index: 5;
022     border-right: 0;
023     padding: 10px 0;
024     font-family: sans-serif;
025 }
026
027 .pricing .price.featured {
028     height: 300px;
029     margin-top: -25px;
030     background: #fff;
031     z-index: 10;
032     border-right: 1px solid #444;
033     margin-right: -1px;
034 }
035
036 .pricing .price:last-child {
037     border-right: 1px solid #444;
038 }
039
040 .pricing h2 {
041     font-size: 34px;
042     margin-bottom: 10px;
043 }
044
045 .pricing h3 {
046     font-size: 26px;
047     margin-bottom: 10px;
048 }
049
050 .pricing .description {
051     border-top: 1px solid #ccc;
052     padding: 10px;
053 }
054
055 .pricing .button {
056     display: inline-block;
057     position: absolute;
058     bottom: 10px;
059     left: 10%;
060     right: 10%;
061     text-decoration: none;
062     color: #fff;
063     background: #77b753;
064     border-radius: 5px;
065     padding: 5px;
066     border: 1px solid #6ea94c;
067     -moz-box-shadow: 1px 0 2px #6ea94c;
068     -webkit-box-shadow: 1px 0 2px ##6ea94c;
069     box-shadow: 1px 0 2px #6ea94c;
070 }
071
072 .pricing .featured .button {
073     bottom: 25px;
074     padding: 10px;
075     font-weight: bold;
076 }
077
078 .pricing .button:hover {
079     background: #83c95b;
080 }

 

Click on the image to see it on jsfiddle

What should the shortcode look line the the editor?

Here’s what I will use.

001

professional

002     [price title="small" featured="false" amount="2.99/month" button_link="#" button_text="button text"]some text here[/price]
003     [price title="medium" featured="true" amount="5.99/month" button_link="#" button_text="button text"]some text here[/price]
004     [price title="large" featured="false" amount="9.99/month" button_link="#" button_text="button text"]some text here[/price]
005

$1995

Join Now

Nothe that I haven’t specified how many [price][/price] blocks are there. I’m going to parse them with a regex anyway, so I don’t need to expicitly state how many of them are in the shortcode. This way it will be easier to edit the shortcode.

The shortcode function

We start with something simple:

001 function sc_pricing($atts, $content = null, $code) {
002     if(is_null($content)) return;
003
004 }
005 add_shortcode('pricing', 'sc_pricing');

Note that the actual shortcode is pricing, not the price blocks that are in

professional

    • ‘s content. We have to parse the price blocks manually. The following regex should match a price block:
001 preg_match_all('/[priceb([^]]*)]((?:(?![/price]).)+)[/price]/s', $content, $matches);
002
003 $sub_atts = $mathes[1];
004 $sub_contents = $matches[2];
005
006 $price_count = sizeof($sub_atts);

We then save the matched attributes in $sub_atts and the matched contents in $sub_contents. The length of the attributes array is the number of [price] blocks.

Here’s what we’ll start at:

001 function sc_pricing($atts, $content = null, $code) {
002     if(is_null($content)) return;
003
004     preg_match_all('/[priceb([^]]*)]((?:(?![/price]).)+)[/price]/s', $content, $matches);
005
006     $sub_atts = $mathes[1];
007     $sub_contents = $matches[2];
008
009     $price_count = sizeof($sub_atts);
010
011     ob_start();
012 ?>
013
014 <?php
015     return ob_get_clean();
016 }
017 add_shortcode('pricing', 'sc_pricing');

I use output buffering, because it looks ugly if I wrap everything in strings. Now we just have to put our content inside it.

Let’s begin by adding our static example, just to see if it works:

001 function sc_pricing($atts, $content = null, $code) {
002     if(is_null($content)) return;
003
004     preg_match_all('/[priceb([^]]*)]((?:(?![/price]).)+)[/price]/s', $content, $matches);
005
006     $sub_atts = $matches[1];
007     $sub_contents = $matches[2];
008
009     $price_count = sizeof($sub_atts);
010
011     ob_start();
012 ?>
013     <div class="pricing col-3 clearfix">
014         <div class="price">
015             <h2>small</h2>
016             <h3>2.99/month</h3>
017             <div class="description">some text here</div>
018             <a href="button link" title="button text" class="button">button text</a>
019         </div>
020         <div class="price featured">
021             <h2>medium</h2>
022             <h3>5.99/month</h3>
023             <div class="description">some text here</div>
024             <a href="button link" title="button text" class="button">button text</a>
025         </div>
026         <div class="price">
027             <h2>large</h2>
028             <h3>9.99/month</h3>
029             <div class="description">some text here</div>
030             <a href="button link" title="button text" class="button">button text</a>
031         </div>
032     </div>
033 <?php
034     return ob_get_clean();
035 }
036 add_shortcode('pricing', 'sc_pricing');

If you get something like the screenshot above – you’re good to go.

Now, we don’t need three .price divs, we need a loop:

001 <div class="pricing col-3 clearfix">
002     <?php for($i=0; $i<$price_count; $i++): ?>
003         <div class="price">
004             <h2>small</h2>
005             <h3>2.99/month</h3>
006             <div class="description">some text here</div>
007             <a href="button link" title="button text" class="button">button text</a>
008         </div>
009     <?php endfor ?>
010 </div>

That’s kind of a progress, but we show the same content three times now. We have to populate it with the right stuff:

001 <div class="pricing col-3 clearfix">
002     <?php for($i=0; $i<$price_count; $i++): ?>
003         <?php $sub_atts[$i] = shortcode_parse_atts($sub_atts[$i]); ?>
004         <div class="price">
005             <h2><?php echo $sub_atts[$i]['title'] ?></h2>
006             <h3><?php echo $sub_atts[$i]['amount'] ?></h3>
007             <div class="description"><?php echo $sub_contents[$i]?></div>
008             <a href="<?php echo $sub_atts[$i]['button_link']?>" title="<?php echo $sub_atts[$i]['button_text']?>" class="button"><?php echo$sub_atts[$i]['button_text']?></a>
009         </div>
010     <?php endfor ?>
011 </div>

Notice the shortcode_parse_atts() function – you pass a string of shortcode-like attributes to it and it returns a nice associative array.

Just two more things remaining – adding the correct column count and adding a featured class to the “featured” price. Here’s the final version of our shortcode functions with these two implemented:

001 function sc_pricing($atts, $content = null, $code) {
002     if(is_null($content)) return;
003
004     preg_match_all('/[priceb([^]]*)]((?:(?![/price]).)+)[/price]/s', $content, $matches);
005
006     $sub_atts = $matches[1];
007     $sub_contents = $matches[2];
008
009     $price_count = sizeof($sub_atts);
010
011     ob_start();
012 ?>
013     <div class="pricing col-<?php echo $price_count?> clearfix">
014         <?php for($i=0; $i<$price_count; $i++): ?>
015             <?php $sub_atts[$i] = shortcode_parse_atts($sub_atts[$i]); ?>
016             <div class="price <?php if($sub_atts[$i]['featured'] == 'true') echo 'featured'?>">
017                 <h2><?php echo $sub_atts[$i]['title'] ?></h2>
018                 <h3><?php echo $sub_atts[$i]['amount'] ?></h3>
019                 <div class="description"><?php echo $sub_contents[$i]?></div>
020                 <a href="<?php echo $sub_atts[$i]['button_link']?>" title="<?php echo $sub_atts[$i]['button_text']?>" class="button"><?php echo$sub_atts[$i]['button_text']?></a>
021             </div>
022         <?php endfor ?>
023     </div>
024 <?php
025     return ob_get_clean();
026 }
027 add_shortcode('pricing', 'sc_pricing');

# Shortcode generator

I have to admit that long shortcodes like the one I’ve shown you are difficult or at least slow to type by hand. And while I can’t read your mind and write it for you, we can ease the pain by implementing a generator. But because we’re all lazy, I’ll use the SmartMetaBox class that I’ve shown you a couple of weeks ago to generate the shortcode generator ;)

As a prerequisit, you have to get familiar with the SmartMetaBox tutorial link, because I won’t get into detail about the generator’s API in this tutorial.

We’ll start with a class ShortcodeGenerator which extends SmartMetaBox. We don’t need a save function, so we will omit it. Furthermore, we have to change the render function so that it prefixes the field’s ids and names with the id of the meta box – this way we will avoid collisions between different generator instances. We will also add a “Generate” button which will send the shortcode to the editor. And last, we will create a simple helper function “add_shortcode_generator” which will be similar to “add_smart_meta_box”, but it will work with ShortcodeGenerator.

Here’s what our new class looks like:

001 /**
002  * A really basic shortcode generator which uses parts of SmartMetaBox
003  *
004  * @author: Nikolay Yordanov <me@nyordanov.com> http://nyordanov.com
005  * @version: 1.0
006  *
007  */
008
009 class ShortcodeGenerator extends SmartMetaBox {
010     // create meta box based on given data
011
012     public function __construct($id, $opts) {
013         if (!is_admin()) return;
014         $this->meta_box = $opts;
015         $this->id = $id;
016         add_action('add_meta_boxes', array(&$this,
017             'add'
018         ));
019     }
020
021     // Callback function to show fields in meta box
022
023     public function show($post) {
024         echo '<table class="form-table">';
025         foreach ($this->meta_box['fields'] as $field) {
026             extract($field);
027             $id = $this->id .'-'. $id;
028
029             $value = isset($field['default']) ? $default : '';
030
031             echo '<tr>', '<th style="width:20%"><label for="'.$id.'">'.$name.'</label></th>', '<td>';
032
033             include dirname(__FILE__)."/../smart_meta_box/smart_meta_fields/$type.php"; // in my example this is where the field templates are
034
035             if (isset($desc)) {
036                 echo '&nbsp;<span class="description">' . $desc . '</span>';
037             }
038             echo '</td></tr>';
039         }
040         echo '</table>';
041
042         echo '<a href="#" class="generate-shortcode button">'.__('Generate').'</a>';
043     }
044 };
045
046 function add_shortcode_generator($id, $opts) {
047     new ShortcodeGenerator($id, $opts);
048 }

And here’s how to use it with the pricing shortcode:

001 include 'smart_meta_box/SmartMetaBox.php';
002 include 'shortcode_generator/ShortcodeGenerator.php';
003
004 function register_sh_gen() {
005
006     $fields = array(
007         array(
008             'name' => 'Pricing options',
009             'id' => 'pricing-options',
010             'type' => 'select',
011             'default' => 3,
012             'options' => array(
013                 3=>3,4,5
014             ),
015         )
016     );
017
018     for($i=1; $i<=5; $i++) {
019         $prefix = "Price $i - ";
020
021         $fields[] = array(
022             'name' => $prefix.'Title',
023             'id' => 'title-'.$i,
024             'type' => 'text',
025         );
026
027         $fields[] = array(
028             'name' => $prefix.'Amount',
029             'id' => 'amount-'.$i,
030             'type' => 'text',
031         );
032
033         $fields[] = array(
034             'name' => $prefix.'Featured?',
035             'id' => 'featured-'.$i,
036             'type' => 'checkbox',
037             'default' => false
038         );
039
040         $fields[] = array(
041             'name' => $prefix.'Button link',
042             'id' => 'button_link-'.$i,
043             'type' => 'text',
044         );
045
046         $fields[] = array(
047             'name' => $prefix.'Button text',
048             'id' => 'button_text-'.$i,
049             'type' => 'text',
050         );
051
052         $fields[] = array(
053             'name' => $prefix.'Description',
054             'id' => 'decription-'.$i,
055             'type' => 'textarea',
056         );
057     }
058
059     add_shortcode_generator('pricing-shortcode', array(
060         'title' => 'Pricing shortcode',
061         'pages' => array('post', 'page'),
062         'context' => 'normal',
063         'priority' => 'high',
064         'fields' => $fields
065     ));
066
067 }
068 add_action('admin_init', 'register_sh_gen');

Now, all this does is render some not so pretty meta box:

meta-450x1024

Now, we have several things to do here. First, the “pricing options” select should hide or show the correct number of options (notice how in the shortcode it says 3, but all five groups are shown – we have to change that). Second. I’d like to have some css for the textareas and maybe for the “Generate” button. This tutorial is more about the general idea and not about how thins look, but it about ten lines, so I don’t see a problem with adding it. And third, the “Generate” button should, well, generate the shortcode.

We’ll start by adding these two lines inside add_shortcode_generator(). This way we won’t load them if there’s no generator on the page. Note that add_smart_meta_box() and add_shortcode_generator() are not supposed to be called before the admin_init action has fired and with these additions, they should not be called after admin_print_styles, too.

001 wp_enqueue_style('shortcode-generator', get_bloginfo('template_directory').'/shortcode_generator/generator.css');
002 wp_enqueue_script('shortcode-generator', get_bloginfo('template_directory').'/shortcode_generator/generator.js', array('jquery'), false, true);

And here are the contents of generator.css:

001 .generate-shortcode {
002     margin: 20px 0 10px 0;
003     display: block;
004     text-align: center;
005 }
006
007 #pricing-shortcode .form-table textarea {
008     width: 100%;
009 }
010
011 #pricing-shortcode .form-table tr:nth-child(6n+1) {
012     border-bottom: 1px solid #ddd;
013 }
014
015 #pricing-shortcode .form-table tr:nth-child(n+20) {
016     display: none;
017 }

The most important part of this code are the last two selectors. The first one adds a border after each pricing group. The second hides the settings for the fourth and fifth pricing groups. We will show these with javascript.

Let’s start with making the “Pricing options” dropdown work as intended. The basic idea is really simple. On change of the select, we show every option and then we hide the redundant ones. Here’s how to do it:

001 (function($, undefined) {
002
003 $(function() {
004     $('#pricing-shortcode-pricing-options').change(function() {
005         var hide = $(this).val()*6+2;
006
007         $('#pricing-shortcode .form-table tr').show();
008         $('#pricing-shortcode .form-table tr:nth-child(n+'+hide+')').hide();
009     });
010 });
011
012 })(jQuery);

And now the last part of the javascript that we need is to add the code which generated the shortcode. We start with a simple event handler:

001 $('#pricing-shortcode .generate-shortcode').click(function(e) {
002     var count = $('#pricing-shortcode-pricing-options').val();
003     var result = '

professional

$1995

Join Now

';

004
005     // we'll put a loop here iterating over the pricing options
006
007     result += '

$1995

Join Now ';

008     send_to_editor("n"+result+"n");
009     e.preventDefault();
010 });

We will also create a simple helper function so that we won’t need to write the full ids of the elements:

001 var get_price_option = function(name, num) {
002     var el = $('#pricing-shortcode-'+name+'-'+num);
003
004     return el.is(':checkbox')? el.is(':checked') : el.val();
005 }

And now, the loop. I have commented the important parts, but it’s pretty straightforward – get some values and stringify them ;)

001 var opts = ['title', 'amount', 'featured', 'button_link', 'button_text'];
002
003 for(i=1; i<=count; i++) {
004     var sub_result = ' [price';
005
006     for(j in opts) {
007         sub_result += ' '+opts[j]+'="'+get_price_option(opts[j], i)+'"'; // we add a key="val" attribute for each setting
008     }
009
010     sub_result += '] ' + get_price_option('description', i) + ' [/price]'; // and as content we add the descriptions
011
012     result += sub_result;
013 }

And here is the completed generator.js:

001 (function($, undefined) {
002
003 $(function() {
004     $('#pricing-shortcode-pricing-options').change(function() {
005         var hide = $(this).val()*6+2;
006
007         $('#pricing-shortcode .form-table tr').show();
008         $('#pricing-shortcode .form-table tr:nth-child(n+'+hide+')').hide();
009     });
010
011     var get_price_option = function(name, num) {
012         var el = $('#pricing-shortcode-'+name+'-'+num);
013
014         return el.is(':checkbox')? el.is(':checked') : el.val();
015     }
016
017     $('#pricing-shortcode .generate-shortcode').click(function(e) {
018         var count = $('#pricing-shortcode-pricing-options').val();
019         var result = '

professional

';
020         var opts = ['title', 'amount', 'featured', 'button_link', 'button_text'];
021
022         for(i=1; i<=count; i++) {
023             var sub_result = ' [price';
024
025             for(j in opts) {
026                 sub_result += ' '+opts[j]+'="'+get_price_option(opts[j], i)+'"';
027             }
028
029             sub_result += '] ' + get_price_option('description', i) + ' [/price]';
030
031             result += sub_result;
032         }
033
034         result += '

$1995

Join Now ';

035         send_to_editor("n"+result+"n");
036         e.preventDefault();
037     });
038 });
039
040 })(jQuery);

Well, we’re pretty much ready. This code is not so useful if you have multiple shortcodes that need to have a generator, but what I wanted to do is to explain some basic concepts. Feel free to modify the code for your needs and ask questions, if any. As with SmartMetaBox, I have uploaded the code to github.