This article deals only with routes defined in config/routes.php
file - also called URL rules. It is also written with the assumption that you have basic understanding of Craft Cms routing functionality. You can learn more about routes in Craft Cms documentation.
What's it all about? #
One of the most common applications of routing functionality in Craft Cms is creating a tag page. Let's consider such situation.
First, we need to define route in config/routes.php
file:
<?php
return [
tag-list/<slug>' => ['template' => '_pages/single_tag'],`
];
Now, we need to create link to tag page inside Twig template. In example below we are looping trough tags assigned to some entry:
{% for singletag in entry.tags.all() %}
<a href="tag-list/{{singletag.slug}}">{{singletag.title}}</a>`
{% endfor %}
Do you already see the problem? tag-list
keyword is hardcoded inside routes.php
, but also in Twig file. What if we create ten such links within various fragments of our twig templates? And then we need to change tag-list
fragment of route to - for example, tags
? In such situation, we would need to change it in 11 places - one time inside routes.php
and ten times within Twig templates. This is in direct contradiction of DRY (dont repeat yourself) rule - route URL structure should be defined only in one place.
Accessing routes from within Twig templates #
Craft CMS exposes most of its core functionality within craft.app
Twig variable. Let's use dump
to inspect our routes, using craft.app.routes.getConfigFileRoutes
method:
<pre>{{ dump(craft.app.routes.getConfigFileRoutes) }}</pre>
array(1) {
["tag-list/<slug>"]=>
array(1) {
["template"]=>
string(28) "_pages/single_tag"
}
}
As we can see, route URL structure IS available for us inside Twig templates. But array returned by getConfigFileRoutes
is numeric - do we need to remember numeric index of each route? Let's modify routes.php
file a little, by giving each route name
attribute.
<?php
return [
'tag-list/<slug>' => ['template' => '_pages/single_tag', 'name' => 'tag'],
];
Now, we can loop through array of routes and find the one we need - route that has name
attribute with value of tag
.
{% set routeName == 'tag' %}
{% set keyword = 'name' %}
{% set routeString = null %}
{% for route, routeSettings in craft.app.routes.getConfigFileRoutes %}
{% if routeSettings[keyword] is defined and routeSettings[keyword] == routeName %}
{% set routeString = route %}
{% endif %}
{% endfor %}
Twig code above will assign url structure of route to variable - in our example this structure is tags/<slug>
. Now, we just need to replace <slug>
token with actual slug of specific tag we are linking to. In order to do that, we need to divide route structure into an array of segments using split
filter, use regular expression to find segment with token and replace it.
{% set token = singletag.slug %}
{% set routes = routeString|split('/') %}
{% set resultArray = [] %}
{% for value in routes %}
{% if value matches '/\\<[\\w]+\\>/' %}
{% set value = token %}
{% endif %}
{% set resultArray = resultArray|merge([value]) %}
{% endfor %}
{% set result = resultArray|join('/') %}
And there we have it - variable result
contains ready to use route url! We can use it inside href
attribute of link. For tag with slug 'super-blog-tag', variable result
will contain 'tag-list/super-blog-tag'.
Now we just have pass route url to url function - it will prepent it with base url of current site.
{{url(result)}}
Macro to the rescue #
Of course we don't need to paste all this code into template each time we want to use it. When dealing with repetitive fragments of Twig code, its always worth to encapsule it within macro.
{% macro url(routeName, tokens) %}
{% spaceless %}
{% set keyword = 'name' %}
{% set routeString = null %}
{% for route, routeSettings in craft.app.routes.getConfigFileRoutes %}
{% if routeSettings[keyword] is defined and routeSettings[keyword] == routeName %}
{% set routeString = route %}
{% endif %}
{% endfor %}
{% if routeString and tokens is not empty %}
{% set routes = routeString|split('/') %}
{% set tokenIndex = 0 %}
{% set resultArray = [] %}
{% for value in routes %}
{% if value matches '/\\<[\\w]+\\>/' %}
{% if tokens is iterable %}
{% set value = tokens[tokenIndex] %}
{% set tokenIndex = tokenIndex + 1 %}
{% else %}
{% set value = tokens %}
{% endif %}
{% endif %}
{% set resultArray = resultArray|merge([value]) %}
{% endfor %}
{% set result = resultArray|join('/') %}
{{url(result)}}
{% endif %}
{% endspaceless %}
{% endmacro %}
This macro accepts two parameters - routeName
and tokens
. routeName
is string identyfying our route within config/routes.php
. tokens
can be either string - when your route contains single token, or numeric array, if it contasins multiple tokens.
This is how such macro would be used in practice - assuming that singletag
object is avaible:
{% import 'macros' as m %}
<a href="{{m.url('tag', singletag.slug)}}">{{singletag.title}}</a>
Inspiration #
Those of you that worked with Django before, might have noticed this macro's similarity to Django url template tag. This is no coincidence - I wanted to replicate same functionality with Craft CMS.
I hope that some day Craft devs will add such functionality to Craft - and if you also would like that to happen, you can upvote THIS github issue.