Tuesday 4 September 2018

Post-Processing html after it has been generated in Sitecore

Sometimes we might want to do an operation on the entire html generated for a page, such as move elements around, do some text replacements or regex-based url amendments.  The MVC framework has various features like action filters, result filters and so on, but we can't really use these with Sitecore the same way we would a native MVC site.  In traditional MVC your page is the result of a single action, but in Sitecore it is lots of actions stitched together so there is a little more work to do, so let's get started.

What I have done is created a base class that can be re-used for different filters you might need.

namespace MyNamespace
{
    using System.IO;
    using System.Text;

    public class MemoryStreamFilter : MemoryStream
    {
        private readonly Stream _responseStream;
        private readonly Encoding _encoding;
        private readonly MemoryStream _buffer;

        public MemoryStreamFilter(Stream stream, Encoding encoding)
        {
            _buffer = new MemoryStream();
            _responseStream = stream;
            _encoding = encoding;
        }

        public virtual string ProcessHtml(string html)
        {
            // This function will be overloaded in your derived class to do the actual work
            return html;
        }

        public override void Flush()
        {
            var html = _encoding.GetString(_buffer.ToArray());

            html = ProcessHtml(html);

            var outBuffer = _encoding.GetBytes(html);

            _responseStream.Write(outBuffer, 0, outBuffer.Length);

            base.Flush();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            _buffer.Write(buffer, offset, count);
        }
    }
}


This class is based on the standard MemoryStream class however it overrides the Write method to append the data to an internal buffer.  When that buffer is flushed it converts the internal buffer to a string and calls ProcessHtml (which will do the required updates, whatever those may be), then writes the updated html to the response stream.  Almost all of the methods here are going to be calling indirectly by the underlying MVC\ASP.net framework.  When we ultimately hand this class over to the MVC framework as a filter it will stream the html in chunks via the Write method and when all of the html has been streamed the framework will call the Flush method, and this is where we update the html and write it to the response.

That's our base class, so let's leverage it to do something simple like replace "Hello" with "Goodbye".

namespace MyNamespace
{
    using System.IO;
    using System.Text;

    public class HelloGoodbuyFilter : MemoryStreamFilter
    {
        public HelloGoodbuyFilter(Stream stream, Encoding encoding) : base(stream, encoding)
        {
        }

        public override string ProcessHtml(string html)
        {
            StringBuilder sb = new StringBuilder(html);

            sb.Replace("Hello", "Goodbye");

            return sb.ToString();
        }
    }
}


Next we need to configure our filter so it is used, and we do this via the httpRequestProcessed pipeline so let's create a new pipeline step for our filter.

namespace MyNamespace
{
    using Sitecore.Pipelines.HttpRequest;

    public class MyCustomFilter : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            // Here we're checking a few things are true before deciding to use our filter.
            // We only want to use it when in normal mode and for our front-end site
            if (Sitecore.Context.Item == null
                || !Sitecore.Context.PageMode.IsNormal
                || Sitecore.Context.Site.Name != "website")
            {
                return;
            }

            // Create our filter
            var filter = new HelloGoodbuyFilter(args.Context.Response.Filter, args.Context.Response.ContentEncoding);

            // Tell the context to use it
            args.Context.Response.Filter = filter;
        }
    }
}


Now we create a config path file in the "app_config/include" folder to plug in our custom step.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <pipelines>
      <httpRequestProcessed>
        <processor type="MyNamespace.MyCustomFilter, MyAssembly"/>
      </httpRequestProcessed>
    </pipelines>
  </sitecore>
</configuration>

Here's the output from one of my components that is driven by a rich text box as it appears in preview mode.

<div class="text-group">
   <div class="text-block">
      <p>Hello, John.</p>
   </div>
</div>

But in normal mode;

<div class="text-group">
   <div class="text-block">
      <p>Goodbye, John.</p>
   </div>
</div>

No comments:

Post a Comment