Avoid a FOUCed content delivery experience using CSS and JavaScript

A Flash of Unstyled Content (FOUC) problem occurs when unstyled content is displayed in its raw form during page load, then later laid out as desired. This can be a problem in a Sitecore site as in any other; recently we encountered this while using the jQuery Exposure plugin.

To avoid this, try placing the following, or a functional equivalent, in the <head> section of your layout, after the script tag for jQuery itself:

<style>
.no-fouc {
    visibility : hidden ;
}
</style>

<script>
    $(function () { $( ".no-fouc" ).removeClass("no-fouc"); });
</script>

Then simply apply the no-fouc class to an appropriate container of the elements causing the problem.

Advertisements

Easily construct site-resolving URLs in Sitecore using extension methods

New: Dynamically evaluate C# expressions and execute C# scripts with a single statement, from anywhere in a Sitecore or .NET application. Click here for more info.

The LinkManager class is familiar to anyone who has had to construct URLs in Sitecore. It’s useful, especially with the site resolution feature, designed to help one construct cross-domain links. However, it’s important to remember to set the targetHostName attribute in the site configuration for best results.

As one would expect, given the high degree of extensibility of the Sitecore platform, one can extend and change the default link provider mechanism used to generate links. However, in a large number of cases, writing a custom class and configuring it properly, or even constructing a UrlOptions object explicitly and using it in calls to LinkManager, can be avoided by using a few extension methods. Just as the LinkManager.GetItemUrl() method was designed to make link generation easier, a single-shot method for constructing URLs, right from any item, would make things easier in many programming scenarios.

Below is sample code for such an approach. Features include the ability to programmatically set URL options as the default for a site or container, and a method to call from an Item instance to generate a URL, with the option to override the URL option defaults. Site resolution is performed for any content item in a site configured in Web.config with the targetHostName property, even if in a different site from the context item. If desired, one can tweak this approach further, such as by constructing the default URL options used here from configuration settings dealing with links and site resolution.

There’s nothing wrong with the LinkManager implementation, but a bit of sugar such as this would make it easier to use the framework. It wouldn’t prevent switching to a different link provider implementation, either, since the static extension methods simply front-end the built-in API.

public static class SitecoreDataItemExtensions
{
   static SitecoreDataItemExtensions()
   {
      defaultUrlOptions = new Sitecore.Links.UrlOptions();
      defaultUrlOptions.AddAspxExtension = false;
      defaultUrlOptions.AlwaysIncludeServerUrl = true;
      defaultUrlOptions.LanguageEmbedding = Sitecore.Links.LanguageEmbedding.Never;
      defaultUrlOptions.LowercaseUrls = true;
      defaultUrlOptions.ShortenUrls = true;
      defaultUrlOptions.SiteResolving = true;
      defaultUrlOptions.UseDisplayName = false;   
   }

   private static System.Collections.Concurrent.ConcurrentDictionary<string, Sitecore.Links.UrlOptions> siteUrlOptionsMap = new System.Collections.Concurrent.ConcurrentDictionary<string,Sitecore.Links.UrlOptions>();
   private static Sitecore.Links.UrlOptions defaultUrlOptions;

   /// <summary>
   /// Constructs and returns a clone of the default URL options for the specified site
   /// </summary>
   /// <param name="siteContext">The site for which to get the default URL options</param>
   /// <returns>A clone of the URL options, using the container-wide default as a fallback, with the specified site context applied</returns>
   private static Sitecore.Links.UrlOptions GetDefaultUrlOptions(Sitecore.Sites.SiteContext siteContext)
   {
      Sitecore.Links.UrlOptions urlOptions = defaultUrlOptions;
      try { siteUrlOptionsMap.TryGetValue(siteContext.Name, out urlOptions); } catch {}
      if (urlOptions == null)
         urlOptions = defaultUrlOptions;

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      return urlOptions;
   }

   /// <summary>
   /// Sets a clone of these UrlOptions as the default to use for all sites in this container
   /// </summary>
   public static void SetAsDefault(this Sitecore.Links.UrlOptions urlOptions)
   {
      if (urlOptions == null) return;

      Sitecore.Links.UrlOptions clone = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      clone.Site = null;
      defaultUrlOptions = clone;
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteContext">The optional site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, Sitecore.Sites.SiteContext siteContext = null)
   {
      if (urlOptions == null) return;
      else if (siteContext == null)
      {
         urlOptions.SetAsDefault();
         return;
      }

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      urlOptions.SiteResolving = true;
      
      siteUrlOptionsMap.AddOrUpdate(siteContext.Name, urlOptions, (k, v) => urlOptions);
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteName">The optional name of the site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, string siteName = null)
   {
      if (urlOptions == null) return;

      Sitecore.Sites.SiteContext siteContext = null;
      if (!string.IsNullOrWhiteSpace(siteName))
      {
         try
         {
            siteContext = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
         } 
         catch (Exception e) {
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>); " + e.Message);
         }
         if (siteContext == null) 
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>)");
      }
      
      if (siteContext == null)
         siteContext = urlOptions.Site;

      urlOptions.SetAsSiteDefault(siteContext);
   }

