Actions

Difference between revisions of "Supporting SEF URLs in your component"

From Joomla! Documentation

(Update 3.0 to 3.1)
(47 intermediate revisions by 17 users not shown)
Line 1: Line 1:
{{incomplete}}
+
{{version|1.5,2.5,3.1}}
{{RightTOC}}
+
{{:Search Engine Friendly URLs}}
[[Category:Development]]
+
In Joomla!, each [[component]] is responsible for handling its own SEF URLs. Therefore, as the [[Developers|developer]] of a component, you will have to create your own '''router''' to allow your component to use SEF URLs.
  
== Routing Overview ==
+
== The Concept ==
  
Joomla! 1.5 is capable of creating and parsing URLs in any format, including human readable URL's. Another improvement is that this still works even if Joomla! runs a server other than Apache with the mod_rewrite module.
+
Assuming you are following standard development practices, your component is probably using "system URLs" that look a lot like <tt><nowiki>http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50</nowiki></tt>, and your goal is to transform this into <tt><nowiki>http://www.example.com/example-menu-item/20/1</nowiki></tt>. As the developer, you have two tasks: signalling the system that certain pieces of text are URLs and need to be transformed, and explaining the system how to transform URLs.
  
A good example of this is the "Welcome to Joomla" article. The first link shown was generated without mod_rewrite, and the second link was generated with mod_rewrite:
+
=== Applying <code>JRoute::_</code> ===
  
* <code><nowiki>http://www.example.com/index.php/the-­news/1-­latest­-news/-welcome­-to­-joomla</nowiki></code>
+
It is difficult and inefficient for Joomla! to figure out which parts of your component's output are URLs. To support SEF URLs, you will need to change URL-generating code so that it applies <code>JRoute::_</code> before outputting the URL:
* <code><nowiki>http://www.example.com/the-­news/-latest-­news/1-­welcome-­to­-joomla</nowiki></code>
+
 
 +
<source lang="php">
 +
echo JRoute::_('index.php?view=article&id=1&catid=20');
 +
</source>
 +
 
 +
Notice that it is possible to leave out the parameters <code>option</code> and <code>Itemid</code>. <code>option</code> defaults to the name of the component currently being executed, and <code>Itemid</code> defaults to the current menu item's ID.
 +
 
 +
In general, you should only apply this to URLs that users and/or search engines are able to see. For example, there is no need to transform URLs used in redirects that immediately result in other redirects.
 +
 
 +
If the user turns off SEF URLs in the site's settings, <code>JRoute::_</code> will produce working non-SEF URLs without any changes to the code.
 +
 
 +
=== Writing a router ===
 +
 
 +
You'll also need to write a router, which is a single file with two functions that convert system URLs to and from SEF URLs. This file needs to be placed at <tt>/components/com_yourcomponent/router.php</tt>.
 +
 
 +
The first function, <code>[componentname]BuildRoute(&$query)</code>, must transform an array of URL parameters into an array of segments that will form the SEF URL. Schematically, the transformation works as follows:
 +
 
 +
:<tt><nowiki>http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50</nowiki></tt>
 +
 
 +
::<span style="font-size: xx-large">&darr;</span> <code>JRoute::_</code>, called by your component or any other extension
 +
 
 +
:<code>$query = array('view' => 'article', 'id' => 1, 'catid' => 20)</code>
 +
 
 +
::<span style="font-size: xx-large">&darr;</span> Your router's <code>com_yourcomponentBuildRoute</code>
 +
 
 +
:<code>$segments = array(20, 1);</code>
 +
 
 +
::<span style="font-size: xx-large">&darr;</span> Joomla's internal route building (for display)
 +
 
 +
:<tt><nowiki>http://www.example.com/example-menu-item/20/1</nowiki></tt>
 +
 
 +
The second function, <code>[componentname]ParseRoute($segments)</code>, must transform an array of segments back into an array of URL parameters. Schematically:
 +
 
 +
:<tt><nowiki>http://www.example.com/example-menu-item/20/1</nowiki></tt>
 +
 
 +
::<span style="font-size: xx-large">&darr;</span> Joomla's internal route parsing
 +
 
 +
:<code>$segments = array(20, 1);</code>
 +
 
 +
::<span style="font-size: xx-large">&darr;</span> Your router's <code>com_yourcomponentParseRoute</code>
 +
 
 +
:<code>$query = array('view' => 'article', 'id' => 1, 'catid' => 20)</code>
 +
 
 +
The two functions must cooperate in such a way that the original URL can be reconstructed. You can think of <code>BuildRoute</code> as a form of [[wikipedia:Encoding|encoding]] and <code>ParseRoute</code> as the corresponding decoding. When the original URL isn't properly reproduced, your component will stop working.
  
 
== Preparing Your Data for Routing ==
 
