EPiServer Segments Explained, registering custom routes in EPiServer

I’ve previously blogged in EPiServer 7 Routing For Dummies which talked a bit about segments in Episerver. Today we’re going to go one step further and create our own custom segment.

What is a segment?

In the switch over to .Net and MVC the way Url’s are generated are very different.  In the old static html life world, we created folders and html files.  The websites Url structure was made up of this folder and file hierarchy.

With MVC we have controllers and actions and Url’s are based on the controllers name and the methods declared in them.  In the background these are wired up to views and Url’s etc.. are all handled by this thing called routing.

When you work with routing in MVC you will have to start playing with custom routes, like this one

routes.MapRoute(
"Default",
"{controller}/{action}",
new { controller = "Home", action = "Index" }
);

The above snippet defines two segments {controller] and {action}. It tells MVC that when it finds a match on these two segments, do something.

When we add Episerver into the mix they introduce a few of their own custom segments, the blow code defines a default Episerver route:

RouteTable.Routes.MapContentRoute(
"myRoute", "{language}/{node}/{action}/",
new {action = "name"});

In Episerver, as we have page types we don’t do any matching on controller directly as pages can live anywhere in a virtual hierarchy in the editor.  Instead we have {node} which will contain an Episerver content reference.  This is the thing that make the current page magic works.  We also defined {language} segment to deal with multi-language if you’re using it.

Custom_Segment

In this example then search is the first segment and about is the second segment.

Why would I need to create a custom segment?

The most common scenario of why you will need to create a custom segment is to probably SEO wise to make your url’s a bit more user friendly. In the old world let’s say you had a search page that you used a query string to pass in the search result

www.website.com/search/?search=searchterm

With MVC we can make the URL look a lot more cooler by using the URL to pass in the search query, like so:

www.website.com/search/Searchterm

There are loads of variations of these types of requirements like tagging, categories, news
, pagination etc..

Creating a custom segment

What we’re going to do is define our own segment for a search page and then populate a search term parameter so the search term is automatically passed into a controller.

public class SearchSegment : ParameterSegment
{
IContentLoader _contentLoader;
public SearchSegment(string name, IContentLoader contentLoader)
: base(name)
{
_contentLoader = contentLoader;
}
public override bool RouteDataMatch(SegmentContext context)
{
var segmentPair = context.GetNextValue(context.RemainingPath);
var searchTerm = segmentPair.Next;
if (!string.IsNullOrEmpty(searchTerm))
{
return ProcessExtraSegment(context, segmentPair);
}
if (context.Defaults.ContainsKey(Name))
{
context.RouteData.Values[Name] = context.Defaults[Name];
return true;
}
return false;
}
private bool ProcessExtraSegment(SegmentContext context, SegmentPair segmentPair)
{
var content = _contentLoader.Get<IContent>(context.RoutedContentLink);
if (context is SearchPage)
{
context.RouteData.Values[Name] = context.Defaults[Name];
}
else
{
context.RouteData.Values[Name] = segmentPair.Next;
context.RemainingPath = segmentPair.Remaining;
}
return true;
}

So, to create a custom segment, we need to either inherit from SegmentBase or ParameterSegment.  When you inherit from SegmentBase you have to implement GetVirtualPathSegment and RouteDataMatch.  Instead of implement from ParameterSegment, we only have to implement GetVirtualPathSegment

using EPiServer.Web.Routing.RouteParser;
public class ParameterSegment : SegmentBase
{
public ParameterSegment(string name);
EPiServer.Web.Routing.Segments.SegmentBase.Name
public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values);
public override bool RouteDataMatch(SegmentContext context);
}

In GetVirtualPathSegment, what we’re going to do is check that the current page request is of type search page.  If it is populate a segment called “searchterm”.  This means if we have an input string called search term in our search controller it will automatically be populated.  The text it will be populated by will be the next segment, e.g. searchterm

www.website.com/search/searchterm

In our code the first thing we do is get the actual segment value itelf

var segmentPair = context.GetNextValue(context.RemainingPath);
var searchTerm = segmentPair.Next;

This should return us the text ‘searchterm’ in our example. The next bit will assign the segment value into a custom segment.

private bool ProcessExtraSegment(SegmentContext context, SegmentPair segmentPair)
{
var content = _contentLoader.Get<IContent>(context.RoutedContentLink);
if (content is SearchPage)
{
context.RouteData.Values[Name] = context.Defaults[Name];
}
else
{
context.RouteData.Values[Name] = segmentPair.Next;
context.RemainingPath = segmentPair.Remaining;
}
return true;
}

In this method we’re using the Episerver api to check if the current page is the search page.  If it is, then we set the current segment into RouteData and assign it to [Name].

If the page isn’t a search page we carry on and process the page as normal.

Registering The Segment

The last part of the puzzle is actually registering our customer segment.  This is done in the global.ascx file.  If we don’t set the pages up correctly, we’ll get a 404 page error as the MVC handler won’t know how to translate the Url into the correct Episerver age to use.

I should warn you it can be a bit of a pain to debug this when you set it up wrong.  Whenever you get a 404, what we need to do is whenever a match is made on an action, call our custom segment handler so we can tell MVC that we have a correct page match and then populate our custom search term route data.

