PnP – Office 365 Starter Intranet Solution (Part 4: The navigation implementation)

In publishing intranets (or web sites in general), navigation implementation of menus are always almost the same:

  • A top navigation menu (i.e customers always want a mega menu)
  • A contextual menu (i.e the quick launch or left menu)
  • A breadcrumb
  • Header links
  • Footer links

By default, SharePoint provides you the first three* based on a taxonomy or structural navigation (sites and sub-sites). But what happens to the two others? You can’t use the default navigation behavior to implement a footer or a header component. To bypass this constraint, I developed a custom navigation mechanism based on the SharePoint taxonomy to be able to implement any navigation component using an unique pattern.

Maybe some of you have already used my previous contribution. In the present solution, I didn’t reinvent anything and it is basically the same. I changed the Require JS approach to a Webpack approach. With Require JS, I experienced some performance issues due to the number of files loaded asynchronously (Require JS has an « optimizer » called r.js but definitely not as efficient as Webpack). Each navigation menu has its own data source based on a taxonomy term set (except for breadcrumb and contextual menus. See the performance considerations section below).

navigation implementation - News Page Navigation Menus
News Page Navigation Menus

 

*Breadcrumb is not enabled in SharePoint by default.

Static vs dynamic contents

In an intranet or a website, contents are generally divided into two types having their own lifecycle:

  • The static content like the « About us » page. This type of content is relatively permanent.
  • The dynamic content like the company news and announcements. This type of content is updated very often.

Even though the static content part is relatively easy to manage via a taxonomy term set, the dynamic one is not much. For obvious maintenance and performance reasons, we don’t want to create a single taxonomy term for each news in the portal. That’s why we create an « anchor » term instead. For example: all news will be tagged under the « News » term which represents a category instead of a single page. If you already know the « Cross Site Publishing » feature, this is basically the same concept here except for the URL construction with slugs.

Static page navigation

Static page navigation
Static page navigation

 

 

 

News page navigation

News navigation
News navigation

 

Working with taxonomy using JavaScript

In SharePoint, there is no REST endpoint for the taxonomy service (yet?) so you still have to use the SharePoint CSOM (or JSOM to be more precise). Before you can use CSOM functions, you will have to load dedicated SharePoint scripts (like sp.taxonomy.js, sp.js, etc.). In my previous solution I used Require JS to load these scripts « on-demand ». Because of Webpack is basically a « bundler » and not a « loader » we can’t use this approach anymore, so I had to fall back to the SharePoint SOD mechanism. All dependencies are loaded via the init() function in the taxonomy.ts file like this:

public init(): Promise<void>  {
 
    // Initialize SharePoint script dependencies
    SP.SOD.registerSod("sp.runtime.js", "//www.sharepointeurope.com/_layouts/15/sp.runtime.js");
    SP.SOD.registerSod("sp.js", "//www.sharepointeurope.com/_layouts/15/sp.js");
    SP.SOD.registerSod("sp.taxonomy.js", "//www.sharepointeurope.com/_layouts/15/sp.taxonomy.js");
    SP.SOD.registerSod("sp.publishing.js", "//www.sharepointeurope.com/_layouts/15/sp.publishing.js");
 
    SP.SOD.registerSodDep("sp.js", "sp.runtime.js");
    SP.SOD.registerSodDep("sp.taxonomy.js", "sp.js");
    SP.SOD.registerSodDep("sp.publishing.js", "sp.js");
 
    let p = new Promise<void>((resolve) => {
 
        SP.SOD.loadMultiple(["sp.runtime.js", "sp.js", "sp.taxonomy.js", "sp.publishing.js"], () => {
 
            // Resolve the promise allowing you to execute your logic using Promise.then(() => { ... });
            resolve();
        });
    });
 
    return p;
}

Working with « navigation » taxonomy is slightlty diffrent than the « classic » taxonomy. The key method to work with navigation term sets is getAsResolvedByWeb():

...
let termSet: SP.Taxonomy.TermSet = termStore.getTermSet(termSetId);
 
let webNavigationTermSet = SP.Publishing.Navigation.NavigationTermSet.getAsResolvedByWeb(context, termSet, currentWeb, "GlobalNavigationTaxonomyProvider");
...