== Preparing Your Data for Routing ==
Line 16: Line 59:
 
=== The Alias ===
 
=== The Alias ===
  
The first step is the generation of the so called alias. The alias is used in the URL instead of the title (the title is the text you want to have in the url). The alias has to be URI safe, which means accented UTF­8 characters are replaced by their ASCII­7 equivalents, white spaces by hyphens, etc.
+
The first step is the generation of the so called alias. The alias is used in the URL instead of the title (the alias is the text you want to have in the URL). The alias has to be URI safe, which means accented UTF­8 characters are replaced by their ASCII­7 equivalents, white spaces by hyphens, etc.
  
 
The alias can be defined by the user, but you should ensure that the above requirements for a URL safe alias are met. A good way to do so is to use the JTable::check() method during the save process. Have a look at this example code:
 
The alias can be defined by the user, but you should ensure that the above requirements for a URL safe alias are met. A good way to do so is to use the JTable::check() method during the save process. Have a look at this example code:
  
<pre>function check()
+
<source lang="php">
 +
 
 +
function check()
 
{
 
{
      jimport( 'joomla.filter.output' );
+
    jimport( 'joomla.filter.output' );
      $alias = JOutputFilter::stringURLSafe( $this-­>title );
+
    if(empty($this->alias)) {
      if (empty( $this-­>alias ) || $this-­>alias === $alias ) {
+
    $this->alias = $this->title;
              $this->alias = $alias;
+
    }
      }
+
    $this->alias = JFilterOutput::stringURLSafe($this->alias);
      /* All your other checks */
+
 
      return true;
+
    /* All your other checks */
}</pre>
+
    return true;
 +
}
 +
</source>
  
If the alias field is empty or has a different type than the URL safe title then this one will be used as the alias.
+
If the alias field is empty the title will be used as alias. Then the alias will be made URLSafe using the <code>JFilterOutput::stringURLSafe()</code> method.
  
 
=== The Slug ===
 
=== The Slug ===
  
Continuing with the same example, the "slug" - "1­-welcome­-to­-joomla" has two parts. The first part is the article identifier (id) and the second is the alias. They are separated by a hyphen. These two elements were combined during the database query in the model:
+
Continuing with the same example, the "slug" - "1­:welcome­-to­-joomla" has two parts. The first part is the article identifier (id) and the second is the alias. They are separated by a colon. These two elements were combined during the database query in the model:
  
<pre>$query = 'SELECT a.*'.
+
<source lang="php">$query = 'SELECT a.*, '.
        ' CASE WHEN CHAR_LENGTH (a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug,'.
+
        'CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(":", a.id, a.alias) ELSE a.id END as slug,'
        [...];</pre>
+
        /*...*/;</source>
  
 
After this step the slug is used instead of the id.
 
After this step the slug is used instead of the id.
  
== Routing URL's ==
+
== Routing URLs ==
  
The <code>JRoute::_</code> method translates the internal Joomla! URL to a custom URL. <code>JRoute</code> has three parameters and its prototype is:
+
The <code>JRoute::_</code> method translates the internal Joomla! URL to a custom URL. <tt>JRoute</tt> has three parameters and its prototype is:
  
<pre>JRoute::_( $url, $xhtml = true, $ssl=0 );</pre>
+
<source lang="php">JRoute::_( $url, $xhtml = true, $ssl=null );</source>
  
 
Where:
 
Where:
Line 57: Line 104:
 
The most important parameter is <code>$url</code>. A call to this method might look like:
 
The most important parameter is <code>$url</code>. A call to this method might look like:
  
<pre>JRoute::_( 'index.php?view=article&id='.$row->slug );</pre>
+
<source lang="php">JRoute::_( 'index.php?view=article&id='.$row->slug );</source>
  
 
<code>$row-­>slug</code> is the value that was generated in step 2 from a combination of id and title alias.
 
<code>$row-­>slug</code> is the value that was generated in step 2 from a combination of id and title alias.
Line 68: Line 115:
  
 
* Create the application route. The application route is fully handled by JRouter and the component developer doesn’t have to do anything to make it work.
 
* Create the application route. The application route is fully handled by JRouter and the component developer doesn’t have to do anything to make it work.
* Create the component route. To create the component route, JRouter looks for the router.php in the component directory which is responsible for building the route for the component.
+
* Create the component route. To create the component route, JRouter looks for the <tt>router.php</tt> in the component directory which is responsible for building the route for the component.
  
 
== The Component Router ==
 
== The Component Router ==
  
