Skip to main content

Optimizely

Custom XhtmlString Render Service – Force Absolute URL for Images

Headless Cms1

Working with a headless website setup sometimes can be challenging.

For example, if you insert an image directly into the TinyMCE text editor sometimes (it depends on a lot of factors) you can see the relative path to the server storage where the image is saved. But because it’s a headless setup, the front end part is at one web address and the back end could be in a different address which can be seen if someone inspects the page source.

The solution for everything is always to write a custom XhtmlString render service because you will mostly use text properties as content working with a headless website setup.

The Solution

The idea is to hijack the final page render and parse the page content.

First, let’s implement an XhtmlStringExtensions class where we will do the parsing of the output content. To do that we will scan the page content for the image tags and replace all image sources attribute with the absolute source path of the image.

public static class XhtmlStringExtensions
{
    /// <summary>
    /// Parses the XHtml String and forces all relative images to be absolute
    /// </summary>
    public static string ForceAbsoluteImgUrls(this XhtmlString html)
    {
        return html == null ? string.Empty : html.ToHtmlString().ForceAbsoluteImgUrls();
    }

    public static string ForceAbsoluteImgUrls(this string input)
    {
        var logger = LogManager.GetLogger(typeof(XhtmlStringExtensions));
        var logPrefix = $"{nameof(XhtmlStringExtensions)}->ForceAbsoluteImgUrls:";

        try
        {
            var doc = new HtmlDocument();
            doc.LoadHtml(input);

            // find all image tags, if there are none just return out- otherwise we need to adjust them

            var imgs = doc.DocumentNode.SelectNodes("//img");
            if (imgs == null)
                return input;

            var scheme = EPiServer.Web.SiteDefinition.Current.SiteUrl.Scheme;
            var serverUrl = new HostString(EPiServer.Web.SiteDefinition.Current.SiteUrl.Host);

            logger.Information($"{logPrefix} scheme={scheme} & serverUrl={serverUrl}");

            // for each image, interrogate src accordingly
            foreach (var img in imgs)
            {
                var src = img.Attributes["src"]?.Value;

                logger.Information($"{logPrefix} img src found: {src}");

                // try parsing uri and determine if it's absolute
                Uri uri = null;
                if (Uri.TryCreate(src, UriKind.Absolute, out uri))
                {
                    if (uri.IsAbsoluteUri)
                    {
                        logger.Information($"{logPrefix} img src is absolute, skipping ({src})");
                        continue; // if it's already absolute, just continue processing
                    }

                }

                // must not be absolute, so go ahead and build an absolute url for it
                var newSrc = UriHelper.BuildAbsolute(scheme, serverUrl, src);
                logger.Information($"{logPrefix} img src is not absolute, fixing setting old {src} to {newSrc}");
                img.SetAttributeValue("src", newSrc);
            }

            var outerHtml = doc.DocumentNode.OuterHtml;
            return outerHtml; // return out the new resulting HTML
        }
        catch (Exception ex)
        {
            logger.Error($"{logPrefix} Error encountered when trying to force absolute URLs in XhtmlStrings", ex);
        }

        return input;
    }
}

Next, we need to implement our custom Xhtml render service from where we will call our Xhtml string extension method.

// hijack the base implementation and force it to use canonical URLs for all src properties
[ServiceConfiguration(typeof(CustomXhtmlRenderService))]
public class CustomXhtmlRenderService : XhtmlRenderService
{
    private ILogger logger;

    public CustomXhtmlRenderService()
        : base(ServiceLocator.Current.GetInstance<ContentApiOptions>(), 
            ServiceLocator.Current.GetInstance<IHtmlHelper>(), 
            ServiceLocator.Current.GetInstance<ITempDataProvider>(),
            ServiceLocator.Current.GetInstance<ICompositeViewEngine>(),
            ServiceLocator.Current.GetInstance<IModelMetadataProvider>())
    {
        logger = ServiceLocator.Current.GetInstance<ILogger<CustomXhtmlRenderService>>();
    }

    public CustomXhtmlRenderService(
        ContentApiOptions options,
        IHtmlHelper htmlHelper,
        ITempDataProvider tempDataProvider,
        ICompositeViewEngine compositeViewEngine,
        IModelMetadataProvider metadataProvider,
        ILogger<CustomXhtmlRenderService> logger) : base(options, htmlHelper, tempDataProvider, compositeViewEngine, metadataProvider)
    {
        this.logger = logger;
    }

    public override string RenderXhtmlString(HttpContext context, XhtmlString xhtmlString)
    {
        var result = base.RenderXhtmlString(context, xhtmlString);

        try
        {
            result = result.ForceAbsoluteImgUrls();
        }
        catch (Exception e)
        {
            logger.LogError($"Unable to correct Absolute Urls in XhtmlString requested from {context?.Request?.GetDisplayUrl()}", e);
        }

        return result;
    }
}

And finally, we need to register our custom Xhtml render service in Startup.cs .

var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(XhtmlRenderService));
services.Remove(serviceDescriptor);
services.AddTransient(typeof(XhtmlRenderService), typeof(CustomXhtmlRenderService));

And that’s it!

 

Additionally, if you want to use this custom Xhtml renderer service in the project that has a front end (regular website) and the headless mode enabled, you can use it simply by adding a template override for the XhtmlString property.

To do so, in your Views folder add this file \Shared\DisplayTemplates\XhtmlString.cshtml .

@using EPiServer.Core
@model XhtmlString
<!-- XhtmlString.cshtml Display Template Start -->
@Html.Raw(Model.ForceAbsoluteImgUrls())
<!-- XhtmlString.cshtml Display Template End -->

 

Happy coding!

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Nenad Nicevski

Nenad is a Certified Optimizely (former EPiServer) Content Cloud Developer. Full stack developer focused on ASP.NET CMS technologies, mostly ASP.NET Core and ASP.NET with MVC. No stranger to WordPress, Drupal 7/8 and Angular JS. In his free time he loves to play basketball, guitar and to make fun of himself to amuse his daughter!

More from this Author

Follow Us