Content URL, with or without shortcut?
Episerver CMS is a Content Management System. It manages content. Having predictable methods for creating, modifying and retrieving content is crucial for a CMS. Most of the content can probably be accessed using some sort of URL. Retrieving the URL to any given content should also be a thing I would expect a CMS to handle without any surprises.
Just a simple URL
So given I have some sort of content, in this example a PageData object. PageData is the base of all pages in an Episerver site. How do I get the URL from C# code?
We could try the LinkURL property.
var url = pageData.LinkURL;
--> /link/87b3117447584dfead58dceee3b36c42.aspx
It provides some sort of URL. It's not a friendly URL. It's not friendly for our website's visitors and it's not friendly to search engines. But it works. If you try to visit the URL in a browser, Episerver will respond with an HTTP 301 Moved Permanently, redirecting you to a more friendly URL like «/en/my-new-page».
You can also find this URL directly in the database:
SELECT LinkURL
FROM tblContentLanguage
WHERE fkContentID = pageData.LinkURL
--> ~/link/87b3117447584dfead58dceee3b36c42.aspx
It's just the ContentGUID made lowercase and prepended with /link/ and appended with .aspx
SELECT ContentGUID
FROM tblContent
WHERE pkID = pageData.LinkURL
--> 87B3117447584DFEAD58DCEEE3B36C42
A friendly URL, may be generated by UrlResolver like this.
var url = UrlResolver.Current.GetUrl(pageData.ContentLink);
--> /en/my-new-page/
Suppose we move to a Razor view, and our model contains a reference to a PageData object, a ContentReference. We would like to present a clickable html link, that points to our PageData.
The pagetype contains this property:
public virtual ContentReference Link { get; set; }
Adding this to our Razor view:
@Html.ContentLink(Model.Link)
Produces this html output, much like I would expect:
<a href="/en/my-new-page/">My new page</a>
Say I would like full control over my anchor tag. Maybe add another link text, some custom attributes or just some marquee.
<a href="@Url.ContentUrl(Model.Link)">
<h1>
<marquee style="color:red">Let's get this party started!</marquee>
</h1>
</a>
Still the output is as expected:
<a href="/en/my-new-page/">
<h1>
<marquee style="color:red">Let's get this party started!</marquee>
</h1>
</a>
The problem
So, times move on, and I create a new page, and I want my old page to send visitors to my new page. I use the shortcut property on the Settings tab:
And add a shortcut from «My new page» to «My brand new page»:
Once more I check my view with this markup:
@Html.ContentLink(Model.Link)
It has changed, and the URL now points to My brand new page! Great!
<a href="/en/my-brand-new-page/">My new page</a>
Any of the following will also respect the shortcut property:
@Html.PropertyFor(x => x.Link);
@Html.DisplayFor(x => x.Link);
What about the link with marquee? This:
<a href="@Url.ContentUrl(Model.Link)">
<h1>
<marquee style="color:red">Let's get this party started!</marquee>
</h1>
</a>
Still produces the same output, completely ignoring the shortcut settings on My new page!
<a href="/en/my-new-page/">
<h1>
<marquee style="color:red">Let's get this party started!</marquee>
</h1>
</a>
So two methods ContentUrl and ContentLink. The first gives me only the Url and the latter gives me markup for the full Link. This is as expected. But why does one respect shortcut settings, while the other does not?
What if I have a ContentReference, and just want the URL, that respect the shortcut settings? Can Episerver help me with that? No.
If I had a PageData object I could use UrlExtensions.PageUrl, that will respect the shortcut settings. In a view I often have a ContentReference, and would prefer not having to call ContentLoader to be able to get a predictable URL.
@Url.PageUrl(pageData.LinkURL)
Does it really matter?
Being consistent is good. Giving different URLs to the same content in different scenarios, without good reason, is confusing.
If you create a link to the old content, and a user clicks that link, the user will be redirected to the new content. It will still work, but one less redirect is better.
The solution
I can implement my own UrlExtension as Episerver has done in the Alloy demo site. Just 137 lines of code, including this:
public static string PageUrl(this UrlHelper urlHelper,
PageData page, RouteValueDictionary routeValueDictionary)
{
if (page == null || !page.HasTemplate())
return string.Empty;
switch (page.LinkType)
{
case PageShortcutType.Normal:
case PageShortcutType.Shortcut:
case PageShortcutType.FetchData:
return PageUrl(urlHelper, page.PageLink, routeValueDictionary, null, null);
case PageShortcutType.External:
return page.LinkURL;
case PageShortcutType.Inactive:
return string.Empty;
default:
return string.Empty;
}
}
I would prefer not having to maintain my own UrlExtension-class to perform a task that should be key to any Content Management System.
I luckily managed to convince Episerver Support and the dev team that this kind of functionality should be part of Episerver core. It has been registered as bug «CMS-14788».
The bug was fixed in EPiServer.CMS.Core 11.20.1.