We will have two functions In the router.php. One is responsible for building the URL and the other is responsible for parsing it. In the next examples, a very basic and a more advanced one, we assume that we have three views that links can point to. The first is a categories overview (view=categories), the second is a single category (view=category) and the third is a single article (view=article).
+
We will have two functions in the <tt>router.php</tt>. One is responsible for building the URL and the other is responsible for parsing it. In the next examples, a very basic and a more advanced one, we assume that we have three views that links can point to. The first is a categories overview (<code>view=categories</code>), the second is a single category (<code>view=category</code>) and the third is a single article (<code>view=article</code>).
 +
 
 +
The file <tt>router.php</tt> should be in the site area of your component. It is not used on admin/backend pages. Don't forget to add it to your XML [[Manifest files|manifest file]] in the site folder.
  
 
=== A Simple Example ===
 
=== A Simple Example ===
Line 78: Line 127:
 
This simple example will illustrate the basics of implementing a router for your component.
 
This simple example will illustrate the basics of implementing a router for your component.
  
<pre>function [''Componentname'']BuildRoute( &$query )
+
<source lang="php">
 +
function [componentname]BuildRoute( &$query )
 
{
 
{
 
       $segments = array();
 
       $segments = array();
Line 92: Line 142:
 
       };
 
       };
 
       return $segments;
 
       return $segments;
}</pre>
+
}
 +
 
 +
</source>
 +
 
 +
<code>JRouter</code> passes a $query array to the <code>[''componentname'']BuildRoute</code> function. This function will add the relevant parts of the array to the $segments array in the right order and will return the properly ordered array. The content of the <code>$query</code> array needs to be unset, otherwise <code>JRouter</code> will add it to the URL in the form of a query string (i.e. any variables that are not handled by the router will be passed in the query string).
  
<code>JRouter</code> passes a $query array to the <code>[''Componentname'']BuildRoute</code> function. This function will add the relevant parts of the array to the $segments array in the right order and will return the properly ordered array. The content of the <code>$query</code> array needs to be unset, otherwise <code>JRouter</code> will add it to the URL in the form of a query string (i.e. any variables that are not handled by the router will be passed in the query string).
+
The prefix ''componentname'' is the name for your component, as found in the directory holding the component's files. For instance, a component "Magic" in directory <tt>/components/com_magic/...</tt> would use a prefix <code>magic</code> (all lower case).
  
The next function in the <code>router.php</code> parses the URL:
+
The next function in the <tt>router.php</tt> parses the URL:
  
<pre>function [''Componentname'']ParseRoute( $segments )
+
<source lang="php">
 +
function [componentname]ParseRoute( $segments )
 
{
 
{
 
       $vars = array();
 
       $vars = array();
Line 118: Line 173:
 
       }
 
       }
 
       return $vars;
 
       return $vars;
}</pre>
+
}
 +
</source>
  
What happens here? In the function <code>[''Componentname'']BuildRoute</code> we arranged the items in the <code>$query</code> array in a specific sequence. This means that in this example the view is first, the catid is second and the id is third in the array.
+
What happens here? In the function <code>[''componentname'']BuildRoute</code> we arranged the items in the <code>$query</code> array in a specific sequence. This means that in this example the view is first and the id is second in the array.
  
 
By reading <code>$segments[0]</code>, we access the name of the view. We set the right view and/or identifier depending on its value and we return the <code>$vars</code> array to <code>JRouter</code>. $vars should be an associative array similar to the array that was passed to the BuildRoute method.
 
By reading <code>$segments[0]</code>, we access the name of the view. We set the right view and/or identifier depending on its value and we return the <code>$vars</code> array to <code>JRouter</code>. $vars should be an associative array similar to the array that was passed to the BuildRoute method.
  
The above example of the <code>router.php</code> is a very simple way to generate sef URL's but should show how this works quite clearly.
+
The above example of the <tt>router.php</tt> is a very simple way to generate SEF URLs but should show how this works quite clearly.
  
 
The generated URL in this example contains the name of the view and doesn't reflect the content hierarchy:
 
The generated URL in this example contains the name of the view and doesn't reflect the content hierarchy:
  
<code><nowiki>http://www.example.com/[menualias]/[view]/[slug]</nowiki></code>
+
<tt><nowiki>http://www.example.com/[menualias]/[view]/[slug]</nowiki></tt>
  
 
=== A More Advanced Example ===
 
=== A More Advanced Example ===
Line 136: Line 192:
 
The goal is URL's that look like:
 
The goal is URL's that look like:
  