It allows you to retrieve the navigation properties for a specific term set even if this one is not bound to the current web as the navigation source*. If you use the classic approach to get taxonomy terms, navigation properties will be not accessible. The term set object must be a « NavigationTermSet » and not a « TermSet« . Last but not least, to get this work you will have to ensure your term is set a navigation term set:

navigation-term-set
Navigation Term Set

 

* Set a taxonomy term set as the navigation source for a SharePoint site is only useful to give a context for friendly URLs.

The getNavigationTaxonomyNodes() method returns an array of navigation nodes with all needed properties like the id, term custom properties, etc. Then, they can be rendered differently depending the navigation component (in this case with a Knockout HTML template).

...  
 
// Ensure all SP dependencies are loaded before retrieving navigation nodes
this.taxonomyModule.init().then(() => {  
 
    this.taxonomyModule.getNavigationTaxonomyNodes(new SP.Guid(termSetId)).then(navigationTree => {
 
        // Do something with navigation nodes
 
    });
 
});
 
...

 

Performance considerations

Using a lot of JavaScript code with multiple asynchronous calls may cause some significant performance issues causing a poor user experience. This is especially true for navigation menus. Because of static navigation nodes in an intranet don’t change very often (they don’t include news item links), we don’t need to retrieve them every time during the initial page load. They can be cached.

The easiest options to cache data using JavaScript is to use either cookies or the browser local storage. In this situation and because we don’t need to send data to a server, the local storage option is the most appropriate choice for caching. Notice that not all browsers support this feature. Navigation nodes are stored in the local storage as a JSON string built by a custom method because of circular dependencies due to the input object tree structure (see the stringifyTreeObject() method in the utility.ts file).

The cache activation is controlled manually by a boolean value in a configuration item within a dedicated SharePoint list (one item for each language):

cache-control
Cache control

 

In my previous code sample in the PnP repository, I used a term set property to store this information. However, for a performance purpose, making a REST call during each page load to see if the cache is enabled is by far faster than loading multiple scripts using SOD mechanism and CSOM. Also, a configuration list allows to have a configurable solution and not « hard code » the source term set id for navigation menus in the master page or elsewhere.

One query to rule them all: the Pub/Sub pattern

You may have noticed that there is only one data source shared for the top, contextual and breadcrumb menus. Again, it is all about performance. For these menus, the behavior is similar to the default SharePoint one and use only one term set. The visibility of each navigation node is controlled by the following OOTB properties:

Node visibility
Node visibility

 

I used the publish/subscribe pattern to « sync » menus (via the PubSub JS library). The main menu component is in charge to retrieve all the navigation nodes and publish them to the contextual and breacrumb menu components which are just subscribers:

In the main menu component

...
 
// Publish the data to all subscribers (contextual menu and breadcrumb) via the "navigationNodes" topic key
PubSub.publish("navigationNodes", { nodes: navigationTree } );
 
...

 

In the contextual and breadcrumb menus
...
// Get the navigation nodes for the "navigationNodes" topic key
PubSub.subscribe("navigationNodes", (msg, data) => {
 
    // Filter navigation nodes based on their properties and visibility
    ...
 
}).catch((errorMesssage) => {
 
    pnp.log.write(errorMesssage, pnp.log.LogLevel.Error);
});
 
...

 

Useful things to know
  • With some refactoring (for example, define navigation term sets in the global term store instead of inside the site collection scope), you can use this solution to build a global shared navigation for your site.
  • In this sample, you can’t use friendly URL because of the solution is multilingual and there is only one single SharePoint site (more explanation in the next article about the multilingualism implementation). To be short, a navigation term set can only be associated with only one site.
  • There is no automatic process to « map » the navigation link of the term to the page URL when setting the « Site Map Position » field. The reason behind this is because I didn’t want to implement a dedicated SharePoint add-in (the ratio time/gain isn’t worth it in this case). When web hooks for lists and document libraries will come, maybe it could be interesting to automate this part.

Contact the Author: Franck Cornu
– LinkedIn: HTTPS://CA.LINKEDIN.COM/IN/FRANCKCORNU
– Twitter: @FRANCKCORNU
– Blog: HTTP://THECOLLABORATIONCORNER.COM/

Reference:
Cornu, F. (2016). Part 4: Office 365 Starter Intranet Solution (Part 4: The navigation implementation) [online] Available at: http://thecollaborationcorner.com/2016/08/31/part-4-the-navigation-implementation [Accessed 24 Feb. 2017].

Share this on...

Rate this Post:

Share: