First, let me say this: we LOVE Umbraco here at Liquid Interactive. The simplicity, flexibility, and extensibility of the platform make it a joy to use. However, quite often a client requires a workflow that includes review of a “staging” site prior to making changes live.

The official answer to this is, of course, Umbraco Courier. However, in our experience, Courier is a complex solution that can be confusing for clients and requires additional development to handle custom data types.

This can be more work than necessary when, sometimes, all is the client needs is a way to preview a round of changes prior to publishing them live. Out of the box, Umbraco provides this through the “Preview” button. However, this has the following restrictions:

  • The user must be logged into the CMS.
  • Navigation is limited to child items of the previewed page.
  • Changes to non-public nodes (site setting, etc.) cannot be previewed.

This approach is also not optimal in situations where a QA team needs full access to the site and a client wants to circulate a link internally for UAT testing.

So, what if we could leverage the preview functionality of Umbraco without tying it to a CMS user? Seems straightforward enough, so we set out to do a proof of concept with two goals in mind:

  • Make it simple, with no modification needed to the core.
  • Build it as a package that can be reused for future projects and shared with the Umbraco community.

Before we talk about the proof of concept, let’s quickly review how Umbraco preview works (in a general sense). First, preview xml is created by cloning the umbraco.config and inserting unpublished information. Then, when a preview is requested, a cookie is set instructing umbraco to load content from the preview xml rather than the umbraco.config.

So we need to do two main things to create our virtual staging environment: create staging xml and instruct Umbraco to use it.

Creating Staging XML

To do this, we loosely modelled a “StageContent” class from the core’s umbraco.presentation.preview class. (Note, we will use the “PrepareStageDocument” method to update our stageviewer.xml whenever a content item is saved.)

The salient parts of this method are below.

       //get content root  
       var documentObject = new Document(-1);  
       // clone xml  
       XmlContent = (XmlDocument)content.Instance.XmlContent.Clone();  
       //include the current node's preview xml  
       //This is to cover saveandpublish scenarios where the published data isn't included in the clone
       if (currentDoc.Id != -1)  
       {  
         var stageXml = currentDoc.ToPreviewXml(XmlContent);  
         if (documentObject.Published == false  
           && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(currentDoc.Id))  
         {  
           stageXml.Attributes.Append(XmlContent.CreateAttribute("isDraft"));  
         }  
         XmlContent = content.AppendDocumentXml(currentDoc.Id, currentDoc.Level, currentDoc.ParentId, stageXml, XmlContent);  
       }  
       if (includeSubs)  
       {  
         //use the built in GetNodesForPreview method of the API to get the draft form of each child node  
         //and add it to the staged content  
         foreach (var stageNode in documentObject.GetNodesForPreview(true))  
         {  
           var stageXml = XmlContent.ReadNode(XmlReader.Create(new StringReader(stageNode.Xml)));  
           if (stageNode.IsDraft) {  
             stageXml.Attributes.Append(XmlContent.CreateAttribute("isDraft"));  
           }  
           XmlContent = content.AppendDocumentXml(stageNode.NodeId, stageNode.Level, stageNode.ParentId, stageXml, XmlContent);  
         }  
       }  
       SaveStageSet();  

Instruct Umbraco to Use the Staging XML

The Umbraco preview model doesn’t work here; we don’t want to set a cookie, and we don’t want to check if the user is logged in with permissions. We simply want to check if the user is accessing the site from a staging domain and then display the staging content.

To do this, we can use a simple content finder (inherit from Umbraco.Web.Routing.IContentFinder).

Below we check for the staging domain and replace the xml.

  public bool TryFindContent(PublishedContentRequest contentRequest)  
     {  
       var d = contentRequest.Uri.DnsSafeHost.ToLower();  
       if (!IsStagingDomain(d)) return false;  
       //get our staging xml  
       var sc = new StagedContent();  
       sc.EnsureInitialized(true, () =>  
       {  
         if (sc.ValidStageSet)  
           sc.LoadStageset();  
       });  
       if (sc.ValidStageSet)  
       {  
         //replace the live xml with the staging xml  
         UmbracoContext.Current.HttpContext.Items["UmbracoXmlContextContent"] = sc.XmlContent;  
       }  
       //run next ContentFinder in Pipeline  
       return false;  
     }  

Putting the pieces together

All that remains is to be sure our logic is called at the appropriate times. To keep the staging xml updated, we attach to the ContentService.Saved event. And then, to ensure our custom Content Finder is called, we insert it before the “ContentFinderByNiceUrl”.

  public class Events : ApplicationEventHandler  
   {  
     public Events()  
     {  
       ContentService.Saved += Saved;  
       TreeControllerBase.MenuRendering += TreeControllerBase_MenuRendering;  
       StagingDomain.AfterDelete += StagingDomain.DeleteStagingInfo;  
       // StagingDomain.AfterSave  
     }  
     protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)  
     {  
       ContentFinderResolver.Current.InsertTypeBefore<ContentFinderByNiceUrl, StagedContentFinder>();  
     }  
     private void Saved(IContentService sender, SaveEventArgs<IContent> args)  
     {  
       var sc = new StagedContent();  
       var firstOrDefault = args.SavedEntities.FirstOrDefault();  
       if (firstOrDefault != null)  
       {  
         sc.PrepareStageDocument(firstOrDefault.Id, true);  
       }  
       else  
       {  
         sc.PrepareStageDocument(-1, true);  
       }  
     }  
   }  

That’s it! A simple staging site using the current umbraco instance. Of course, the simplicity of this solution does not come without a few drawbacks:

  • We are only previewing content changes, not code changes.
  • We are previewing everything in draft state, not selective changes.

But the simplicity and reliability of this solution compared to a multiple server/courier environment is just the ticket for many clients.

We hope to share our solution with the Umbraco community in the very near future, after we put on some final polishes and integrate the staging URL settings into the back office. Look for a downloadable package to be released soon!

Update: Our plugin is now available in Part 2.

Looking for help in Umbraco? We're an Umbraco Certified Partner. Contact us and let's talk about how Liquid can help you!

Steve Bridges

About Steve Bridges

Currently serving as Liquid's CIO and Director of Application Development, Steve possesses nearly 20 years of experience in professional software development.  He cut his technical teeth in the financial industry where he spent a decade developing websites, applications, and line of business applications for a credit card company. It was there he developed a love for marketing and found a home in agency life.   A lifelong Lehigh Valley native, he jumped at the chance 5 years ago to join the Liquid family.  

Published Mar 18, 2015