* When viewing an article: <code><nowiki>http://www.example.com/[menualias]/[category]/[article]</nowiki></code>
+
* When viewing an article: <tt><nowiki>http://www.example.com/[menualias]/[category]/[article]</nowiki></tt>
* When viewing a category: <code><nowiki>http://www.example.com/[menualias]/[category]</nowiki></code>
+
* When viewing a category: <tt><nowiki>http://www.example.com/[menualias]/[category]</nowiki></tt>
* When viewing the categories overview: <code><nowiki>http://www.example.com/[menualias]</nowiki></code>
+
* When viewing the categories overview: <tt><nowiki>http://www.example.com/[menualias]</nowiki></tt>
  
 
Let's assume we have done step 1 and 2 also for the category.
 
Let's assume we have done step 1 and 2 also for the category.
Line 144: Line 200:
 
The link to the article would look like this:
 
The link to the article would look like this:
  
<pre>JRoute::_( 'index.php?view=article&catid='.$row-­>catslug .'&id='.$row-­>slug );</pre>
+
<source lang="php">
 +
JRoute::_( 'index.php?view=article&catid='.$row-­>catslug .'&id='.$row-­>slug );
 +
</source>
  
 
And the Link to the category would look like this:
 
And the Link to the category would look like this:
  
<pre>JRoute::_( 'index.php?view=category&id='.$row->catslug );</pre>
+
<source lang="php">
 +
JRoute::_( 'index.php?view=category&id='.$row->catslug );
 +
</source>
  
The corresponding <code>router.php</code>:
+
The corresponding <tt>router.php</tt>:
  
<pre>function [''Componentname'']BuildRoute(&$query)
+
<source lang="php">
 +
 
 +
function [''Componentname'']BuildRoute(&$query)
 
{
 
{
 
       $segments = array();
 
       $segments = array();
Line 167: Line 229:
 
       unset( $query['view'] );
 
       unset( $query['view'] );
 
       return $segments;
 
       return $segments;
}</pre>
+
}
 +
</source>
  
 
The difference now is that we don’t add the name of the view to the <code>$segments</code> array. We still unset the view key since otherwise, <code>JRouter</code> would add it to the URL as part of the query string. Another new thing here is the additional parameter catid that we push into the <code>$segments</code> array.
 
The difference now is that we don’t add the name of the view to the <code>$segments</code> array. We still unset the view key since otherwise, <code>JRouter</code> would add it to the URL as part of the query string. Another new thing here is the additional parameter catid that we push into the <code>$segments</code> array.
  
<pre>function [''Componentname'']ParseRoute($segments)
+
<source lang="php">
 +
function [''Componentname'']ParseRoute($segments)
 
{
 
{
 
       $vars = array();
 
       $vars = array();
       $menu =& JMenu::getInstance();
+
       $app =& JFactory::getApplication();
 +
      $menu =& $app->getMenu();
 
       $item =& $menu->getActive();
 
       $item =& $menu->getActive();
 
       // Count segments
 
       // Count segments
 
       $count = count( $segments );
 
       $count = count( $segments );
 
       //Handle View and Identifier
 
       //Handle View and Identifier
       switch( $item-­>query['view'] )
+
       switch( $item->query['view'] )
 
       {
 
       {
 
               case 'categories':
 
               case 'categories':
Line 188: Line 253:
 
                               $vars['view'] = 'article';
 
                               $vars['view'] = 'article';
 
                       }
 
                       }
                       $id = explode( ':', $segments[$count­1] );
+
                       $id = explode( ':', $segments[$count-1] );
 
                       $vars['id'] = (int) $id[0];
 
                       $vars['id'] = (int) $id[0];
 
                       break;
 
                       break;
 
               case 'category':
 
               case 'category':
                       $id  = explode( ':', $segments[$count­1] );
+
                       $id  = explode( ':', $segments[$count-1] );
 
                       $vars['id']  = (int) $id[0];
 
                       $vars['id']  = (int) $id[0];
 
                       $vars['view'] = 'article';
 
                       $vars['view'] = 'article';
Line 198: Line 263:
 
       }
 
       }
 
       return $vars;
 
       return $vars;
}</pre>
+
}
 +
</source>
  
 
You can see that this ParseRoute function has a lot of different code parts in comparison to the previous. The reason for this is simple. We don’t have the name of the view in the <code>$segments</code> array and we need to find another way to determine it.
 
You can see that this ParseRoute function has a lot of different code parts in comparison to the previous. The reason for this is simple. We don’t have the name of the view in the <code>$segments</code> array and we need to find another way to determine it.
Line 204: Line 270:
 
We need to find out which level of hierarchy we are in by receiving the root element. We do this by looking to the view name of the active menu item:
 
We need to find out which level of hierarchy we are in by receiving the root element. We do this by looking to the view name of the active menu item:
  
