Search Engine Friendly URLs in CakePHP

One thing that is not built-in to CakePHP is the ability to use search engine friendly controller and action names when those names consist of more than one word. CakePHP does an excellent job of allowing friendly URLs in general, but this typically involves single-word controller and action names. So what about controller and action names with multiple words? In CakePHP, this is handled with class names defined in CamelCase and method names defined in camelBack.

For instance, you may have a blog system built with CakePHP that uses a typical controller name, such as PostsController, and a typical action name, such as view, which takes a blog post slug (search engine friendly name) as its first argument. In the MVC paradigm, this would be accessed with a URL like the one below.

Most everyone would agree that this is a “friendly” URL, and in this case, the sluggable behavior, or something similar, would allow you to easily provide a slug as the first argument to the PostsController->view() method, as shown below.

class PostsController extends AppController {
    public $name = 'Posts';
    public function view($slug) {
        $aPost = $this->Post->findBySlug($slug);
        return $this->set(compact('aPost'));

The purpose of using a slug is to provide a dash-separated (hyphen-separated) version of the blog post title in the URL for SEO, a common feature of most popular blog publishing systems.

Multiple Word Controller Names

CakePHP allows multiple-word controller names to be accessed from the URL in several ways.

  1. CamelCase ControllerName, as in “”;
  2. camelBack controllerName, as in “”;
  3. underscore-separated, as in “”;
  4. or any combination of the above.

This gives you a lot of options for selecting your URL scheme of choice, but one option that is not available out of the box with CakePHP is a dash-separated controller name in the URL. This troubles me because I prefer dash-separated URLs, and Google recommends hyphens instead of underscores in your URLs.

I also prefer to avoid uppercase letters in URLs entirely, so with CakePHP, the only viable option out of the box for multiple word controller and action names is to use underscore-separated names. For these reasons, I decided to implement my own way of allowing for dash-separated controller and action names in the URL to map to their respective CamelCase and camelBack names in CakePHP.

Implementing Dash-separated Controller and Action Names in CakePHP URLs

I will not claim that my method for implementing dash-separated controller and action names in CakePHP is the best way, and it does have its downside, but it is the quickest way that I could think of to accomplish this task without too much trouble.

To do this, I simply added the following snippet of code to the /app/config/routes.php file.

$aUri = explode('/', $_GET['url']);
$bControllerDash = strpos($aUri[0], '-');
$bActionDash = isset($aUri[1]) && strpos($aUri[1], '-');
if ($bControllerDash || $bActionDash) {
    $sController = ($bControllerDash) ? str_replace('-', '_', $aUri[0]) : $aUri[0];
    $sAction = ($bActionDash) ? Inflector::variable(str_replace('-', '_', $aUri[1])) : 'index';
    Router::connect('/' . $aUri[0] . (isset($aUri[1]) ? '/' . $aUri[1] . '/*' : ''), array(
        'controller' => $sController,
        'action' => $sAction

In this block of code, the default $_GET['url'] parameter that gets passed from the query string to CakePHP is analyzed for the existence of dashes (hyphens) in either the controller or action portion of the URL. If either the controller or action name contains any dashes, those dashes are replaced with underscores, and in the case of the action name, it is converted to its camelBack name using CakePHP’s Inflector::variable method. This only applies if you are using action names defined in camelBack, of course.

The Downside

The downside to using this method is that it prevents the proper use of CakePHP’s built-in reverse routing. In other words, using the HtmlHelper::link method to create a link with a code block like the one below will not map to a dash-separated version of the URL in the generated link.

print $html->link('My link', array('controller' => 'controllerName', 'action' => 'actionName'));

Improving Reverse Routing in CakePHP in the Future

I would recommend that the CakePHP team look at adding a configuration option in the future that would allow you to define the type of URLs you would like to be generated when using reverse routing. This way you could always have clean URLs in a consistent format by simply using reverse routing, the /app/config/routes.php configuration file, and a URL type or style setting.

For example, a method could be called inside routes.php with an array of parameters to set the style for all generated URLs:

Router::setUrlStyle(array('case' => 'lower', 'separator' => 'hyphen'));

In this way, such preferences as the case and the word separator could be set for consistency and to ensure search engine friendly URLs.


  1. Hey Philip, thanks for this post. I think it would be helpful for others as well if you submit it on CakePHP site as an article.

    1. @Saliem: The .htaccess file is already set up to point all URLs to the CakePHP index file and route them from there. Handling the routing in the framework itself allows for cleaner and more centralized business logic, but of course everything is debatable.

  2. Hi, I’m a non-developer seeking to create SEO friendly URLs in cake that don’t display the ‘controllers.’:

    The ideal end result:

    Etc, etc

    However, it seems that we have to have something like

    Etc, etc

    Essentially, I dont’ want ‘article’ & ‘view.’ How could this be implemented. I’m thinking that ‘rel canonical’ will not work because the ‘home’ of the URL is unique and not duplicated. Thus I’m thinking that mod_rewrite in Apache is the only option, but have no clue how this works with CakePHP. Many, many thanks in advance for your help!

  3. You can save one query to the databaseHere is my login acoitn (Cakephp 2.0.6)public function login if $this->request->is ‘post’ App::Import ‘Utility’, ‘Validation’ ; if isset $this->data ‘User’ ‘username’ && Validation::email $this->data ‘User’ ‘username’ $this->request->data ‘User’ ’email’ = $this->data ‘User’ ‘username’ ; $this->Auth->authenticate ‘Form’ = array ‘fields’ => array ‘username’ => ’email’ ; if ! $this->Auth->login $this->Session->setFlash __ ‘Invalid username or password, try again’ ; else $this->redirect $this->Auth->redirect ;

Leave a Reply

Your email address will not be published. Required fields are marked *