Creating a Link to a Page in your EPiServer 7 MVC View

March 2022 Update: This post was originally written for EPiServer version 7. In 2021, Episerver (now known as Optimizely) released version 12. While some parts of this post are outdated, other parts are still relevant and may be helpful. No content in this post has been changed for the current version.

When developing an EPiServer 7 site (or really any site, for that matter), making a link to a page is one of the most common things you'll do. So, I thought I'd do something a little more basic and explore how to handle this task in your front-end page and block templates, specifically using MVC Razor views. In this post, we'll look at what you can currently use to create a link to a page, then I'll introduce some extension methods that you can use to give yourself a little more flexibility in your MVC view.

Current Options and Limitations

First of all, resist the temptation to solely rely on the LinkURL property in the PageData object. This will return an internal link to the page that isn't very user friendly. Sure, when you click the link it takes you to the correct page, but we don't want to be showing these links to the outside world. Instead, rely on the helpers and extensions that EPiServer 7 provides.

Html.PageLink

The simplest and most common way to create a link to page is to use EPiServer's Html.PageLink helper. Using this helper, you can pass in a PageData object, a PageReference, or even a LinkItem, and it will create an HTML anchor, using the clean, friendly URL to the page. For the text of the link, if you do not supply it with a text string, it will use the PageName property for the page (or the Text property for a LinkItem object).

@Html.PageLink(Model.SomePageReference)
@Html.PageLink(Model.SomePageData)
@Html.PageLink("A link to a page", Model.SomeLinkItem)

You can also pass in route data information that can change the URL (helpful when working with language branches) or additional HTML attributes to add to the anchor tag.

@Html.PageLink("A link to a Swedish page", Model.SomePageReference, new { language = "sv-SE" }, null)
@Html.PageLink("A link with a class", Model.SomePageReference, null, new { @class = "link-class" })

Be sure to check the SDK for this helper, because there are a lot more options you could use that might be a better fit for your needs.

In most cases, this HTML helper is what you'll use to create a link to a page. There are some situations, however, where you'll need a little more flexibility. A common example is when you want to wrap an image tag with an anchor tag, or when you want to wrap more elements. To solve that situation, you could build an image link HTML helper, or you could just retrieve the actual URL to the page using Url.PageUrl.

Url.PageUrl

Another way is to use EPiServer's Url.PageUrl helper. This helper only takes one parameter: the internal URL for the page. It will then use EPiServer's PermanentLinkUtility to find the friendly URL to the page, and return only the URL as a string. You could then take that string and use it directly in an HTML anchor tag.

<a href="@Url.PageUrl(Model.SomePageData.LinkURL)">
    A link to a page
</a>

This covers a lot of the other cases when you need better flexibility when creating a link to a page. The only thing I don't like about this, though, is that you need to have the PageData of the page to use it. So if you have a PageReference property on your page or block type, you'll need to use IContentLoader or IContentRepository to get the PageData from the PageReference. Wouldn't it be easier to just pass in a PageData or a PageReference object instead?

Another Option: Extending the UrlHelper

The solution I put together, which I think provides the best of both worlds, is heavily based on the code for EPiServer's Html.PageLink helper. It only returns the friendly URL to a page like Url.PageUrl, but still gives you the flexibility to pass in a PageData or a PageReference object like Html.PageLink. Rather than extending HtmlHelper, it extends UrlHelper and works alongside of the Url.PageUrl helper.

UrlExtensions.cs

public static class UrlExtensions
{
    public static string PageUrl(this UrlHelper urlHelper, PageReference pageLink)
    {
        return UrlExtensions.PageUrl(urlHelper, pageLink, (object)null, (IContentRepository)null);
    }

    public static string PageUrl(this UrlHelper urlHelper, PageReference pageLink, object routeValues)
    {
        return UrlExtensions.PageUrl(urlHelper, pageLink, routeValues, (IContentRepository)null);
    }

    public static string PageUrl(this UrlHelper urlHelper, PageData page)
    {
        return UrlExtensions.PageUrl(urlHelper, page, (object)null);
    }