<pre>$item-­>query['view']</pre>
+
<source lang="php">
 +
$item-­>query['view']
 +
</source>
  
 
Also we need to know the number of items in the <code>$segments</code> array:
 
Also we need to know the number of items in the <code>$segments</code> array:
  
<pre>$count = count( $segments );</pre>
+
<source lang="php">
 +
$count = count( $segments );
 +
</source>
  
 
With this information we can correctly set the view for all possible three cases:
 
With this information we can correctly set the view for all possible three cases:
  
* The menu item is a link to the categories view and the <code>$segments</code> array has two items (<code>$catid</code> and <code>$id</code>). In this case we know that we need to parse a link to an article .
+
* The menu item is a link to the categories view and the <code>$segments</code> array has two items (<code>$catid</code> and <code>$id</code>). In this case we know that we need to parse a link to an article.
* The menu item is a link to the categories view and the $segments array has one item ($id). In this case we know that we need to parse a link to a category.
+
* The menu item is a link to the categories view and the <code>$segments</code> array has one item (<code>$id</code>). In this case we know that we need to parse a link to a category.
* The menu item is a link to a category. In this case, we know that any item in the $segments array is the identifier for an article .
+
* The menu item is a link to a category. In this case, we know that any item in the <code>$segments</code> array is the identifier for an article.
  
The result of all this code is nice and human readable component URL's.
+
The result of all this code is clean and human-readable component URLs.
  
 +
== Routers and Menu Items ==
  
== Application Route Parsing ==
+
A last important part of creating a router is considering what to do with menu items. As explained on [[Search Engine Friendly URLs]], the output of the component router is used ''after'' the first segment of a route, the first segment being the menu item's alias. This creates a difficult question: how is your router and/or other code to know which menu item to route through?
  
The [[API Execution Order]] outlines that the route (URL) is parsed immediately after initialisation is complete.  Since fancy URL's are not treated (yet) in the Administrator, we will follow the route parsing process in detail when <code>JSite::route</code> in the <code>index.php</code> file.
+
Suppose, for example, that your component is currently producing output for the page <tt>/dogs</tt>, which lists all dogs in the system. Of course, the items in the list need to be links to pages that display more details about one dog. What should the URL to the dog with ID 21 and name Fido become? Using a router that works according to the principles we've seen so far, the route that is produced is <tt>dogs/21-fido</tt>, or with some additional work <tt>/dogs/fido</tt>. But perhaps the user has created a menu item with the alias <tt>mydoggy</tt> which displays exactly that dog's details. Then it is probably the user's intention to route this URL through that menu item, and the item in the list should link to the page <tt>/mydoggy</tt>.
  
* Call to <code>JApplication::route</code>
+
More generally, whenever you are building a route, you will need to find the menu item that is most suitable as a ''starting point'' for building your route. The term ''starting point'' is emphasized because the rest of the route depends on the configuration of the menu item. In our example above, <tt>/dogs/21-fido</tt> is an acceptable route, <tt>/mydoggy</tt> is arguably even better, but <tt>/mydoggy/21-fido</tt> is simply wrong, since <tt>/mydoggy</tt> is in itself a menu item that is set up to display <tt>fido</tt>'s information.
** Clone the URI
+
** Call to <code>JApplication::getRouter</code>
+
*** Call to <code>JRouter::getInstance</code> passing the type ("site")
+
** Call to <code>JRouterSite::parse</code> passing the URI
+
*** Strip the suffix if applicable (added to $vars['format'])
+
*** Re-set the route (URI)
+
*** Call to <code>JRouter::parse</code> passing the URI
+
**** Call to <code>JRouterSite::_processParseRules</code> passing the URI (this will call custom route rules)
+
***** Call to <code>JRouter::_processParseRules</code> passing the URI
+
****** Call any custom routing rules (probably added via a system plugin using the <code>onAfterInitialise</code> event trigger) passing the URI
+
****** Returns an array of vars
+
***** If SEF mode, replace <code>start</code> variable with </code>limitstart</code>
+
**** If raw mode, call to <code>JRouterSite::_parseRawRoute</code> passing the URI
+
**** If SEF mode, call to <code>JRouterSite::_parseSefRoute</code> passing the URI
+
***** If the route (the URI path) is empty, load it from the default menu item; set the active menu item as the default
+
***** If first part is <code>/component/com_content</code>, set the <code>option</code> as the second segement.  Null the <code>Itemid</code>.
+
***** Else, loop through menu alias values and take off segments that match as the menu tree is traversed.  Set <code>option</code> and <code>Itemid</code> based on the last menu item found.
+
***** If the <code>Itemid</code> is set in the URL, set the active menu item based on this value.
+
***** Push the vars collected so far (eg, <code>option</code>, <code>Itemid</code>, etc) into the router object (<code>$this</code>).
+
***** If the route and <code>option</code> is set, load the component router;
+
***** Else, get the active menu item and get the route vars from it
+
  
 +
