Search code examples
asp.net-mvcasp.net-routing

Manipulate the url using routing


In my website I have the following route defined:

routes.MapRoute(
   name: "Specific Product",
   url: "product/{id}",
   defaults: new { controller = "", action = "Index", id = UrlParameter.Optional }
);

In that way I want customers to be able to add the ID of the product and go to the product page.

SEO advisors have said that it would be better if we could add a description of the product on the URL, like product-name or something. So the URL should look something like:

/product/my-cool-product-name/123

or

/product/my-cool-product-name-123

Of course the description is stored in the db and I cannot do that with a url rewrite (or can I?)

Should I add a redirection on my controller (this would seem to do the job, but it just doesn't feel right)

On a few sites I checked they do respond with a 301 Moved Permanently. Is that really the best approach?

UPDATE

As per Stephen Muecke's comment I checked on what is happening on SO.

The suggested url was my own Manipulate the url using routing and i opened the console to see any redirections. Here is a screenshot:

SO-301-response


Solution

  • So, first of all very special thanks to @StephenMuecke for giving the hint for slugs and also the url he suggested.

    I would like to post my approach which is a mix of that url and several other articles.

    My goal was to be able to have the user enter a url like:

    /product/123

    and when the page loads to show in the address bar something like:

    /product/my-awsome-product-name-123

    I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently response is used in all i checked. Even SO as shown in my question uses 301 to add the title of the question. I thought that there would be a different approach that would not need the second round trip....

    So the total solution i used in this case was:

    1. I created a SlugRouteHandler class which looks like:

      public class SlugRouteHandler : MvcRouteHandler
      {
          protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
          {
              var url = requestContext.HttpContext.Request.Path.TrimStart('/');
      
              if (!string.IsNullOrEmpty(url))
              {
                  var slug = (string)requestContext.RouteData.Values["slug"];
                  int id;
      
                  //i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
                  if (Int32.TryParse(slug, out id))
                  {
                      //get the product from the db to get the description
                      var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
                      //if the product exists then proceed with the transformation. 
                      //if it does not exist then we could addd proper handling for 404 response here.
                      if (product != null)
                      {
                          //get the description of the product
                          //SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
                          var description = String.Concat(product.name, "-", id).SEOFriendly(); 
                          //transform the url
                          var newUrl = String.Concat("/product/",description);
                          return new RedirectHandler(newUrl);
                      }
                  }
      
              }
      
              return base.GetHttpHandler(requestContext);
          }
      
      }
      
    2. From the above i need to also create a RedirectHandler class to handle the redirections. This is actually a direct copy from here

      public class RedirectHandler : IHttpHandler
      {
          private string newUrl;
      
          public RedirectHandler(string newUrl)
          {
              this.newUrl = newUrl;
          }
      
          public bool IsReusable
          {
              get { return true; }
          }
      
          public void ProcessRequest(HttpContext httpContext)
          {
              httpContext.Response.Status = "301 Moved Permanently";
              httpContext.Response.StatusCode = 301;
              httpContext.Response.AppendHeader("Location", newUrl);
              return;
          }
      }
      

    With this 2 classes i can transform product ids to SEO friendly urls.

    In order to use these i need to modify my route to use the SlugRouteHandler class, which leads to :

    1. Call SlugRouteHandler class from the route

      routes.MapRoute(
         name: "Specific Product",
         url: "product/{slug}",
         defaults: new { controller = "Product", action = "Index" }
      ).RouteHandler = new SlugRouteHandler();
      

    Here comes the use of the link @StephenMuecke mentioned in his comment.

    We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.

    1. We need to create an Action filter to handle the new param passed before calling the controller

      public class SlugToIdAttribute : ActionFilterAttribute
      {
      
          public override void OnActionExecuting(ActionExecutingContext filterContext)
          {
              var slug = filterContext.RouteData.Values["slug"] as string;
              if (slug != null)
              {
                  //my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id. 
                  //if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
                  int id;
                  Int32.TryParse(slug.Split('-').Last(), out id);
                  if (id != 0)
                  {
                      //the controller expects an id and here we will provide it
                      filterContext.ActionParameters["id"] = id;
                  }
              }
              base.OnActionExecuting(filterContext);
          }
      }
      

    Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.

    I really do not care if the product name is actually the product name. You could try fetching the following urls:

    \product\123

    \product\product-name-123

    \product\another-product-123

    \product\john-doe-123

    and you would still get the product with id 123, though the urls are different.

    1. Next step is to let the controller know that it has to use a special filer

      [SlugToId]
      public ActionResult Index(int id)
      {
      }