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!