Several approaches are available to tackle this problem. Joomla!'s core components take a mixed approach, separating responsibilities in two units of code: the router itself and the so-called <code>[componentname]RouteHelper</code>. The <code>[componentname]RouteHelper</code> provides methods that find the most suitable menu item for a given piece of data to be displayed, while the router analyzes the menu item and puts any information that is not determined by the menu item's configuration into the route. This does mean that the ''calling code'' must explicitly call the helper's method before routing (<code>echo JRoute::_(DogsRouteHelper::getDogRoute(21));</code>).
  
== Application Route Building ==
+
== See Also ==
  
TODO
+
There is a useful thread on this subject here: [[jtopic:148632]] (note, may be out of date)
  
== Custom Router Rules ==
+
For details on the internals of routing, see [[Routing implementation details]].
 
+
<noinclude>[[Category:Tutorials]][[Category:Component Development]][[Category:Search Engine Friendly URLs]]</noinclude>
TODO
+
 
+
== Additional References ==
+
 
+
There is a useful thread on this subject here: [[jtopic:148632]] (note, may be out of date)
+

Revision as of 04:40, 29 April 2013


<translate> Search engine friendly (SEF), human-readable or clean URLs are URLs that make sense to both humans and search engines because they explain the path to the particular page they point to. Since version 1.5, Joomla! is capable of creating and parsing URLs in any format, including SEF URLs. This does not depend on URL rewriting executed by the web server, so it works even if Joomla! runs a server other than Apache with the mod_rewrite module. The SEF URLs follow a certain fixed pattern, but the user can define a short descriptive text (alias) for each segment of the URL.

Internally, the local part of a SEF URL (the part after the domain name) is called a route. Creating and processing SEF URLs is therefore referred to as routing, and the relevant code is called a router. </translate>

In Joomla!, each component is responsible for handling its own SEF URLs. Therefore, as the developer of a component, you will have to create your own router to allow your component to use SEF URLs.

Contents

The Concept

Assuming you are following standard development practices, your component is probably using "system URLs" that look a lot like http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, and your goal is to transform this into http://www.example.com/example-menu-item/20/1. As the developer, you have two tasks: signalling the system that certain pieces of text are URLs and need to be transformed, and explaining the system how to transform URLs.

Applying JRoute::_

It is difficult and inefficient for Joomla! to figure out which parts of your component's output are URLs. To support SEF URLs, you will need to change URL-generating code so that it applies JRoute::_ before outputting the URL:

echo JRoute::_('index.php?view=article&id=1&catid=20');

Notice that it is possible to leave out the parameters option and Itemid. option defaults to the name of the component currently being executed, and Itemid defaults to the current menu item's ID.

In general, you should only apply this to URLs that users and/or search engines are able to see. For example, there is no need to transform URLs used in redirects that immediately result in other redirects.

If the user turns off SEF URLs in the site's settings, JRoute::_ will produce working non-SEF URLs without any changes to the code.

Writing a router

You'll also need to write a router, which is a single file with two functions that convert system URLs to and from SEF URLs. This file needs to be placed at /components/com_yourcomponent/router.php.

The first function, [componentname]BuildRoute(&$query), must transform an array of URL parameters into an array of segments that will form the SEF URL. Schematically, the transformation works as follows:

http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50
JRoute::_, called by your component or any other extension
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
Your router's com_yourcomponentBuildRoute
$segments = array(20, 1);
Joomla's internal route building (for display)
http://www.example.com/example-menu-item/20/1

The second function, [componentname]ParseRoute($segments), must transform an array of segments back into an array of URL parameters. Schematically:

http://www.example.com/example-menu-item/20/1
Joomla's internal route parsing
$segments = array(20, 1);
Your router's com_yourcomponentParseRoute
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)

The two functions must cooperate in such a way that the original URL can be reconstructed. You can think of BuildRoute as a form of encoding and ParseRoute as the corresponding decoding. When the original URL isn't properly reproduced, your component will stop working.

Preparing Your Data for Routing

The Alias

The first step is the generation of the so called alias. The alias is used in the URL instead of the title (the alias is the text you want to have in the URL). The alias has to be URI safe, which means accented UTF­8 characters are replaced by their ASCII­7 equivalents, white spaces by hyphens, etc.

The alias can be defined by the user, but you should ensure that the above requirements for a URL safe alias are met. A good way to do so is to use the JTable::check() method during the save process. Have a look at this example code:

