Adding friendly URLs to The Cake Blog Tutorial

By Mariano Iglesias (mariano)
On this tutorial we'll learn how to modify The Cake Blog Tutorial to allow friendly URLs when accessing posts.

Introduction

How many times you wondered how great it would be if your URLs didn't look so much like:

http://www.server.com/posts/view/1058
But more like:

http://www.server.com/posts/view/my_first_post
Jeez even the bakery could use something like this :)

On this article I'm going to show you how easy it is to make your model records be accessed by friendly URLs. In fact, we're going to modify CakePHP's popular The Cake Blog Tutorial to allow friendly URLs.

Modify your Table

The first thing we'll need to do is to add a field called url that will hold a unique value for each post. On the blog tutorial you can see that we have a table called posts with several fields. Run your database administration and issue the following command:

Download code ALTER TABLE `posts` ADD `url` VARCHAR(255) NOT NULL AFTER `id`;
We just added a field called `url` after field `id`.

Create your AppModel

The next step will be to add the logic on the model to allow the automatic creation of a unique value for the field url based on the post title.

Since we may want to add friendly URLs to other models we're going to insert the appropiate methods to our AppModel, rather than adding it to the Posts model. So create a file named app_model.php on your /app directory. It should look like this:

Model Class:

Download code <?php 
class AppModel extends Model
{
    function 
getUniqueUrl($string$field)
    {
        
// Build URL
        
        
$currentUrl $this->_getStringAsURL($string);
        
        
// Look for same URL, if so try until we find a unique one
        
        
$conditions = array($this->name '.' $field => 'LIKE ' $currentUrl '%');
        
        
$result $this->findAll($conditions$this->name '.*'null);
        
        if (
$result !== false && count($result) > 0)
        {
            
$sameUrls = array();
            
            foreach(
$result as $record)
            {
                
$sameUrls[] = $record[$this->name][$field];
            }
        }
    
        if (isset(
$sameUrls) && count($sameUrls) > 0)
        {
            
$currentBegginingUrl $currentUrl;
    
            
$currentIndex 1;
    
            while(
$currentIndex 0)
            {
                if (!
in_array($currentBegginingUrl '_' $currentIndex$sameUrls))
                {
                    
$currentUrl $currentBegginingUrl '_' $currentIndex;
    
                    
$currentIndex = -1;
                }
    
                
$currentIndex++;
            }
        }
        
        return 
$currentUrl;
    }
    
    function 
_getStringAsURL($string)
    {
        
// Define the maximum number of characters allowed as part of the URL
        
        
$currentMaximumURLLength 100;
        
        
$string strtolower($string);
        
        
// Any non valid characters will be treated as _, also remove duplicate _
        
        
$string preg_replace('/[^a-z0-9_]/i''_'$string);
        
$string preg_replace('/_[_]*/i''_'$string);
        
        
// Cut at a specified length
        
        
if (strlen($string) > $currentMaximumURLLength)
        {
            
$string substr($string0$currentMaximumURLLength);
        }
        
        
// Remove beggining and ending signs
        
        
$string preg_replace('/_$/i'''$string);
        
$string preg_replace('/^_/i'''$string);
        
        return 
$string;
    }
}
?>

The method _getStringAsURL() converts a string to a friendly URL form. For example, running:

Download code _getStringAsURL('Hello CakePHP baker, baking hard?');
Will be transformed into:

Download code hello_cakephp_baker_baking_hard
The method getUniqueUrl takes two parameters:

  1. $string: the string that will be used to generate the URL. On our case this is the post title.
  2. $field: the field that will hold the generated URL. On our case this is url.

It will start by generating the friendly URL version of the post title and then look over the table to see if the generated URL was assigned to another record. If so, it will add _1, _2, _3, etc. until it finds a unique version.

It is important to know that we will only generate a friendly URL when the post is being inserted to the database, not when it is being modified. This is a common procedure on friendly URL generation since you never know if you already have incoming links to the generated URL.

Modify your Model

Now we are ready to modify the Post model to allow the creation of a friendly URL when inserting a new post. As The Cake Blog Tutorial shows the latest version of the file /app/models/post.php looked like this:

Model Class:

Download code <?php 
class Post extends AppModel
{
    var 
$name 'Post';
    
    var 
$validate = array(
        
'title'  => VALID_NOT_EMPTY,
        
'body'   => VALID_NOT_EMPTY
    
);
}
?>

Change it so we can add the URL generation. It should now look like this:

Model Class:

Download code <?php 
class Post extends AppModel
{
    var 
$name 'Post';
    
    var 
$validate = array(
        
'title'  => VALID_NOT_EMPTY,
        
'body'   => VALID_NOT_EMPTY
    
);
    
    function 
beforeSave()
    {
        if (empty(
$this->id))
        {
            
$this->data[$this->name]['url'] = $this->getUniqueUrl($this->data[$this->name]['title'], 'url');
        }
        
        return 
true;
    }
}
?>

As you can see we just added a method called beforeSave(), which is a function that CakePHP automatically calls before saving a model instance to the database. There, we start by checking that the ID for the record has not been set. This is the case when inserting a new post. We then set the value of the url field to be the friendly URL version of the value of the field title.

