Difference between #9 and #8 of Using search engine and user friendly URLs

unchanged
Title
Using search engine and user friendly URLs
unchanged
Category
Tutorials
unchanged
Tags
SEO, URL
changed
Content
The [Definitive Guide](/doc/guide/topics.url) introduces the fundamentals of
managing URLs in a Yii application. In this tutorial, we introduce a practical
technique that can quickly turn your application into using
search-engine-friendly URLs.

Assume we have an application that mainly consists of CRUD operations for
several object types. For example, in the blog demo, we need CRUD operations for
`Post`, `Comment` and `User`. Using `Post` as an example, our goal is to
implement its CRUD operations with the following URLs:

* reading a post: `http://example.com/post/99/this+is+a+sample+post`
* listing posts: `http://example.com/post`
* listing posts with pagination and sorting:
`http://example.com/post?page=2&sort=title`
* creating a post: `http://example.com/post/create`
* updating a post: `http://example.com/post/update?id=99`
* deleting a post: `http://examplecom/post/delete?id=99`

The above URLs are all very short and readable. For the "reading a
post" URL, we append the post title to the URL because it is preferred by
search engines which expect URLs themselves to provide some useful information
when indexing the corresponding pages. The rest of the pages are less important
for search engines because they provide less information. For this reason, we
put all parameters in the query string part instead of path info.


Configuring `CUrlManager`
-------------------------

In order to accomplish the above URL formats, we need to configure the
[CUrlManager] component in the application configuration as follows,

~~~
[php]
return array(
	'components'=>array(
		'urlManager'=>array(
			'urlFormat'=>'path',
			'showScriptName'=>false,
			'rules'=>array(
				'<controller:\w+>'=>'<controller>/list',
				'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
				'<controller:\w+>/<id:\d+>/<title>'=>'<controller>/view',
				'<controller:\w+>/<id:\d+>'=>'<controller>/view',
			),
		),
	),
);
~~~

In the above configuration, we first specify to use the `path` URL format which
turns the usual query-string-based URLs into path-info-based ones (e.g. from
`/index.php?r=post/list` to `/index.php/post/list`.) We then state that the
entry script name should be hidden from the URLs. Last, we specify a list of URL
formatting rules based on our earlier description.

Let's explain a bit more about the URL formatting rules. 

In the first rule, we specify that if the path info of an incoming URL is a
single word (e.g. `post`), then it should be treated as a controller ID and the
corresponding route should be the controller ID with the `list` action. In our
example, this means if the path info is `post`, it should be treated as the
`post/list` route; if the path info is `comment`, it would be `comment/list`;
and so on. On the other hand, when we create a URL by calling
`$controller->createUrl('post/list')`, this rule would apply and we should
obtain a URL `/post`. If we pass in additional GET parameters when creating the
URL, they will appear in the query string part (e.g. `/post?page=99`, which may
be generated by a pager widget).

In the second rule, we specify that if the path info of an incoming URL consists
of two words separated by a slash, then they form the needed the route. For
example, if the path info `post/create`, then this would also be the route to
execute the request (i.e., the `post` controller and the `create` action). When
we create a URL by calling `$controller->createUrl('post/create')`, this rule
would apply and we should obtain a URL `post/create`. Any additional GET
parameters would be appended to the query string part of the URL (e.g.
`/post/update?id=99`).

The last two rules are for the reading URLs. One rule is for reading URLs with
the `title` parameter, and one without.


Hiding Entry Script from URL
----------------------------

We need one more step in order to remove `index.php` from our URLs, i.e.,
configuring the Web server. For Apache HTTP server, as described in the
[Definitive Guide](/doc/guide/topics.url), we need to place a file named
`.htaccess` under the directory containing the entry script. The file should
have the following content:

~~~
Options +FollowSymLinks
IndexIgnore */*
<IfModule mod_rewrite.c>
RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# otherwise forward it to index.php
RewriteRule . index.php
</IfModule>
~~~

Please consult the user reference if you are using a different Web server.


Keeping DRY (Don't Repeat Yourself)
-----------------------------------

We often need to insert URLs pointing to object detail pages. For example, in
the object list page, we need to link to the detail page for every object; In an
object detail page, we may also need to link to the detail page for its related
objects. 

We can certainly call `createUrl()` in all relevant view scripts. However, in
order to keep DRY, a better place to call this method is in the model classes.
In each model class, we can add a `url` property which returns the result of
calling `createUrl()`. Then, whenever we need the URL to the detail page of an
object, we can readily obtain it using the expression `$object->url`.

The above approach can be further improved. That is, instead of implementing the
`url` property in every model class, we do it in a base model class and have all
concrete model classes to extend from this base class. Below is such an attempt,

~~~
[php]
class ActiveRecord extends CActiveRecord
{
	public function getUrl()
	{
		$controller=get_class($this);
		$controller[0]=strtolower($controller[0]);
		$params=array('id'=>$this->id);
		// add the title parameter to the URL
		if($this->hasAttribute('title'))
			$params['title']=$this->title;
		return Yii::app()->urlManager->createUrl($controller.'/view', $params);
	}
}

// class Post extends ActiveRecord { ... }
~~~

We can save this base class in a file named `ActiveRecord.php` and put it under
the directory `protected/components` so that when we extend this class to write
new model classes, we do not need to explicitly include this class file, thanks
to the Yii class autoloading feature.

With the above approach, we can achieve very flexible URL schemes while keeping
our code clean. If some day we want to change the format of the detail page URL
(e.g., besides title, we also want to add category information into the URL), we
can immediately accomplish this by updating only the `getUrl()` method in the
base class. If, a model has a very special format for its detail page URL, we
can override the `getUrl()` method in this model class.

Finally, it may also be a good idea to implement in the model classes the
methods that return other type of URLs. For example, we can implement the
`getListUrl()` method in the base class similar to what we do for `getUrl()`.
Then we can readily obtain the list page URL for `Post` model using the
expression `Post::model()->listUrl`.

### Links
[Chinese version](http://projects.ourplanet.tk/node/97)
Write new article
  • Written by: qiang
  • Updated by: Yang He
  • Category: Tutorials
  • Yii Version: 1.1
  • Votes: +23 / -1
  • Viewed: 61,101 times
  • Created on: Oct 23, 2009
  • Last updated: Jun 30, 2012
  • Tags: SEO, URL