function check()
{
    jimport( 'joomla.filter.output' );
    if(empty($this->alias)) {
            $this->alias = $this->title;
    }
    $this->alias = JFilterOutput::stringURLSafe($this->alias);
 
    /* All your other checks */
    return true;
}

If the alias field is empty the title will be used as alias. Then the alias will be made URLSafe using the JFilterOutput::stringURLSafe() method.

The Slug

Continuing with the same example, the "slug" - "1­:welcome­-to­-joomla" has two parts. The first part is the article identifier (id) and the second is the alias. They are separated by a colon. These two elements were combined during the database query in the model:

$query = 'SELECT a.*, '.
         'CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(":", a.id, a.alias) ELSE a.id END as slug,'
         /*...*/;

After this step the slug is used instead of the id.

Routing URLs

The JRoute::_ method translates the internal Joomla! URL to a custom URL. JRoute has three parameters and its prototype is:

JRoute::_( $url, $xhtml = true, $ssl=null );

Where:

  • $url is a string containing the absolute or relative internal Joomla! URL.
  • $xhtml is a boolean value that specifies whether or not the output should be in XHTML. This parameter is optional and if omitted defaults to true.
  • $ssl is an integer value that specifies whether the URI should be secure. It should be set to 1 to force the URI to be secure using the global secure site URI, 0 to leave it in the same state as when it was passed, and -1 to force the URI to be unsecure using the global unsecure site URI.

The most important parameter is $url. A call to this method might look like:

JRoute::_( 'index.php?view=article&id='.$row->slug );

$row-­>slug is the value that was generated in step 2 from a combination of id and title alias.

Another advantage of using JRoute is that the router now handles $option (the component name) and the $Itemid (the menu item ID). The component itself doesn’t have to know its name ($option) or the active menu item ($Itemid) like it did in previous version of Joomla!.

It is important that you think about the sequence of the URL parameter in this stage. This will be more clear when we have a deeper look at the router.php in the next section.

The building process of JRouter is divided into two steps:

  • Create the application route. The application route is fully handled by JRouter and the component developer doesn’t have to do anything to make it work.
  • Create the component route. To create the component route, JRouter looks for the router.php in the component directory which is responsible for building the route for the component.

The Component Router

We will have two functions in the router.php. One is responsible for building the URL and the other is responsible for parsing it. In the next examples, a very basic and a more advanced one, we assume that we have three views that links can point to. The first is a categories overview (view=categories), the second is a single category (view=category) and the third is a single article (view=article).

The file router.php should be in the site area of your component. It is not used on admin/backend pages. Don't forget to add it to your XML manifest file in the site folder.

A Simple Example

This simple example will illustrate the basics of implementing a router for your component.

function [componentname]BuildRoute( &$query )
{
       $segments = array();
       if(isset($query['view']))
       {
                $segments[] = $query['view'];
                unset( $query['view'] );
       }
       if(isset($query['id']))
       {
                $segments[] = $query['id'];
                unset( $query['id'] );
       };
       return $segments;
}

JRouter passes a $query array to the [componentname]BuildRoute function. This function will add the relevant parts of the array to the $segments array in the right order and will return the properly ordered array. The content of the $query array needs to be unset, otherwise JRouter will add it to the URL in the form of a query string (i.e. any variables that are not handled by the router will be passed in the query string).

The prefix componentname is the name for your component, as found in the directory holding the component's files. For instance, a component "Magic" in directory /components/com_magic/... would use a prefix magic (all lower case).

The next function in the router.php parses the URL:

function [componentname]ParseRoute( $segments )
{
       $vars = array();
       switch($segments[0])
       {
               case 'categories':
                       $vars['view'] = 'categories';
                       break;
               case 'category':
                       $vars['view'] = 'category';
                       $id = explode( ':', $segments[1] );
                       $vars['id'] = (int) $id[0];
                       break;
               case 'article':
                       $vars['view'] = 'article';
                       $id = explode( ':', $segments[1] );
                       $vars['id'] = (int) $id[0];
                       break;
       }
       return $vars;
}

What happens here? In the function [componentname]BuildRoute we arranged the items in the $query array in a specific sequence. This means that in this example the view is first and the id is second in the array.

By reading $segments[0], we access the name of the view. We set the right view and/or identifier depending on its value and we return the $vars array to JRouter. $vars should be an associative array similar to the array that was passed to the BuildRoute method.

The above example of the router.php is a very simple way to generate SEF URLs but should show how this works quite clearly.

The generated URL in this example contains the name of the view and doesn't reflect the content hierarchy:

http://www.example.com/[menualias]/[view]/[slug]