    public static string PageUrl(this UrlHelper urlHelper, PageData page, object routeValues)
    {
        if (!PageDataExtensions.HasTemplate(page))
            return string.Empty;

        switch (page.LinkType)
        {
            case PageShortcutType.Normal:
            case PageShortcutType.Shortcut:
            case PageShortcutType.FetchData:
                return UrlExtensions.PageUrl(urlHelper, page.PageLink, routeValues, (IContentLoader)null, (IPermanentLinkMapper)null, (LanguageSelectorFactory)null);

            case PageShortcutType.External:
                return page.LinkURL;

            case PageShortcutType.Inactive:
                return string.Empty;

            default:
                return string.Empty;
        }
    }

    private static string PageUrl(this UrlHelper urlHelper, PageReference pageLink, object routeValues, IContentRepository contentRepository)
    {
        if (contentRepository == null)
            contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
        if (PageReference.IsNullOrEmpty(pageLink))
            return string.Empty;
        PageData page = contentRepository.Get<PageData>((ContentReference)pageLink);
        return UrlExtensions.PageUrl(urlHelper, page, routeValues);
    }

    private static string PageUrl(this UrlHelper urlHelper, PageReference pageLink, object routeValues, IContentLoader contentQueryable, IPermanentLinkMapper permanentLinkMapper, LanguageSelectorFactory languageSelectorFactory)
    {
        RouteValueDictionary routeValueDictionary = new RouteValueDictionary(routeValues);
        if (!routeValueDictionary.ContainsKey(RoutingConstants.LanguageKey))
            routeValueDictionary[RoutingConstants.LanguageKey] = (object)ContentLanguage.PreferredCulture.Name;
        if (!routeValueDictionary.ContainsKey(RoutingConstants.ActionKey))
            routeValueDictionary[RoutingConstants.ActionKey] = (object)"index";
        routeValueDictionary[RoutingConstants.NodeKey] = (object)pageLink;
        UrlExtensions.SetAdditionalContextValuesForContent(urlHelper, pageLink, routeValueDictionary, contentQueryable, permanentLinkMapper, languageSelectorFactory);
        return urlHelper.Action((string)null, routeValueDictionary);
    }

    private static void SetAdditionalContextValuesForContent(this UrlHelper urlHelper, PageReference pageLink, RouteValueDictionary values, IContentLoader contentQueryable, IPermanentLinkMapper permanentLinkMapper, LanguageSelectorFactory languageSelectorFactory)
    {
        bool IdKeep = HttpContext.Current.Request.QueryString["idkeep"] != null;
        contentQueryable = contentQueryable ?? ServiceLocator.Current.GetInstance<IContentLoader>();
        permanentLinkMapper = permanentLinkMapper ?? ServiceLocator.Current.GetInstance<IPermanentLinkMapper>();
        languageSelectorFactory = languageSelectorFactory ?? ServiceLocator.Current.GetInstance<LanguageSelectorFactory>();
        IContent content = contentQueryable.Get<IContent>(pageLink, languageSelectorFactory.Fallback(values[RoutingConstants.LanguageKey] as string ?? ContentLanguage.PreferredCulture.Name, true));
        if (content == null)
            return;
        if (IdKeep)
            values["id"] = (object)content.ContentLink.ToString();
        UrlExtensions.SetAdditionalContextValuesForPage(values, IdKeep, content);
    }

    private static void SetAdditionalContextValuesForPage(RouteValueDictionary values, bool IdKeep, IContent content)
    {
        PageData pageData = content as PageData;
        if (pageData == null)
            return;
        if (pageData.LinkType == PageShortcutType.Shortcut)
        {
            PropertyPageReference propertyPageReference = pageData.Property["PageShortcutLink"] as PropertyPageReference;
            if (propertyPageReference != null && !PageReference.IsNullOrEmpty(propertyPageReference.PageLink))
            {
                values[RoutingConstants.NodeKey] = (object)propertyPageReference.PageLink;
                if (IdKeep)
                    values["id"] = (object)((object)propertyPageReference).ToString();
            }
        }
    }
}

At this point, I'm just supporting passing in PageData and PageReference objects, but it could easily be extended to support the other objects that Html.PageLink supports. I also made it support additional route data information, which will help when creating links to pages in another language branch.

<a href="@Url.PageUrl(Model.SomePageReference, new { language= "sv-SE" })">
    <img src="swedish_flag.png" />
</a>

I hope you find this useful, and that it gives you the most flexibility you need when creating a link to a page in your EPiServer site!