React and EPiServer – Getting friendly URLs in EPiServer’s XHtml fields using JOS.ContentSerializer for React support

So just a follow up on the previous article React and EPiServer – Custom rendering of EPiServer block in XhtmlString field without partial block controller in regards to friendly URLs.

We noticed that all our internal EPiServer links in the XHtml field properties remained on the internal form ~/link/{guid}.aspx, even in the view mode. The reason for this was really quite obvious since we had not implemented any handling of them (we wouldn’t get EPiServer’s default functionality for XHtmlString since we use JOS.ContentSerializer to output JSON data to React).

Internal EPiServer URLs in XHtmlString property in view mode

Digging into the our custom implementation of the IXhtmlStringPropertyHandler interface, adding a few debug break points we could see the following.

An internal URL in an UrlFragment in EPiServer's XHtml field.

All of the UrlFragment objects in the XHtmlString field are internal. So we would need to alter the implementation a bit to rewrite them ourselves.

First we need to update the guard clauses, and do a bit of refactoring. The block management is moved into a simliar method as described below.

CustomXhtmlStringPropertyHandler.cs

private void HandleFragmentsFor(ref XhtmlString xhtmlString)
{
  if (xhtmlString?.Fragments == null || !xhtmlString.Fragments.Any())
  {
    return;
  }
  if (SkipHandlingOf(xhtmlString.Fragments))
  {
    return;
  }

  xhtmlString = xhtmlString.CreateWritableClone();
  for (var i = 0; i < xhtmlString.Fragments.Count; i++)
  {
    var fragment = xhtmlString.Fragments[i];

    // ...

    if (fragment is UrlFragment urlFragment &&
        TryConvertUrlFragment(urlFragment, out var uFragment))
    {
      xhtmlString.Fragments[i] = uFragment;
      continue;
    }

If we have an UrlFragment, and it’s convertable from internal to external (friendly) URL, we will replace the occurence with the friendly version in the writable clone array. See above.

The conversion method is shown below. If there are referenced permanent link ids available, we can assume that the UrlFragment is in fact an internal EPiServer URL. In this case, we will also need to find the mode of the current context: view mode, edit mode, preview mode. This will be used to render the correct type of URL. In the current version of EPiServer, the ContextMode.Default option is considered to be view mode.

private bool TryConvertUrlFragment(UrlFragment fragment, out UrlFragment outFragment)
{
  if ((fragment?.ReferencedPermanentLinkIds).IsNullOrEmpty())
  {
    // Not an internal EPiServer URL.
    outFragment = null;
    return false;
  }

  // We need to keep EPiServer's Context Mode while converting URLs (View mode, Edit mode, Preview mode).
  var mode = _contextBase?.Request?.RequestContext?.GetContextMode() ?? ContextMode.Default;
  var internalUrl = new UrlBuilder(fragment.InternalFormat);
  var friendlyUrl = _urlResolver.GetUrl(internalUrl, mode);
  outFragment = new UrlFragment(friendlyUrl);

  return true;
}

_contextBase is a standard HttpContextBase object injected in the constructor, and the _urlResolver is just a wrapper for EPiServer’s functionality. The updated helper methods may be found below.

private static bool Has<T>(StringFragmentCollection fragments)
{
  return fragments.OfType<T>().Any();
}

private bool SkipHandlingOf(StringFragmentCollection fragments)
{
  return !HasBlocksOfType<IHasCustomXhtmlFieldRenderer>(fragments) && !Has<UrlFragment>(fragments);
}