Advertisement
  1. Code
  2. WordPress
  3. Plugin Development

Build a Short URL Service with WordPress Custom Post Types

Scroll to top
Read Time: 19 min

WordPress is normally used as a blog engine or as a content management system (CMS), but those are not the only things it can be used for. With a little imagination, you can do virtually anything you want with it! In this tutorial I will teach you how to build a shortened URL service, write some plugin code using object oriented techniques, and deal with WordPress routers and custom error messages.

Note that this is an advanced tutorial - we'll be using some relatively advanced PHP techniques, but I'll make sure to link you over to any resources articles that you might need to understand along the way. Just remember, the point of this tutorial is to push the limits of WordPress, and that's going to take some real thought!


Overview

You should be familiar with the idea of what a "short URL" service is nowadays. If not, check out Goo.gl, bit.ly, or any of the others out there. Those are all fine and well, but what if you want your own? There a few reasons that you might want this (not just for vanity purposes), and this is an excellent chance to look at some of the more advanced areas of WordPress that you might not be familiar with.

The Goal: We will create a Custom Post Type called "url", its post title will be used as the original url. For each post (actually each url now), we generate a key for it, or the user enters his/her own key, which we will call the "vanity key" from now on. This vanity key will be appended to site's url and we have a short url.

Here's the breakdown of what we'll be doing in layman's terms:

  1. Assume your site has domain: http://wp.tutsplus.net.
  2. We have a very long url: https://code.tutsplus.com/tutorials/create-a-multi-layout-portfolio-with-wordpress--net-20304.
  3. We will assign it a "vanity key" of "1A", so short url will be: http://wp.tutsplus.net/1A.
  4. Anyone who hits it gets redirected to: https://code.tutsplus.com/tutorials/create-a-multi-layout-portfolio-with-wordpress--net-20304.

If you are not very familiar with plugin development yet, you should take a look at these useful tutorials before going further:

Also make sure you have mod_rewrite installed, and enable the Pretty Permalink. We will use a custom field called _vanity_key to store this vanity key for each post.

Enough chatting, let's get started!


Step 1 Setting up the plugin class

We will use OOP here.
Let's call this plugin 'wp-vanity'. We create a folder wp-vanity in wp-content/plugins. Then we create a main file call vanity.php and put it in folder wp-vanity.

File vanity.php

1
2
class Vanity {
3
4
    static private $_self = null;
5
    const POST_TYPE = 'url';
6
7
    /**

8
     * Always return the same instance of plugin so we can access its property and its method

9
     * from anywhere

10
     * @return Vanity

11
     */
12
    static public function singleton() {
13
        if (!self::$_self) {
14
            $classname = __CLASS__;
15
            self::$_self = new $classname;
16
            self::$_self->_bootstrap();
17
        }
18
        return self::$_self;
19
    }
20
21
    /**

22
     * Construct plugin and set useful property for later reference

23
     */
24
    public function __construct() {
25
    }
26
27
    /**

28
     * Init cache class!

29
     * Load all styles, script, define JavaScript var with useful URL

30
     */
31
    private function _bootstrap() {
32
        //Add action, filter should be put here

33
    }
34
35
}
36
37
$wp_vanity = Vanity::singleton();

We organize code into a class called "Vanity". We also define a class constant POST_TYPE for the name of our post type. Then we use the singleton() pattern, so that in the future you can make the plugin bigger without needing to deal with global variables since we always get the same instance of the "Vanity" class when we use method singleton().

You see the method bootstrap() will be automatically called after creating object in method singleton(). Thus, everything related to add_action, add_filter should be put here.

If you are not familiar with Singleton pattern, then read A beginners guide to design patterns.

Experienced PHP devs may wonder why I didn't put that code in __construct() method. This is for reasons of safety. If you have too much long-execution code in method __construct(), then what might happen if one of those lines calls to method singleton() again. Well, at that time, the executing of __construct() has not finished yet, thus, the object is not yet returned; Therefore Vanity::$_self is not assigned. As the result method singleton() will create an object one more time. In order to be safe, we should not put code which call method singleton() in constructing method.

We manually call bootstrap() in method singleton() after object is created. Of course, if you make sure nothing goes wrong you can just put it in _construct() straight away.
For now, we will put every code related to add_action, add_filter in method bootstrap().


Step 2 Dealing with custom post type

In this step, we will register our custom post type, add a meta box to display our vanity key and short link.

Registering Our Custom Post Type