   private static List<Sitecore.Sites.SiteContext> siteContexts = null;

   /// <summary>
   /// Gets a list of site contexts for this container
   /// </summary>
   private static List<Sitecore.Sites.SiteContext> SiteContexts
   {
      get 
      {
         if (siteContexts == null)
         {
            List<Sitecore.Sites.SiteContext> siteContextList = new List<SC.Sites.SiteContext>();

            Sitecore.Sites.SiteContext sc;
            foreach (string siteName in Sitecore.Sites.SiteContextFactory.GetSiteNames())
            {
               sc = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
               if (sc != null) siteContextList.Add(sc);
            }

            siteContexts = siteContextList;
         }

         return siteContexts;
      }
   }

   /// <summary>
   /// Gets the site context, if any, for this item
   /// </summary>
   public static Sitecore.Sites.SiteContext GetSiteContext(this Sitecore.Data.Items.Item item)
   {
      if (item == null) return null;
      string itemPath = item.Paths.FullPath.ToLower();

      foreach (Sitecore.Sites.SiteContext siteContext in SiteContexts)
         if (!string.IsNullOrWhiteSpace(siteContext.RootPath) &&
            siteContext.RootPath.StartsWith("/sitecore/content/") &&
            (siteContext.RootPath.Length > 18) &&
            itemPath.StartsWith(siteContext.RootPath.ToLower()))
            return siteContext;

      return null;
   }

   /// <summary>
   /// Gets a URL for this item; if no optional URL options are supplied, uses site-resolving default URL options
   /// </summary>
   public static string GetUrl(this Sitecore.Data.Items.Item item, Sitecore.Links.UrlOptions urlOptions = null)
   {
      if (item == null) throw new ArgumentNullException();
      Sitecore.Sites.SiteContext siteContext = item.GetSiteContext();

      if (urlOptions == null)
      {
         if (siteContext != null)
            urlOptions = GetDefaultUrlOptions(siteContext);
      }
      else if (siteContext != null)
      {
         urlOptions.Site = siteContext;
      }

      if (urlOptions == null)
         return Sitecore.Links.LinkManager.GetItemUrl(item);
      else
         return Sitecore.Links.LinkManager.GetItemUrl(item, urlOptions);
   }

}

Use ASP.NET output caching safely with post-back

The venerable ASP.NET output caching mechanism is useful for caching HTML fragments generated by controls. This can have a massive impact on performance and scalability of sites, but the implementation is not without drawbacks. One common problem encountered with web forms is how to use output caching, but disable it for post-back or similar scenarios. This can be important, for example, in presenting paged search results, where it can be advantageous to cache the first page of results but users may less frequently go to the second page.

In order to do this, override the GetVaryByCustomString method in Global.asax as follows:

  /// <summary>
  /// Added to support control output caching, varying by URL. 
  /// </summary>
  public override string GetVaryByCustomString(HttpContext context, string custom)
  {
	  switch (custom.ToUpper())
	  {
		  case "RAWURL":
			  {

				  if (context.Request.RequestType.Equals("POST"))
				  {
					  context.Response.Cache.SetNoServerCaching();

					  return "POST " + DateTime.Now.Ticks + " " + context.Request.RawUrl;
				  }
				  else 
					  return context.Request.RawUrl;
			  }
		  default:
			  return "";
	  }
  }

Then, on any control for which you wish to enable output caching but only if the page load is not a post-back, add the following directive:

<%@outputcache duration=”3600″ varybyparam=”none” varybycustom=”RAWURL” %>

Note that the ‘duration’ value is in seconds, and set it accordingly. Obviously the ‘varybycustom’ value can be set to any value desired, as long as it is trapped appropriately in GetVaryByCustomString().

One can easily combine this approach with one sensitive to different cookies as well (one example). One could similarly key off of page-level variables stored in view state, application state variables, etc.; the name of the variable by which to vary may for example be stuffed, with an appropriate prefix, into ‘varybycustom’ in the directive. Strategies like these can be used to achieve a range of different effects, for instance to use output caching safely with paging.