I'm trying to build a site map for my site collections, let's call it for the purpose of this topic a secondary navigation menu.

Think of the "You are here" type of map, with the structure of a table of content, root site is 1, sub sites are 1.1, a sub site of the sub site 1.1.1, like this:

Root (Site collection)
- Subsite 1
- - Subsite 1.1
- - - Subsite 1.1.1
- Subsite 2
- - Subsite 2.1
- - - Subsite 2.1.1
- - - Subsite 2.1.2

This can be achieved easily with a web part called Table of Contents, exactly as intended with the one problem: it is based on CQWP and it slows to a crawl -pun intended-. In Sharepoint On Premises it would probably be almost imperceptible but on SPO it can make waiting for it to load a modem connection from the 90s.

Is there a way to build a table of contents using CSWP?

  • Are you looking to include all site collections on this map, or a single site collection? Could you use the REST API? Do you think that would be faster?
    – S. Walker
    Commented Aug 9, 2017 at 23:20
  • Thank you for your reply. I'm looking for a per site collection map, so each of the primary ones will have one. Sadly I haven't been able to dedicate time to development and I'm not familiar with the REST API. The good thing about using content search CSWP (As per my research, and its recommended everywhere versus using content query CQWP), is that there is a crawler caching all of the information, so you get it almost instantly without doing an actual call to crawl (Which makes CQWP so slow).
    – Yugo
    Commented Aug 10, 2017 at 13:11

2 Answers 2


I built this using a CSWP and a custom Display Template. In CSWP you have to restrict the search to only return Sites (Query string = "contentclass:STS_Web" ) and sort the results either by Url or SPSiteUrl.

Then you create a custom Display Template (simplest way is to copy an modify the file Item_Site.html in MasterPage Gallery -> Display Templates -> Search.) Let's call it Item_Site_Nav.html. Then edit the Item_Site_Nav.html-list item's properties and add 'UrlDepth':UrlDepth' to the Managed Properties. And lastly set this Display Template in the CSWP Display Settings.

My modified Item_Site_Nav.html looks like this. I think it could be stripped some more.

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"> 
<title>Website for Navigation</title>

<!--[if gte mso 9]><xml>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:MasterPageDescription msdt:dt="string">Website for navigation.</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#SearchResults;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
<mso:ManagedPropertyMapping msdt:dt="string">&#39;Title&#39;:&#39;Title&#39;,&#39;Path&#39;:&#39;Path&#39;,&#39;SiteLogo&#39;:&#39;SiteLogo&#39;,&#39;SiteDescription&#39;:&#39;SiteDescription&#39;,&#39;CollapsingStatus&#39;:&#39;CollapsingStatus&#39;,&#39;DocId&#39;:&#39;DocId&#39;,&#39;HitHighlightedProperties&#39;:&#39;HitHighlightedProperties&#39;,&#39;FileExtension&#39;:&#39;FileExtension&#39;,&#39;ViewsLifeTime&#39;:&#39;ViewsLifeTime&#39;,&#39;deeplinks&#39;:&#39;deeplinks&#39;,&#39;importance&#39;:&#39;importance&#39;,&#39;FileType&#39;:&#39;FileType&#39;,&#39;UrlDepth&#39;:&#39;UrlDepth&#39;</mso:ManagedPropertyMapping>
<mso:CrawlerXSLFile msdt:dt="string"></mso:CrawlerXSLFile>
<mso:HtmlDesignPreviewUrl msdt:dt="string"></mso:HtmlDesignPreviewUrl>
<mso:HtmlDesignConversionSucceeded msdt:dt="string">True</mso:HtmlDesignConversionSucceeded>
<mso:HtmlDesignStatusAndPreview msdt:dt="string">http://spoint.netze-duisburg.loc/_catalogs/masterpage/Display Templates/Search/Item_SiteIndentation.html, Konvertierung erfolgreich.</mso:HtmlDesignStatusAndPreview>
    <div id="Item_Site">
        if(!$isNull(ctx.CurrentItem) && !$isNull(ctx.ClientControl)){
          var urlDepth = ctx.CurrentItem.UrlDepth;
          var indentation = ''; //'<span>-&emsp;</span>'.repeat(urlDepth);
          for (i=0; i<urlDepth; i++) {
            indentation = indentation + "<span>&emsp;</span>";
      <div>_#= indentation =#_<a href="_#= ctx.CurrentItem.Path =#_">_#= ctx.CurrentItem.Title =#_</a></div>


The indentation span-element can be replaced with whatever CSS/html you like.

Don't forget to publish your custom display template.

Cheers, Ben

  • This is working almost like a charm, there are some things I need to fix on the search side but it's definitely what I was looking for. Thank you very much Ben.
    – Yugo
    Commented Aug 18, 2017 at 14:07
  • 1
    You're welcome. And thank you for the idea of using CSWP as navigation. Hadn't thought of that before.
    – ben
    Commented Aug 18, 2017 at 14:13
  • I've set up the search query, the targets, everything is ok so far, but I'm trying to sort them -hopefully through URL- and I can't seem to figure out why it's not sorting, they stay on the "relevant results", ascending or descending and everything stays the same, do you think I may be missing something? I've also tried deleting the JSON sort but I'm unable to get it working.
    – Yugo
    Commented Aug 18, 2017 at 19:47
  • 1. Are you configuring the CSWP in simple or advanced mode? (I used advanced mode). 2. I forgot to mention to disable "Hide duplicates". 3. Any possibility of posting screenshots of the WP configuration?
    – ben
    Commented Aug 18, 2017 at 20:08
  • 1. Advanced, the query is good and it brings everything, the only problem is that even when I give the sorting parameters in the CSWP it's still being sorted by the JSON preference. 2. I figured out the hide duplicates, didn't know why it was only showing half the sites. 3. For testing purposes I'm using the same HTML code you gave in the solution.
    – Yugo
    Commented Aug 21, 2017 at 13:13

I'm not sure about content search webpart, but I've done something similar using javascript (CSOM). With this approach, you can use browser storage (ie. session storage, cookies, etc.) to store the navigation data so repeat loads require no overhead on either the server nor the client. It helps with performance issues assuming more than 1 page load on average.

Here's the code to get immediate subsites... you could expand this further to get the full navigation structure. It's security trimmed as well.

            var ctx = new SP.ClientContext.get_current();
            var siteData = ctx.get_web().getSubwebsForCurrentUser();
            ctx.executeQueryAsync(function (sender, args) {
                var enumerator = siteData.getEnumerator();
                var sites = [];
                while (enumerator.moveNext()) {
                    var site = enumerator.get_current();
                    sites.push({ title: site.get_title(), url: site.get_url(), description: site.get_description() });
                //do something with sites, jquery up some divs or bind with angular, knockout, etc.
            }, function (sender, args) {
  • How would you handle the comment area? I'm not a developer, I can understand what the code does but I don't know enough to finish it up.
    – Yugo
    Commented Aug 15, 2017 at 14:50
  • This might not be the right direction for you to take then. My apologies. While you may be able to fumble your way through it using google and whatnot, it's not going to be quite as simple as even the above code is. That was just to give you an idea... the actual code would involve a few more advanced concepts such as storing local data, recursively loading all subsites, etc.
    – Chad
    Commented Aug 15, 2017 at 15:58
  • Thank you very much Chad, even if this doesn't give the full answer it's still very useful to have for anyone going this way to see.
    – Yugo
    Commented Aug 16, 2017 at 14:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.