We register post type whose name is stored in self::POST_TYPE. I didn't use hard-coding so you can easily change post type name to anything. Also, we just need WordPress to show a title field, and an author field for our post type. We just need title field to enter original URL. A custom field is used for the vanity key. You will handle it later. Now let's create method init() to register the post type:

1
2
    public function init() {
3
        $args = array(
4
            'labels' => array(
5
                'name' => _x('Short Urls', 'post type general name'),
6
                'singular_name' => _x('Short Url', 'post type singular name'),
7
                'add_new' => _x('Add Url', self::POST_TYPE),
8
                'add_new_item' => __('Add New Url'),
9
                'edit_item' => __('Edit Url'),
10
                'new_item' => __('New Url'),
11
                'all_items' => __('All Urls'),
12
                'view_item' => __('View Url'),
13
                'search_items' => __('Search Urls'),
14
                'not_found' =>  __('No url found'),
15
                'not_found_in_trash' => __('No urls found in Trash'), 
16
                'parent_item_colon' => '',
17
                'menu_name' => 'Urls'
18
            ),
19
            'public' => true,
20
            'publicly_queryable' => true,
21
            'show_ui' => true, 
22
            'show_in_menu' => true, 
23
            'query_var' => true,
24
            'rewrite' => true,
25
            'capability_type' => 'post',
26
            'has_archive' => true, 
27
            'hierarchical' => false,
28
            'menu_position' => null,
29
            'supports' => array('title','author')
30
          ); 
31
        register_post_type(self::POST_TYPE, $args);
32
    }

There's not that I can add to the code above. We register with register_post_type and set a label and text for it. Now we'll let WordPress know we want to hook into it with the init hook! We use modified method _bootstrap():

1
2
    private function _bootstrap() {
3
        add_action('init', array($this, 'init'));
4
    }

Adding Custom Meta Box

