Using URL Slugs in a CakePHP App
URL slugs are extremely common on the internet today, particularly on website blogs. The title of the post is used as an identifier in the URL instead of an id number, this makes it possible for a person looking at the URL to know what the post is generally about. For example this post in your address bar is seen as "/posts/view/using_url_slugs_in_a_cakephp_app" instead of "/posts/view/1" which makes the URL meaningful and human readable.
In this article I'm going to go through the benefits of using URL slugs and how to incorporate them into the CakePHP blog application that we have been building over the previous articles.
Advantages of using URL Slugs
- Your URLS are cleaner and more readable
- People can see what the post is about by the title
- Can help with Search Engine Optimisation as keywords are a part of the url
Modifying the Database
Following on from my previous article i'm going to edit the blog application to use URL slugs instead of ids, I'm going to do this for both 'posts' and 'tags'. The first thing we need to do is to modify the database so that the posts and tags tables have a 'slug' column. To do use the following SQL command in phpmyadmin:
ALTER TABLE `posts` ADD `slug` VARCHAR( 255 ) NOT NULL AFTER `title` ; ALTER TABLE `tags` ADD `slug` VARCHAR( 255 ) NOT NULL AFTER `tag` ;
The String to Slug Function
In the past I've been using a fairly basic function to turn a string into a url slug and that function can be seen below, however when researching this article i came across an inbuilt CakePHP function that will do the job better.
function stringToSlug($str) {
// trim the string
$str = strtolower(trim($str));
// replace all non valid characters and spaces with an underscore
$str = preg_replace('/[^a-z0-9-]/', '_', $str);
$str = preg_replace('/-+/', "_", $str);
return $str;
}
The CakePHP Inflector class has a function that will turn a string into a slug in one line of code and it takes 2 arguments, the string to convert and a character to replace the spaces with the default character being an underscore i.e. '_'. We now just need to convert the string to lowercase and our function is complete:
// file: 'app/app_controller.php'
function stringToSlug($str) {
// turn into slug
$str = Inflector::slug($str);
// to lowercase
$str = strtolower($str);
return $str;
}
// CakePHP code in the slug function uses a regular expression:
$string = preg_replace(array('/[^\w\s]/', '/\\s+/') , array(' ', $replacement), $string);
I'm going to place this function in our 'app_controller' file located at '/app/app_controller.php' so that we can use this function across all our controllers by simply calling:
$this->stringToSlug("String to Convert");
Editing the Blog Application
The first thing I'm going to do is modify the 'posts_controller' so that when we try to add a new post, a URL slug is automatically created and inserted into the database. This is extemely easy by creating the slug variable in the $this->data array before it saved to the database.
// file: 'app/controllers/posts_controller.php'
// create the url title slug
$this->data['Post']['slug'] = $this->stringToSlug($this->data['Post']['title']);
// try saving the data
if ($this->Post->save($this->data)) {
When a new post is added to the database, the controller takes the title of the post, turns it into a slug with the function we created earlier and saves it in the 'slug' column of the table. If we have a quick look in our database you will see that the slug was successfully created.
Add the same code to the 'edit' action to make sure that a new URL slug is created when we try to edit a post, this also makes it easy to create slugs for posts that currently don't have one. So quickly go into the edit screen of each post and click save and a URL slug will be created automatically.
The next step is to change the 'view' function to start using the slugs instead of the post id, this is easily done by using CakePHP's magic 'findBy
// file: 'app/controllers/posts_controller.php'
function view($slug = null) {
if (!$slug) {
$this->Session->setFlash('Invalid id for Post.');
$this->redirect('/posts/');
}
$this->set('post', $this->Post->findBySlug($slug));
}
Now we just need to make a quick change to the 'Posts' 'index.thtml' file to change the post link to use the url slug:
// file: 'app/views/posts/index.thml'
// was
echo $html->link('View','/posts/view/' . $post['Post']['id']);
// now
echo $html->link('View','/posts/view/' . $post['Post']['slug']);
We can now test that everything is working by clicking on the 'view' link in the posts index, the application now uses the slug in the URL and successfully finds the post in the database and displays it correctly.
Further Work
Once you have got the 'posts' working with URL slugs it is easy to use the same process to convert any other items such as the 'tags' controller. Please by aware that the process we have been through does not factor in a check to see if the slug is already used in the database. A simple function could be created to do this check and append the new slug with an incremental value but I'm not going to go through that now.
Wrapping Up
So there you have it, incorporating URL slugs into your CakePHP application is really not that hard and the benefits of having a clean, readable url that will help with your SEO campaign far outway the little time and effort that you must put in to make it all work. Hopefully this article has been clear and easy to understand but if your having trouble let me know and I'll try and help you out as much as I can.
In my next article i'm going to go through the process of validating data that a user submits and how to automatically display error messages if a required field is missing.
Comments
Baz L (25/04/2008 - 07:03)
There is a nice behavior that I use that does this.
http://bakery.cakephp.org/articles/view/slug-behavior
It does this automatically on creating (or saving) a model.
James (25/04/2008 - 12:10)
Thanks Baz that Behaviour looks pretty useful, I'll check it out over the next few days. Just been looking at your blog as well, it's got a few nice posts I'll try and get some read over the weekend!
James
Mohamed Hisham (30/10/2008 - 16:29)
WOW, it's Great we can now custom URL i like your blog so much and learned a lot :) Thank you very much,
i want to ask some thing can we use all powerfull of AJAX with cakephp?? because i want to change my develop way by cakephp
Akif (18/05/2009 - 12:14)
Nice work. I am using it now for my new project! Thanks for Sharing.
John (09/09/2009 - 00:55)
I've been using the sluggable behaviour in the bakery for ages... but there are occasions when you need to make a slug in view.
Was completely unaware of Inflector::slug($str); - am now already using it in a helper already!
Cheers
Matt Alexander (17/10/2009 - 11:47)
A history would be good so search engines wouldn't lose the page every time you changed the title.
Mark Conforti (19/10/2009 - 19:35)
James, this is a great article! Got it up and running in no time. I know this is an old post but I'm having an issue I'm hoping you could help me with. Before using findBySlug in my view action, I was using the posts ID. Then in my individual views, I was pulling in images from another table where each image was assigned to a post using it's id. I'm having trouble calling the posts ID now that I'm using slug instead.
In my view action, how can I get the posts ID without calling it? Is there a way to assign it to a variable?
Thanks!
James (19/10/2009 - 22:42)
@Mark: I would get the ID of the Post after the findBySlug() call to the database, that's probably the only way to get it. Or you could tack the id onto the end of the slug and then use string manipulation to parse the id like this:
/posts/view/this_is_a_slug:5
Kind of defeats the point of having a slug but it would work.
Mark Conforti (20/10/2009 - 09:06)
I've actually been trying to get the ID after the slug, that's where I'm having issues.
function view($slug = null) {
if (!$slug) {
$this->Session->setFlash('Invalid id for Post.');
$this->redirect('/posts/');
}
$this->set('post', $this->Post->findBySlug($slug));
$images = $this->Post->Image->find('all', array('conditions'=>array('Image.post_id'=>'Post.id')));
$this->set(compact('post', 'images'));
}
But that doesn't get the Posts id. I'm still new to Cake so that may to way off but if I replace 'Post.id' with 2(or any number), it works fine. Thanks for the quick response. And again, great blog!
James (20/10/2009 - 09:56)
@Mark: Yeah that won't work because the 'Post.id' is just a string and wont contain the actual Post Id, you'll have to do this:
$post = $this->Post->findBySlug($slug);
$images = $this->Post->Image->find('all', array('conditions'=>array('Image.post_id'=>$post['Post']['id'])));
$this->set('post', $post);
Hope that works!
Mark (20/10/2009 - 10:15)
Thank you! Now that I see it, it makes perfect sense! Working great.
And I'm uploading images after reading another tutorial of yours so thanks again!
Coen (28/10/2009 - 13:51)
How do I go about in making a check for slug uniqueness? I've only found one tutorial, but is does in no way work with this as far as I can see.
James (28/10/2009 - 23:56)
@Coen: you would have to check the database to see if that slug already exists, if you want an incremental slug like you mentioned i.e: slug_2, slug_3 then you'll have to create a loop and keep checking the database until the slug isn't found and use that one.
cetver (14/01/2010 - 10:43)
few tips:
without findBySlug
function view($slug = null) {
if (!$slug) {
$this->Session->setFlash('Invalid id for Post.');
$this->redirect('/posts/');
}
$post = $this->Post->find('first', array(
'conditions' => array('slug' => $slug)
)
);
$this->set('post', $post);
}
if you want url like http://www.site.com/my_super_page (http://www.site.com/posts/view/1)
add in /app/config/route.php
Router::connect('/*', array('controller' => 'posts', 'action' => 'view'));
Waqas (29/06/2010 - 09:08)
Hi James,
Thanks for this sharing. It's really helpful. However, I've found another bit that is just a function to add in App Model and you can use it wherever you want.
Also, it handles the uniqueness of slug itself. You don't need to worry about it :)
give it a go: http://www.whatstyle.net/articles/52/generate_unique_slugs_in_cakephp
best regards,
Waqas