# Vanity URLs in Craft CMS


- Date: 06-Jun-2016
- Author: Prateek Rungta
- Tags: Craft, Web Development
- URL: https://miranj.in/blog/2016/vanity-urls-in-craft-cms


One of our recent projects, built on [Craft CMS][craft], 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][1], implementing vanity URLs wasn’t particularly straightforward. This post is a run-through of how we worked our way around it.

[1]:https://craftcms.com/docs/routing
[craft]:https://craftcms.com

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_][dr], 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_][tp].

[dr]:https://craftcms.com/docs/routing#dynamic-routes
[tp]:https://craftcms.com/docs/templating-overview#template-paths

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.

    '<username:{slug}>' => [ 'template' => 'user/_profile' ],

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

<script src="https://gist.github.com/rungta/8a9e3dd48d77be20948751c05f1de0ea.js?file=_profile.twig"></script>
<noscript>
<pre><code>
{# Look for the user #}
{% set user = craft.users.username(username).one() %}

{# Abort if no user was found with that username #}
{% if not user %}
  {% exit 404 %}
{% endif %}

{# Render the user profile page #}
{# ... #}
</code></pre>
</noscript>

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

[batman]:https://en.wikipedia.org/wiki/Bruce_Wayne

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

- The `templates/about/index.twig` template should've been reachable via `/about`
- The `templates/contact.twig` template should've been accessible via `/contact`

However both those URI requests resulted in a 404 response due to line number 6 of `templates/user/_profile.twig`. (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][2].))
[2]:https://www.quora.com/How-do-sites-prevent-vanity-URLs-from-colliding-with-future-features

Now one option was to simply declare dynamic routes for `'about' => [ 'template' => 'about/index' ]` and `'contact' => [ 'template' => '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 `templates/_vanity_router.twig` to achieve this.

<script src="https://gist.github.com/rungta/8a9e3dd48d77be20948751c05f1de0ea.js?file=+_vanity_router.twig"></script>
<noscript>
<pre><code>
{# Treat the slug as a template path #}
{% set template = include(top_level_slug, ignore_missing = true) %}

{# Render the template contents if found, otherwise treat the slug as a username #}
{% if template is not empty %}
  {{- template|raw -}}
{% else %}
  {%- include 'users/_profile' with { username: top_level_slug } -%}
{% endif %}
</code></pre>
</noscript>

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

<script src="https://gist.github.com/rungta/8a9e3dd48d77be20948751c05f1de0ea.js?file=routes.php"></script>
<noscript>
<pre><code>
<?php
/**
 * Dynamic Site Routes
 */
return [
  
  // Existing route declarations
  // ...
  
  // Last rule because it matches all top-level URIs
  '<top_level_slug:{slug}>' => [ 'template' => '_vanity_router' ],
  
];
?>
</code></pre>
</noscript>

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][twitter].

---

_All code samples in this article have been updated for Craft 3 as of March 2019. [Craft 2 versions can still be found on GitHub][c2 snippet]._

[twitter]:https://twitter.com/miranj
[c2 snippet]:https://gist.github.com/rungta/8a9e3dd48d77be20948751c05f1de0ea/115ab340842f69260bb37f01ef0e07c33f5ba7b9

*[POST]:HTTP POST Request
*[URI]:Universal Resource Identifier
*[URIs]:Universal Resource Identifiers
*[CMS]:Content Management System
*[URL]:Universal Resource Locator
*[URLs]:Universal Resource Locators
*[404]:HTTP 404 Page Not Found