Create SEO-friendly URLs with a slug in ASP.NET Core MVC
For a little while, I have been working on a side-project now in ASP.NET Core. This web application generates URLs that have a (GUID) id in it, which is not particularly nice to look at. For this reason, I decided to have a look at how I can implement a so-called slug into the URLs. This is a friendly looking name based on text which looks a lot better and is also better for the SEO of your website. In this post, I will share with you the things I ran into.
The project I am working on is open-source and can be found on GitHub: https://github.com/jfversluis/CfpExchange. The project is very much alive though, so don’t be surprised if the code doesn’t look like it does in this post. The actual running application can be found on https://cfp.exchange/
A bit of background #
The web application I have been working on is fairly simple in basics. One the one hand people can create entries, on the other hand, people can read those same entries. When saving the entries to the database a new unique id is generated in the form of a GUID. To access the same entries, someone would need to go to a link like this: https://cfp.exchange/cfp/details/c6bfc15d-4d82-47a3-93b1-81c2d2dc0e98. That isn’t really nice to look at and also it doesn’t really tell you anything about the contents.
If you look at this blog post, for example, you will notice the URL will have a nice short form of the posts’ title. This is also known as a slug. That is what I wanted for my web application!
Implementing a slug #
Implementing this for my web application was actually pretty easy. Depending on your requirements/wishes there are a couple of ways to do it. I have seen some variations where it is implemented mainly for SEO reasons and the slug is just added after or together with the id of the entity. In my example, I would still have a URL that looks like https://cfp.exchange/cfp/details/c6bfc15d-4d82-47a3-93b1-81c2d2dc0e98/my-slug-here/. When using a simple integer as an id this is feasible. For a GUID, not so much. Therefore I just wanted the slug and no more ID. Of course, the challenge here is to generate a slug that is unique. More on this later.
Generate the slug #
I figured the first thing I needed to do was be able to generate a slug. Surely I wasn’t the first person to want this. So after some Google-fu I ended up, of course, on Stackoverflow. On this link: https://stackoverflow.com/questions/25259/how-does-stack-overflow-generate-its-seo-friendly-urls/25486 the nice folks at SO show us how they do it. Or at least did it a couple of years ago. Inspecting the code it seemed good enough for me. Basically what it does is remove all characters that are not a number or letter and switch spaces with dashes. You can see the full code underneath.
For such a simple task this seems like a lot of code, but if you take a better look at it, you will realize that there are a lot of edge cases in here that are accounted for.
Edit May 24th 2019: #
From a friendly reader from Iceland I got a tip that the “Stackoverflow code” from above wasn’t entirely correct. The ð symbol was replaced by a o, while it should’ve been replaced by a d. Funny how languages work, right?
In any case, I have updated the code to be more precise in this regard. In addition, he mentioned this library: SlugGenerator. Which might be helpful to you. At the time of writing it wasn’t updated in a few months, but then again, how many updates to you see on the alphabet? ;)
With this method, I can simply generate a slug from the title of my entity and save that in a new column. Underneath you can see the piece of code that does this for me.
Because I only want to navigate based on slugs, I need to ensure that my slugs are unique. To do this I simply check if the generate slug is already in the database and simply append a number. There are probably better ways to do this, but for now, I will just see how often a duplicate is generated and deal with it then. Next step: updated my links and routes to be able to deal with the slug.
Navigating based on the slug #
This is where I got a bit confused. Ideally, I wanted both: reaching the page by the GUID and by its slug. Because the website has been up for a couple of weeks, there are already some links circulating and I didn’t want these to break. Again, ideally, I didn’t want to go through each line of code where a link was generated. So, first I tried to create another route which handled the slug, next to the route that was already in place. These had these signatures:
public IActionResult Details(string slug)
public IActionResult Details(Guid id)
I hoped that the system would figure out which route to take. I mean, they are different types, right?! Turns out I was wrong, or at least: I was doing something wrong.
I don’t remember what happened when, but I ended up just one of the Details methods being hit in either case. Meaning that in one case my GUID was empty which caused trouble or in the other case my GUID was a string which caused problems as well.
Then I decided to do things differently. I took the old Details method with the GUID parameter, changed that to a string and then implemented some logic to assume that what the input parameter contained a slug. If not, check if it’s a GUID and go the old route. What I ended up with, is this:
Check if we can find an entity with the string value if not, it is possibly a GUID. Check that. In case we still can’t find anything we just redirect to the homepage. In other cases when we do find a result, return that and show the requested result. Now, this navigation is fixed. I could reach my entities from both the GUID as well as the slug on the same base URL. The last thing I wanted to do is now generate the links on my web application to be links with the slugs.
Generating links with a slug #
As mentioned before, I wanted to do this without having to go through all the places a link was generated. So, I tried to create my own UrlHelper, which somewhat worked, but decided, in the end, it had too much hardcoded routes and deviated from the default implementation too much. And thus, I ended up going through the links one by one. Luckily there weren’t too many places.
To also stick with the default ASP.NET MVC routing I named the slug id as you can see in the Details method above. To fully understand the why or how, let me show you how I generate links in my Razor pages.
In the above code, you see an example of how I generate a slug link. Before, where Model.NewestCfp.Slug is, it said Model.NewestCfp.Id, causing the GUID to be inserted there. At my first attempt, I changed that whole line to this: @(Url.Action(“details”, “cfp”, new { slug = Model.NewestCfp.Slug })). Note that it says slug = instead of id =. This caused my links to look this: https://cfp.exchange/cfp/details/?slug=my-slug-here. While this did work, it still didn’t look that great. I could also have changed the route in the MVC routing engine to work with slug but I figured changing the parameter name to id would be easier. And since I am using the slugs as an ID now as well, it ain’t too bad. Now, my links look great: https://cfp.exchange/cfp/details/good-tech-conf-using-technology-for-social-good
Wrapping up #
Implementing this slug functionality didn’t take as much time in the end. I can imagine that the actual implementation can vary depending on your situation. And it is probably easier if you think of this upfront. Because I implemented this afterward, and if you are doing so as well, don’t forget that you need to update your current entries with a slug as well! Or take into account that an entry can do without it.
Now, my links look great and hopefully it will do wonders for my SEO as well. At least I did learn this! ;-)