Wednesday, March 14, 2012

Slugged URLs

Been working with the Yii Framework for little less than a year now. Something that eventually came up in nearly all projects was SEO-friendly URLs. By default, the URLs Yii generates are already very human readable, but sometimes more is needed, like actually including (urlized) names in the URL. That's commonly called Slugging and the Yii wiki contains a few articles about that. Here is what I have been using.

Given the model Product with a "name" attribute, I first add a method to return a slug, which in this case is the "name" attribute in urlized form.

<?php
class Product extends CActiveRecord
{
    //...
    public function getSlug()
    {
        return YourFavoriteUrlizer($this->name);
    }
    //...
}

Then I add a method to get the route relevant params of the model:

<?php
class Product extends CActiveRecord
{
    //...
    public function getRouteParams()
    {
        return array('id'=>$this->id, 'slug'=>$this->slug);
    }
    //...
}

Now to link to specific instances of Product:

echo CHtml::link($product->name, array('product/view') + $product->routeParams);

CHtml::link uses CHtml::normalizeUrl which in turn uses CUrlManager and routes defined in protected/config/main.php to generate a URL.

Using the default routes, currently the URL of the link would look like this:

http://domain.tld/product/1?slug=urlized-name-of-product

That's not ideal, since the urlized Product or Category name shouldn't appear as query string. The routes need to be extended:

<?php
    //...
    'rules'=>array(
        // these are standard routes
        '<controller:[\w-]+>/<id:\d+>'=>'<controller>/view',
        '<controller:[\w-]+>/<action:[\w-]+>/<id:\d+>'=>'<controller>/<action>',
        '<controller:[\w-]+>/<action:[\w-]+>'=>'<controller>/<action>',
        // SEO friendly
        '<controller:[\w-]+>/<slug:[\w\d-]+>-<id:\d+>'=>'<controller>/view',
    ),
    //...

Now CUrlManager generates URLs like this:

http://domain.tld/product/urlized-name-of-product-1

The new URL rule doesn't interfere with the regular default routes. Only if the "slug"-parameter is added to the route's parameters, will it generate the SEO friendly url. Nothing changes for the controller view action, the slug parameter will just be ignored.

If generating the slug is expensive or it should act as perma-link, it could be stored in the database. Just add a column slug to the database and add something like this to Product::beforeSave:

<?php
class Product extends CActiveRecord
{
    //...
    public function beforeSave()
    {
        if (parent::beforeSave())
        {
            if (empty($this->slug))
            {
                $this->slug = $this->getSlug();
            }
        }
    }
    //...
}

That's the really nice part about Yii, CComponent and it's dynamic properties through getters and setters. In views or controllers, use attributes like slug in this case. Then you're free to use getters/setters or later change to fields in the database, without having to change every place where you used the slug property!

No comments:

Post a Comment