A More Advanced Example

In the next example we will try to get rid of the need for the view and we will try to reflect the current hierarchy level in the URL.

The goal is URL's that look like:

  • When viewing an article: http://www.example.com/[menualias]/[category]/[article]
  • When viewing a category: http://www.example.com/[menualias]/[category]
  • When viewing the categories overview: http://www.example.com/[menualias]

Let's assume we have done step 1 and 2 also for the category.

The link to the article would look like this:

JRoute::_( 'index.php?view=article&catid='.$row-­>catslug .'&id='.$row-­>slug );

And the Link to the category would look like this:

JRoute::_( 'index.php?view=category&id='.$row->catslug );

The corresponding router.php:

function [''Componentname'']BuildRoute(&$query)
{
       $segments = array();
       if(isset( $query['catid'] ))
       {
                $segments[] = $query['catid'];
                unset( $query['catid'] );
       };
       if( isset($query['id']) )
       {
                $segments[] = $query['id'];
                unset( $query['id'] );
       };
       unset( $query['view'] );
       return $segments;
}

The difference now is that we don’t add the name of the view to the $segments array. We still unset the view key since otherwise, JRouter would add it to the URL as part of the query string. Another new thing here is the additional parameter catid that we push into the $segments array.

function [''Componentname'']ParseRoute($segments)
{
       $vars = array();
       $app =& JFactory::getApplication();
       $menu =& $app->getMenu();
       $item =& $menu->getActive();
       // Count segments
       $count = count( $segments );
       //Handle View and Identifier
       switch( $item->query['view'] )
       {
               case 'categories':
                       if($count == 1) {
                               $vars['view'] = 'category';
                       }
                       if($count == 2) {
                               $vars['view'] = 'article';
                       }
                       $id = explode( ':', $segments[$count-1] );
                       $vars['id'] = (int) $id[0];
                       break;
               case 'category':
                       $id   = explode( ':', $segments[$count-1] );
                       $vars['id']   = (int) $id[0];
                       $vars['view'] = 'article';
                       break;
       }
       return $vars;
}

You can see that this ParseRoute function has a lot of different code parts in comparison to the previous. The reason for this is simple. We don’t have the name of the view in the $segments array and we need to find another way to determine it.

We need to find out which level of hierarchy we are in by receiving the root element. We do this by looking to the view name of the active menu item:

$item-­>query['view']

Also we need to know the number of items in the $segments array:

$count = count( $segments );

With this information we can correctly set the view for all possible three cases:

  • The menu item is a link to the categories view and the $segments array has two items ($catid and $id). In this case we know that we need to parse a link to an article.
  • The menu item is a link to the categories view and the $segments array has one item ($id). In this case we know that we need to parse a link to a category.
  • The menu item is a link to a category. In this case, we know that any item in the $segments array is the identifier for an article.

The result of all this code is clean and human-readable component URLs.

Routers and Menu Items

A last important part of creating a router is considering what to do with menu items. As explained on Search Engine Friendly URLs, the output of the component router is used after the first segment of a route, the first segment being the menu item's alias. This creates a difficult question: how is your router and/or other code to know which menu item to route through?

Suppose, for example, that your component is currently producing output for the page /dogs, which lists all dogs in the system. Of course, the items in the list need to be links to pages that display more details about one dog. What should the URL to the dog with ID 21 and name Fido become? Using a router that works according to the principles we've seen so far, the route that is produced is dogs/21-fido, or with some additional work /dogs/fido. But perhaps the user has created a menu item with the alias mydoggy which displays exactly that dog's details. Then it is probably the user's intention to route this URL through that menu item, and the item in the list should link to the page /mydoggy.

More generally, whenever you are building a route, you will need to find the menu item that is most suitable as a starting point for building your route. The term starting point is emphasized because the rest of the route depends on the configuration of the menu item. In our example above, /dogs/21-fido is an acceptable route, /mydoggy is arguably even better, but /mydoggy/21-fido is simply wrong, since /mydoggy is in itself a menu item that is set up to display fido's information.

Several approaches are available to tackle this problem. Joomla!'s core components take a mixed approach, separating responsibilities in two units of code: the router itself and the so-called [componentname]RouteHelper. The [componentname]RouteHelper provides methods that find the most suitable menu item for a given piece of data to be displayed, while the router analyzes the menu item and puts any information that is not determined by the menu item's configuration into the route. This does mean that the calling code must explicitly call the helper's method before routing (echo JRoute::_(DogsRouteHelper::getDogRoute(21));).

See Also

There is a useful thread on this subject here: jtopic:148632 (note, may be out of date)

For details on the internals of routing, see Routing implementation details.