To store the vanity key, we use a custom field called _vanity_key. As editing/adding post, we display a form or in other words, custom meta box, with information about short link (via appending _vanity_key to the site's url), and a text box to let the user enter their own vanity key instead of generating key automatically.

1
2
    private function _bootstrap() {
3
        add_action('init', array($this, 'init'));
4
        add_action('add_meta_boxes', array($this, 'add_meta_box'));
5
    }
6
7
    public function add_meta_box() {
8
        add_meta_box("vanity-meta", "Short URL", array($this, 'meta_box_content'), self::POST_TYPE, "normal", "low");
9
    }
10
11
    public function meta_box_content() {
12
        global $post;
13
        wp_nonce_field('my_vanity_nonce', 'vanity_nonce');
14
15
        $_vanity_key = get_post_meta($post->ID, '_vanity_key', true);
16
17
        <p>
18
            <label><?php echo __('URL Key') ?>:</label>
19
            <input name="_vanity_key" type="text" value="<?php echo $_vanity_key ?>" />
20
            You can put your custom url key here if wanted!
21
        </p>
22
        <?php
23
        if (!empty($_vanity_key)) :
24
            ?>
25
            <p>
26
                <label><?php echo __('Your whole shorted url') ?>:</label>
27
                <input size="50" type="text" value="<?php echo trailingslashit(get_bloginfo('url')), $_vanity_key ?>" />
28
                <a target="_blank" href="<?php echo trailingslashit(get_bloginfo('url')), $_vanity_key ?>">Try it</a>
29
            </p>
30
        <?php endif ?>
31
32
        <?php
33
    }

You can use get_post_meta to get the value of any custom field of a post. If you are still not familiar with meta box and custom field, let read this amazing tutorial again.

We use action add_meta_boxes to register our new meta box, then we use the method, meta_box_content(), to render its inside content! When displaying meta box, we try to get the value of custom field _vanity_key. If we got a non empty value, we display the whole short url with that vanity key and a "Try it" link so that the user can click on it to try short url in a new window!

At this point, if you try to add a new URL you have form like this:

If you edit an URL, you have form like this:

Saving the custom field

When we save the post, WordPress just saves title of post, we must handle our custom field in meta box ourselves. When any post is saved, action save_post is called, so we'll hook into this action:

1
2
    private function _bootstrap() {
3
        add_action('init', array(&$this, 'init'));
4
        add_action('add_meta_boxes', array($this, 'add_meta_box'));
5
        add_action('save_post', array($this, 'save_url'));
6
    }
7
8
    public function save_url($post_id) {
9
        global $post;
10
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
11
            return;
12
13
        // if our nonce isn't there, or we can't verify it, bail 	

14
        if (!isset($_POST['vanity_nonce']) || !wp_verify_nonce($_POST['vanity_nonce'], 'my_vanity_nonce'))
15
            return;
16
17
        // if our current user can't edit this post, bail  

18
        if (!current_user_can('edit_post'))
19
            return;
20
21
        $_vanity_key = empty($_POST['_vanity_key']) ? base_convert($post_id, 10, 36) : preg_replace('/[^a-z0-9_]/i', '_', $_POST['_vanity_key']);
22
        $old_key = get_post_meta($post_id, '_vanity_key', true);
23
        if ($_vanity_key == $old_key) {
24
            //We are updating post, and the key is not changed so it's not necessary to save again

25
            return;
26
        }
27
        update_post_meta($post_id, '_vanity_key', $_vanity_key);
28
    }

If we are saving the post automatically, there is no point to save our custom field. We also checked to make sure form has a valid nonce field to avoid double submit and to make sure data came from the right place.

If the users entered a value in the vanity field, then value of $_POST['_vanity_key'] in PHP is not empty, let's use it, otherwise we automatically generate a key by converting the id of post into base 36 number. Then we use update_post_meta to save it. Before saving, we get the current value of custom field _vanity_key and compare it with our new key which is entered by the user or generated by our code to see if we really need to save it. If the old value and new value are the same there is no point to save it again.

Error handling

Everything is looking pretty good at this point, but maybe you're wondering what happens if the user enters a vanity key which has been used before? Or what if the user enters an invalid url? We need some sort of error handling to help the users along the way here.

Firstly, let's create a method called _key2url. As its name says about it, it will receive a key, and try to find if we already have an URL corresponding to this key.

1
2
    /**

3
     * Find the original url respond to this key

4
     * @global wpdb $wpdb

5
     * @param string $key

6
     * @return bool or string

7
     *         false if not found and original url corresponding otherwise

8
     */
9
    private function _key2url($key) {
10
        global $wpdb;
11
        $sql = "

12
            SELECT m.post_id, p.post_title as url

13
                FROM {$wpdb->prefix}postmeta as m 

14
                LEFT JOIN {$wpdb->prefix}posts as p ON m.post_id=p.id

15
                WHERE  m.meta_key='_vanity_key' AND m.meta_value='%s'

16
            ";
17
        $result = $wpdb->get_row($wpdb->prepare($sql, $key));
18
        if (!$result) {
19
            return false;
20
        }
21
        'http://' != substr($result->url, 0, '7') && $result->url = 'http://' . $result->url;
22
        return $result->url;
23
    }

If a vanity key has not already been used, then a false value will be returned. Otherwise, the URL which matches with that key in database will be returned. We also prepend 'http://' to URL if needed. We must do this because of missing leading 'http://' can make WordPress redirect to ourdomain.com/original.com instead http://original.com.

WordPress stores custom fields in the table "wp_postmeta", and posts are stored in the table "wp_posts". Here "wp_" is a prefix which we can access via $wpdb->prefix. We use MySql JOIN clause to match data. Below is a figure on how WordPress store our custom field (our vanity key in this case)

Okay, let's change our method save_url for some error handling. Note that I added two new methods: invalid_key and invalid_url. We will detail this later.

1
2
    public function save_url($post_id) {
3
        global $post;
4
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
5
            return;
6
        
7
        // if our nonce isn't there, or we can't verify it, bail 

8
        if (!isset($_POST['vanity_nonce']) || !wp_verify_nonce($_POST['vanity_nonce'], 'my_vanity_nonce'))
9
            return;
10
            
11
        // if our current user can't edit this post, bail  

12
        if (!current_user_can('edit_post'))
13
            return;
14
15
        //Also, if the url is invalid, add custom message

16
        if (!preg_match('|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i', $post->post_title)) {
17
            add_filter('redirect_post_location', array($this, 'invalid_url'));
18
        }
19
20
        $_vanity_key = empty($_POST['_vanity_key']) ? base_convert($post_id, 10, 36) : preg_replace('/[^a-z0-9_]/i', '_', $_POST['_vanity_key']);
21
        $old_key = get_post_meta($post_id, '_vanity_key', true);
22
        if ($_vanity_key == $old_key) {
23
            //We are updating post, and the key is not changed so it's not necessary to save again

24
            return;
25
        }
26
        //If our key already exists! Let regenerate till we get a new key

27
        while ($this->_key2url($_vanity_key)) {
28
            $_vanity_key = base_convert(time() + rand(1, 10000), 10, 36);
29
            add_filter('redirect_post_location', array($this, 'invalid_key'));
30
        }
31
        update_post_meta($post_id, '_vanity_key', $_vanity_key);
32
    }
33
34
    public function invalid_key($location, $errnum) {
35
        return $location . '&vanity_message=2';
36
    }
37
38
    public function invalid_url($location, $errnum) {
39
        return $location . '&vanity_message=1' ;
40
    }

We use preg_match('|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i', $post->post_title) to check if we have a valid URL. preg_match returns the number of times pattern matches.

In this case, |^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i is a regular expression pattern for an URL which start with http or https. URL is $post->post_title (remember we use title of the post as original url).

If URL is not valid, we will call add_filter to warning error. Don't worry about what it means now, I will cover it later. Also, once we got the new vanity key which we assigned to $_vanity_key, we call method _key2url in while loop to make sure no post is used that vanity key before.

If the vanity key is already used, we generate a new key. We get the current time by using function time() which returns an int number then plus with a random number and convert total result to a base 36 number .

So, how do we notify the user of these on WordPress back-end? The mechanism for solving this inside WordPress looks like this: after saving post, WordPress redirects the user to the post editing page, and appends some parameters to URL as flags to display messages. Try to look at these to figure out and notice the parameter "message" in URL and the actual message in yellow.

If you look at the URL of WordPress after saving a post, you see something like this: http://house.axcoto.com/vanity/wp-admin/post.php?post=13&action=edit&message=1 and a message is shown as in the picture below:

If you try to modify the "message" parameter on URL you have:

Unfortunately, WordPress doesn't have a document for filter redirect_post_location now but you can understand simply that this hook gives us a simple way to modify the URL which WordPress will redirect to after saving a post.

Well, you should understand now how WordPress shows notifications to the user via parameters on URL. So when saving our post type in method save_url, if any error happens, we will alter the URL which WordPress will redirect to and append our custom parameter. WordPress provides filter redirect_post_location to do this. This is an extract from above code for you see it more clearly:

1
2
    //..

3
   add_filter('redirect_post_location', array($this, 'invalid_key'));
4
   add_filter('redirect_post_location', array($this, 'invalid_url'));
5
   //..

6
7
   public function invalid_key($location, $errnum) {
8
        return $location . '&vanity_message=2';
9
    }
10
11
    public function invalid_url($location, $errnum) {
12
        return $location . '&vanity_message=1' ;
13
    }

In each case, we append a custom parameter vanity_message with a value: 1 means invalid URL, 2 means the key is already use. Next. we must show our custom message with this vanity_message. Let's modify our method meta_box_content:

1
2
    public function meta_box_content() {
3
        global $post;
4
        wp_nonce_field('my_vanity_nonce', 'vanity_nonce');
5
6
        $_vanity_key = get_post_meta($post->ID, '_vanity_key', true);
7
8
        if (!empty($_GET['vanity_message'])) :
9
            switch ((int) $_GET['vanity_message']) :
10
                case 1:
11
                    echo '<div class="updated"><p> URL is not valid</p></div>';
12
                    break;
13
                case 2:
14
                    echo '<div class="updated"><p>Your custom key is already existed so we generated other key</p></div>';
15
                    break;
16
            endswitch;
17
        endif
18
        ?>
19
        <p>
20
            <label><?php echo __('URL Key') ?>:</label>
21
            <input name="_vanity_key" type="text" value="<?php echo $_vanity_key ?>" />
22
            You can put your custom url key here if wanted!
23
        </p>
24
        <?php
25
        if (!empty($_vanity_key)) :
26
            ?>
27
            <p>
28
                <label><?php echo __('Your whole shorted url') ?>:</label>
29
                <input size="50" type="text" value="<?php echo trailingslashit(get_bloginfo('url')), $_vanity_key ?>" />
30
                <a target="_blank" href="<?php echo trailingslashit(get_bloginfo('url')), $_vanity_key ?>">Try it</a>
31
            </p>
32
        <?php endif ?>
33
34
        <?php
35
    }

We can output the error message in our meta box. But the good thing is as long as you set the class of any element on the page to "updated" then WordPress automatically grabs it and moves it to right place like this:

You maybe say "wow" after reading this! But as I told you, WordPress is really clever and does many things for you, just they cannot document everything.


Step 3 Detecting url and redirecting

At this moment, you were able to add a new URL and save the URL. It's now time to try shorten the URL now!

What happens if an user hits short url? Well, WordPress loads up, and will process URL into "query" property of class wp_query.

This class has a global instance: $wp_query. We will hook into one of the WordPress hooks before header is printed out to redirect the users to the original url. If the header is printed out, how come we can make redirect, right? To make it easier to understand, let's hook into action 'get_header'.

1
2
    private function _bootstrap() {
3
        add_action('init', array($this, 'init'));
4
        add_action('add_meta_boxes', array($this, 'add_meta_box'));
5
        add_action('save_post', array($this, 'save_url'));
6
7
        add_action('get_header', array($this, 'check_url'));
8
    }
9
10
    public function check_url() {
11
        global $wp_query;
12
        global $wpdb;
13
        if (!$wp_query->is_404) {
14
            //This is a valid URL of WordPress!

15
            return false;
16
        }
17
        $key = empty($wp_query->query['pagename']) ? false : $wp_query->query['pagename'];
18
        if ($key && $url = $this->_key2url($key)) {
19
            wp_redirect($url);
20
        }
21
    }

So, when you go to url domain.com/foo WordPress will store "foo" as "pagename" of $wp_query->query if it cannot detect any permalink (post slug, category name,..) which matched this URL. Once we got the key, we call method _key2url to get URL of that key. If it found one, we redirect to that original URL. Alternatively, we just call _key2url if WordPress output did not find a page! There is no point to call _key2url every time because it needs query database and this can be a performance issue if your site has huge traffic. Finally, you have done it! That's all that you need to do to have a shorten url service with WordPress.

Step 4 Making it even better

At this point, you can add a url and have a list of url posts in WordPress dashboard! But to see the short url and vanity key, you must edit a post to see it... That's really annoying! So, let's put this vanity key on post listing page. We can achieve this with filter manage_edit-{post_type}_columns and action manage_{post_type}_custom_column

Filter allows us add more columns when listing our custom post type besides normal columns such as: title, author,...etc. Action lets us really build content for that column. As always, you must inject add_action and add_filter to the _bootstrap method:

1
2
    private function _bootstrap() {
3
        add_action('init', array($this, 'init'));
4
        add_action('add_meta_boxes', array($this, 'add_meta_box'));
5
        add_action('save_post', array($this, 'save_url'));
6
7
        add_action('get_header', array($this, 'check_url'));
8
        add_filter('manage_edit-' . self::POST_TYPE . '_columns', array($this, 'custom_column'));
9
        add_action('manage_' . self::POST_TYPE . '_posts_custom_column', array($this, 'column_content'), 10, 2);
10
    }	
11
12
	/**

13
	* WordPress will pass an array of columns to this function. 

14
	* The key of each element is the name of column.

15
	* @param array of columns

16
	*/
17
    public function custom_column($columns) {
18
        $columns['_vanity_key'] = __('Vanity Key', 'wp-vanity');
19
        return $columns;
20
    }
21
22
    public function column_content($column_name, $post_id) {
23
        global $wpdb;
24
        switch ($column_name) {
25
            case '_vanity_key':
26
                $key = get_post_meta($post_id, '_vanity_key', true);
27
                if ($key) {
28
                    echo sprintf('<a target="_blank" href="%s" title="Original URL">%s</a>', trailingslashit(get_bloginfo('url')), $key, $key);
29
                }
30
                break;
31
        }
32
    }

Columns are stored in an array which is passed to our filter method. We add a new column via appending a new element to that array. Method "custom_column" takes care of this. The modified array is returned, WordPress grabs returned value and recognizes the new column. The name of column is _vanity_key. We use it to reference our column later. The title of column is "Vanity Key" - this is the text which appears on table header.

We use method "column_content" to output content of this column. WordPress passes two parameters to functions which hooked action manage_{post
type name}_posts_custom_column
: The first one is name of column, the second one is the id of the post which is rendering.

Based on this, we check to make sure value of variable $column_name is _vanity_key, the name of our column. Then we use get_post_meta to read the custom field _vanity_key. Finally, we print out an "a" element with target="_blank" to open it in a new window. If you had other columns, you could continue with other "case" statements for those columns.

Now you can take a look at two pictures: before and after using above filter, action. The first one doesn't have a vanity column while second one has a vanity column with each post's vanity key.

Conclusion

Finally, you now have your own shorten url service with just around 60-70 minutes of coding and can use your current WordPress site with current domain. Hopefully, you've found this tutorial to be of help. Feel free to reuse this code elsewhere in your projects. If you have something to say or share, or even teach me as well, then please leave me a comment!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.