J3.x:Supporting SEF URLs in your component
From Joomla! Documentation
Note many examples online use namespaced classes. This was introduced in Joomla 3.8. There are non-namespaced versions of all these classes which will work in Joomla 3.4+.
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.
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.
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/example-category/example-article. 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 Route::_
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 \Joomla\CMS\Router\Route::_
before outputting the URL:
echo \Joomla\CMS\Router\Route::_('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, \Joomla\CMS\Router\Route::_
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 containing a class with three functions that convert system URLs to and from SEF URLs. This file needs to be placed at /components/com_yourcomponent/router.php.
The class should be called [componentname]Router
(e.g. for com_content ContentRouter
), and must implement Joomla\CMS\Component\Router\RouterInterface
The first function, build(&$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
- ↓
\Joomla\CMS\Router\Route::_
, called by your component or any other extension
- ↓
$query = array('view' => 'article', 'id' => 1, 'catid' => 20)
- ↓ Your router's
[componentname]Router::build
- ↓ Your router's
$segments = array(20, 1);
- ↓ Joomla's internal route building (for display)
- http://www.example.com/example-menu-item/20/1
The second function, parse($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
[componentname]Router::parse
- ↓ Your router's
$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 build
as a form of encoding and parse
as the corresponding decoding. When the original URL isn't properly reproduced, your component will stop working.
The final function, preprocess($query)
, is a preparation method for URLs. This method is executed on each URL, regardless of SEF mode switched on or not. We will come back to this method later in the tutorial.
Preparing Your Data for Routing
Clearly, any URL format needs to contain some kind of information that identifies the data you want to show. If your system URLs look like http://www.example.com/index.php?option=com_yourcomponent&view=article&id=1&catid=20&Itemid=50, that information is currently the id URL parameter (id=1). You probably want your SEF URLs to contain a textual description of the data they point to. In Joomla!, this is usually done by giving your users a way to enter an alias to be used in the URL.
The Alias
Even if your users can enter an alias, they might not do so, leaving the generation of a sensible alias up to your component. If your data has a title field, you can use that as a candidate for the alias (like the core Content component does).
Considering that the alias will be used in URLs, it has to be URL safe. Joomla! provides a method making arbitrary strings URI safe, which includes replacing accented UTF8 characters by their ASCII7 equivalents, white spaces by hyphens, etc. Whether the user entered the alias or a candidate has been chosen automatically, you should ensure that the above requirements for a URL safe alias are met. A good place for implementing this, if you are using JTable
, is the JTable::check()
method, which is called 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 is used as alias. Then the alias is made URL safe using the JFilterOutput::stringURLSafe()
method.
The Slug
A slug is used to minimise the amount of code you need to support SEF URLs. It consists of the numerical identifier (id) your system URLs used, a colon (:), and the alias you created as described above.
Consider a SEF URL for an Article with id 1 and title "Welcome to Joomla!". The automatically generated alias for this article is welcome-to-joomla, and the slug becomes 1:welcome-to-joomla. In the Content component, the two elements are combined during the database query in the model (a
represents #__content
):
$query = 'SELECT a.*, '.
'CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(":", a.id, a.alias) ELSE a.id END as slug,'
/*...*/;
The advantage of this method of creating a slug is that you can simply use the slug as a drop-in replacement for the id in most places. For example, you don't need to check for and remove the colon and the alias from the request data manually: if you use JInput's int
(integer) filter, it will do that automatically.
Routing URLs
The \Joomla\CMS\Router\Route::_
method translates the internal Joomla! URL to a custom URL. \Joomla\CMS\Router\Route::_
has three parameters and its prototype is:
\Joomla\CMS\Router\Route::_($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:
\Joomla\CMS\Router\Route::_('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 \Joomla\CMS\Router\Route::_
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 \Joomla\CMS\Router\Router is divided into two steps:
- Create the application route. The application route is fully handled by \Joomla\CMS\Router\Router and the component developer doesn’t have to do anything to make it work.
- Create the component route. To create the component route, \Joomla\CMS\Router\Router looks for the router.php in the component directory which is responsible for building the route for the component.
Creating Frontend SEF URLs in the Administrator
From you can also create SEF URLs between any application (the most important use case of this will be creating URLs from the administrator/api to the frontend of Joomla). To achieve this a new method has been introduced. The \Joomla\CMS\Router\Route::link
is very similar to the \Joomla\CMS\Router\Route::_
discussed in the previous section. \Joomla\CMS\Router\Route::link
has four parameters and its prototype is:
\Joomla\CMS\Router\Route::link($client, $url, $xhtml = true, $ssl = null);
The $client parameter in this case is the client name of the application. So for the Joomla Frontend this will be site
. So an example you can run in the administrator section is
$app->enqueueMessage('3. Admin to Site: ' . JRoute::link('site', 'index.php?option=com_content&catid=1&view=article&id=1'));
to get the SEF URL of a Joomla article with ID 1.
The Component Router
We will have three functions in our class in 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 View-based Example
Often in Joomla's URL structure you will have routing structures based on view hierarchies. In the this example we will try to reflect the current hierarchy level in the URL from an existing menu item. 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]
The link to the article would look like this:
\Joomla\CMS\Router\Route::_('index.php?view=article&catid=' . $row->catslug . '&id='.$row->slug);
And the Link to the category would look like this:
\Joomla\CMS\Router\Route::_('index.php?view=category&id=' . $row->catslug);
Since (and implemented in the core components since as the "new component routers"), there is a new way of working on routers using the \Joomla\CMS\Component\Router\RouterView
base class. This handles routing by allowing you to register views into your system. So first of all let's build up our component's router constructor:
/**
* Magic Component router constructor
*
* @param CMSApplication $app The application object
* @param AbstractMenu $menu The menu object to work with
*/
public function __construct($app = null, $menu = null)
{
$category = new RouterViewConfiguration('category');
$category->setKey('id')->setNestable();
$this->registerView($category);
$article = new RouterViewConfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
}
So what have we done here? Well we've registered a category
view that has a routing key of it's id
, which can be nested (i.e. have multiple levels). We've also registered an article view which also has a routing key of id
, which has a parent of the category view.
Now we have registered our component's views into our router. The next step is to register the Joomla rules that use these rules. There are 3 rules provided by Joomla out of the box. The first \Joomla\CMS\Component\Router\Rules\MenuRules
looks to see if the URL matches a known menu item, and ensures in multilingual sites that a language tag is present. The second rule \Joomla\CMS\Component\Router\Rules\StandardRules
uses your view configuration to build up a menu path. Finally the third rule \Joomla\CMS\Component\Router\Rules\NomenuRules
provides a fallback when there is no good match found for building or parsing the URL. After applying these rules our finished router constructor looks like:
public function __construct(CMSApplication $app = null, AbstractMenu $menu = null)
{
$category = new RouterViewConfiguration('category');
$category->setKey('id')->setNestable();
$this->registerView($category);
$article = new RouterViewConfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
parent::__construct($app, $menu);
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
$this->attachRule(new NomenuRules($this));
}
The final piece of the puzzle Joomla needs is to convert the ids to and from their alias. So for each view registered you need to provide a getViewnameSegment($id, $query)
and a getViewnameId($segment, $query)
method, for building and parsing URLs. So for our example we need four functions: getCategorySegment
, getCategoryId
, getArticleSegment
and getArticleId
. You can see the implementation of these four functions at https://github.com/joomla/joomla-cms/blob/3.8.0/components/com_content/router.php . For articles we are going to directly validate our slugs in the database and for categories we will use Joomla's JCategories
class to validate the path to our category.
Building a complicated router in Joomla really is that simple!
A more complex Example
This more complicated example will illustrate the basics of implementing a more custom router for your component.
use Joomla\CMS\Component\Router\RouterBase;
class [componentname]Router extends RouterBase
{
public function build(&$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;
}
\Joomla\CMS\Router\Router
passes a $query
array to the [componentname]Router::build
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 \Joomla\CMS\Router\Router
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).
Note in the above we have chosen to extend \Joomla\CMS\Component\Router\RouterBase
as we do not need to do any preprocessing in this simple example.
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:
public function parse(&$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]Router::build
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 \Joomla\CMS\Router\Router
. $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]
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 \Joomla\CMS\Router\Route::_(DogsRouteHelper::getDogRoute(21));
).
SEF Plugin
The Joomla System SEF plugin will try it's best to handle any URLs that aren't being passed into \Joomla\CMS\Router\Route::_
in your application code. It hooks in on the onAfterRender system plugin event. In this function the body of the response that will be sent to the browser is retrieved using \Joomla\CMS\Application\CMSApplication::getBody
. The body of the response is then searched for links containing "/index.php..." and replaces them with a correct SEF url by calling \Joomla\CMS\Router\Route::_(url)
. Note that this is not the most reliable approach. You should use \Joomla\CMS\Router\Route::_
!
See Also
For details on the internals of routing, see Routing implementation details.