Vanity URLs in Craft CMS

One of our recent projects, built on Craft CMS, required support for top-level user profile URLs like twitter.com/miranj and instagram.com/_basestation (also commonly known as vanity URLs). While Craft offers a fair amount of flexibility for routing requests, implementing vanity URLs wasn’t particularly straightforward. This post is a run-through of how we worked our way around it.

To begin with, the routing precedence in Craft is as follows:

  1. All URIs beginning with the resourceTrigger config setting are treated as a Resource request
  2. All URIs beginning with the actionTrigger config setting (or POST parameter) are treated as an Action request
  3. Any direct URI matches with Entry or Category (or any other Element) object URIs are treated as an Entry/Category/Element request, wherein the corresponding Entry template is loaded with a pre-populated entry object.
  4. The first successful URI match against user declared regular expressions. Craft calls these dynamic routes, and the routes can be declared either via the Control Panel or in craft/config/routes.php.
  5. All URIs that match a file in the template folder(s) when interpreted as a template path.

Given this precedence, the ideal scenario for supporting a user profile page for each User object was to assign our desired URI to the User object. The routing would’ve automatically been handled at the third level (as an Entry/Category/Element request). However, the URI field appears to be unsupported for User objects and we couldn’t figured out a way to change or override that behaviour.

We were left then with the fourth level (dynamic routes). So we added a rule to match all top-level URIs and direct those requests to the user profile template page.

'(?P<username>[^/]+)$' => 'user/_profile',

Since this pattern is quite liberal and will match all top level URIs, we placed this as the last rule in our craft/config/routes.php file. The craft/templates/user/_profile.html template looked something like this:

With these two components in place, we started seeing the desired results. Requesting any valid user profile page like /batman loaded the craft/templates/user/_profile.html template for Bruce Wayne. So far, so good.

The problem we ran into here was that we had broken template path based routes (level 5) for top level URIs. For instance:

  • The template craft/templates/about/index.html should’ve been reachable via /about
  • The template craft/templates/contact.html should’ve been accessible via /contact

However both those URI requests resulted in a 404 response due to line number 6 of craft/templates/user/_profile.html. (Provided there were no users with about or contact as their username. (Always a good idea to maintain a username blacklist when dealing with vanity URLs.))

Now one option was to simply declare dynamic routes for 'about' => 'about/index' and 'contact' => 'contact' and place them before the user profile routing rule. This would’ve worked if we had only a handful of templates with top-level URIs, but it would not have scaled very well for a large number of template path routes.

So how did we get around this? By altering the logic to look for a template path match before looking for a username match. We introduced an intermediate template file craft/templates/_vanity_router.html to achieve this.

And we modified the dynamic route to hand off control to the intermediate template file:

Template paths now take higher precedence than vanity URLs, and that is exactly the behaviour we were going for.

Hope you find this useful for adding vanity URLs to your Craft project. If you’ve taken a different approach or have any feedback on this approach, we’d love to know.