Now, every time a new post is being inserted to your database a unique friendly URL will be generated.

Modify your View

The next step is to modify the way we are building the links to each post. Edit your file /app/views/posts/index.thtml and look for the following expression:

Download code echo $html->link($post['Post']['title'], "/posts/view/".$post['Post']['id']);
Change it to:

Download code echo $html->link($post['Post']['title'], "/posts/view/".$post['Post']['url']);

Modify your Controller

Last but not least we need to change our controller so it will receive the URL rather than the ID of the post the user is trying to access. Edit your file /app/controllers/posts_controller.php and look for the following block of code:

Download code function view($id = null)
{
    $this->Post->id = $id;
    $this->set('post', $this->Post->read());
}

Change this code to look like this:

Download code function view($url)
{
    $post = $this->Post->findByUrl($url);
    
    $this->set('post', $post);
}

Feedback

If you have any comments / questions try to add them (if you think they'll add value to other bakers) as comments on this page. If you want to contact me directly try:

email: mariano@cricava.com blog: http://www.marianoiglesias.com.ar
Otherwise just drop a question on Cake's Google Group mentioning this tutorial on the subject since I am constantly reading/writing on the group.

Got your CakeSchwag? I bought myself the Baseball Jersey and the Khaki Cap. I have to wait till December 29 for them to arrive (I asked a friend from the states to buy them and bring it to me down here... Argentina is a long way from the US.) What are you waiting for?

Remember, smart coders answer ten questions for every question they ask. So be smart, be cool, and share your knowledge.
BAKE ON

 

Comments 170

CakePHP Team Comments Author Comments
 

Comment

1 Making URLs more friendly for serch engines

Mariano instead of using hello_cakephp_baker_baking_hard, I think that you should use hello-cakephp-baker-baking-hard in order to make the url friendly for search engines.

Emiliano
Posted Dec 7, 2006 by emiliano
 

Comment

2 Marking URLs more friendly for search engines

Mariano instead of using hello_cakephp_baker_baking_hard, I think that you should use hello-cakephp-baker-baking-hard in order to make the url friendly for search engines.
AFAIK There's really no difference between - and _ for SE. However it's easy to change it, just go to the method _getStringAsURL($string) and change any occurence of _ with -. Escape it inside RegExs.
Posted Dec 10, 2006 by Mariano Iglesias
 

Comment

3 Using ID instead of sameURLs

I use something quite similar to this, but instead of using the sameURLs method, I simply tack on the post ID to the end of the post (i.e. url would = post-title-1038) to ensure I have a unique URL
Posted Dec 19, 2006 by Ryan
 

Comment

4 Post Slug

I'm just getting started here and this was a perfect tutorial for me as I just finished the 15min Blog Tutorial. Really well written. Thanks!

One very small suggestion- The field "url" isn't really storing a url, only a part of it. So it could accidentally be missused down the road by some newbie like me, right? I think the proper name is a post "slug." At least that's what they call them in Wordpress.
Posted Dec 20, 2006 by Scott Phillips
 

Comment

5 Adding .html

To make it even more search engine friendly, I use the same approach and add this '.html' in the end :)
Posted Dec 24, 2006 by Sohaib Muneer
 

Comment

6 Turning on and off

I found this tutorial GREAT! Thanks!

What I also did was kept it $id and the immediately set $url = $id. Then I put in a test to see if $sefUrls == true ... and if so then would use the modified controller code above (and view code).

To determine if $sefUrls were true or not...one could set it explicitly for each controller/action OR grab it out of a settings table...there's a real good persistent data article on the bakery here to make that process even better.

This way you get to choose the regular way of $id or this new SEF way of $url.

Or hey, even both...but one would have to be careful of the post name was all numbers =) something would have to be added before the SEF I guess...probably not worth all that trouble. However, it's nice to have an option to turn on/off SEF for a larger app somewhere in an admin settings area.
Posted Sep 10, 2007 by Tom Maiaroto
 

Comment

7 A more native Cake 1.2 approach

I'm rather new to all this, so I'm not sure how long this method has been around, but instead of the one listed in the article, Cake 1.2 RC1 offers Inflector::slug(). To get similar output identical to getUniqueUrl(), convert the title to lower case:

Model Class:

<?php 
if( empty($this->id) ) {
   
$this->data[$this->name]['url'] = Inflector::slug(strtolower($this->data[$this->name]['title']));
}
?>

Inflector::slug is specified as slug($string, $replacement = '_'), so it even allows you to specify which character you'd like to use as a separator!
Posted Jun 17, 2008 by Vee
 

Comment

8 Perfect SEO URLs

Make the following changes to get perfect SEO URL's.

Step1: return $currentUrl; -> return $currentUrl.'.htm'; in the function getUniqueUrl($string, $field).
Step2: $string = preg_replace('/[^a-z0-9_]/i', '_', $string); -> $string = preg_replace('/[^a-z0-9_]/i', '-', $string); in the function _getStringAsURL($string).
Step3: $string = preg_replace('/_[_]*/i', '_', $string); -> $string = preg_replace('/_[_]*/i', '-', $string); in the function _getStringAsURL($string).

That's it! Your are done.
Posted Jul 11, 2008 by GKSR