protected override void RegisterRoutes(RouteCollection routes)
{
base.RegisterRoutes(routes);
IContentLoader contentLoader;
ServiceLocator.Current.TryGetExistingInstance(out contentLoader);
IUrlSegmentRouter segmentRouter;
ServiceLocator.Current.TryGetExistingInstance(out segmentRouter);
if (contentLoader != null && segmentRouter != null)
{
segmentRouter.RootResolver = sd => sd.StartPage;
var parameters = new MapContentRouteParameters
{
UrlSegmentRouter = segmentRouter,
BasePathResolver = EPiServer.Web.Routing.RouteCollectionExtensions.ResolveBasePath,
Direction = SupportedDirection.Both
};
var segment = new SearchSegment("action", contentLoader);
var segmentMappings = new Dictionary<string, ISegment> { { "action", segment } };
parameters.SegmentMappings = segmentMappings;
routes.MapContentRoute(
name: "searchterm",
url: "{language}/{node}/{partial}/{action}/{page}",
defaults: new { action = "index" },
parameters: parameters);
}

I’ll hold my hands up that this bit was copied off another blog.

Conclusion

Today we’ve gone over what a segment is.  When we use MVC routing we need a way to map the url to a controller, action method or anything we want.  This is really useful for SEO friendly Url’s.  Instead of using query string values we can add the old query string values directly into the Url if we want to.

To add this type of feature we need to create a custom segment. We can do this by either inheriting from SegmentBase or ParameterSegment.

In this example, we add the code to only add data into the routing data for search pages only.  If we wanted to populate search terms on any page, then we could simply remove the page type check.

The last bit is registering the segment in the global.ascx. We use the action segment to perform the match and then register a custom route for it.

Code Sample

All the above code can be downloaded in a fully working website from my github account here.

JonDJones.com.EpiSiteSearchSampleSite

Jon D Jones

Software Architect, Programmer and Technologist Jon Jones is founder and CEO of London-based tech firm Digital Prompt. He has been working in the field for nearly a decade, specializing in new technologies and technical solution research in the web business. A passionate blogger by heart , speaker & consultant from England.. always on the hunt for the next challenge

More Posts

6 replies
  1. Az
    Az says:

    Thanks Jon.
    This was working well, but I’ve come across an interesting issue when creating links in the TinyMCE editor to content that matches the custom segment. Steps to recreate:
    1. Add a link using the TinyMCE editor to an internal URL that will match the segment, e.g: http://website.com/search/searchterm and publish the page
    2. View the page – the link should work as expected initially. Now open the TinyMCE editor again, make a minor text edit then view the HTML.
    3. The link will have been changed to an internal format, something like “
    4. Publish the page again. The link does not get changed back to the external representation, but remains in the format above, and doesn’t work when the page is accessed by a visitor.

    Maybe this is peculiar to my environment, but it would be interesting to know if you can reproduce the issue, and if you know of a fix,
    thanks

    Reply
    • jon
      jon says:

      Hi, it might be worth posting your issue in the forums, yet sounds like a bug to me. I know EpiServer uses an older version of TinyMCE and in the later version, a lot of bugs have been resolved. A few EMVP’s have been pushing for this work to be done sooner rather than later. The more visibility EpiServer has of the issues the sooner it might get resolved.

      Out of interest, have you set a breakpoint in the segment code when you hit save on the page you are updating and what happens?

      Reply
      • Az
        Az says:

        Thanks Jon,
        Yeah, I tried setting a break point in the ProcessExtraSegment method. I was hoping to be able to differentiate between editing mode and viewing mode, so I added a check for PageEditing.PageIsInEditMode as well. Some interesting things happen. From editing to publishing the break point gets hit 3 times:

        When I’m editing on-page, I add a matching link (like website.com/search/searchterm) and the break point gets hit twice:
        first when the page auto-saves, then again when I click out of the TinyMCE editor (when it loses focus).
        The first time it gets hit, the SegmentContext ContextMode is ‘Default’. I would have expected this to be ‘Preview’ or ‘Edit’. PageEditing.PageIsInEditMode is true as expected.
        The second time the break point gets hit the ContextMode is still ‘Default’, but PageIsInEditMode is now false. Both times segmentPair.Next is ‘searchterm’ and the segmentpair.Remaning is an empty string.

        When I publish, the break point gets hit again. This time the ContextMode is still Default, while PageIsInEditMode is true. The segmentPair.Next is now “episerver” and the segmentPair.Remaining is “CMS/Content/,,1040/searchterm”. However, in the database, the link is stored in the format “~/link/.aspx?epsremainingpath=searchterm”.
        At this point, a user browsing the page is presented with the correct, functioning link.

        If I make another edit to the page, the link is now presented in the incorrect format: “/episerver/CMS/Content/,,1040/searchterm?epieditmode=False&epsremainingpath=searchterm”, and then on publish it’s this value that gets saved to the database, and the link is now broken for site viewers.

        Cheers,

        Reply
        • Az
          Az says:

          Hi Jon, I posted this on the forums here: “http://world.episerver.com/forum/developer-forum/-Episerver-75-CMS/Thread-Container/2016/5/custom-route-bug/”
          Following Johan’s suggestion I added an extra route like this:

          routes.MapContentRoute(
          name: “searchtermEdit”,
          url: “{language}/{nodeedit}/{partial}/{action}/{page}”,
          defaults: new { action = “index” },
          parameters: parameters);

          And it seems to work now. (in the process I encountered another bug, but that’s a different issue).
          Thanks!

          Reply
          • jon
            jon says:

            That’s interesting, I didn’t know you needed to add a new segement for the editor. I’ll add it to my list of future post topics 🙂

  2. Az
    Az says:

    Sorry, the href mentioned looks like this: “/episerver/CMS/Content/,,1040/searchterm?epieditmode=False&epsremainingpath=searchterm”.

    Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *