Whats all the hubbub about!? You may have heard that Microsoft has recently deployed Office 365 Hub sites to targeted release tenants! Hub sites are a new way to organize and join your existing SharePoint sites into a centralized hub to allow for rolling up content, sharing themes between sites and maintaining a consistent navigation across multiple sites. This post will focus entirely on the shared navigation that lives on the hub site and why I think it needs a little bit of improvements in it's current state.
What's the problem anyway?
First let's take a look at what we mean by hub site navigation.
This is hub site navigation. This is a team site that exists within a hub called "Intranet Hub". At the hub level, i can specify any navigation I want and every site will receive the same shared navigation elements! However, the real issue is that these are just static links. What that means is that there is no security trimming and end users who are actively inside your hub sites will see navigation whether or not they have access to the target link locations.
The problem with this, is that you may have multiple sites within your hub that have isolated permissions and your end user may receive 404 error messages. However, this shouldn't stop you from keeping these sites in the same hub, especially if you want shared branding and security trimmed roll up of content. In order to build security trimmed navigation, we will need to do a little bit of custom development using the SharePoint Framework (SPFx).
Before we start
Before we start, let's talk about where we will be getting our data to implement this new navigation. Office 365 hub sites do have a REST API. We still start by using this following API endpoint
/_api/web/HubsiteData
If the site page we are currently on is connected to a hub site, this call should return some properties about the hub site. The real issue I have with this endpoint, is that it doesn't provide the ID of the hub, so we'll have to get that using another query (if there is a better approach for getting the ID of the hub, I'd love to know). In any case, we'll be using the following endpoint to get the hub ID.
/_api/HubSites?$filter=SiteUrl eq '<hubsiteurl>'
We'll take the URL that comes from the previous call and use it to find the associated hub site object. The result of this call will give us the ID (GUID) of the Hub.
This GUID is very important and we'll be using it with the SharePoint Search API to find all other sites related to this hub. In SharePoint, there is a managed property for this ID and it's called DepartmentId. The Search API is what gives us security trimming because it executes on behalf of the current logged in user. Any sites that are returned, are only sites the user has access to.
Using this managed property, we'll be able to execute a query through the search API to find all sites associated with this DepartmentId (hub site).
/_api/search/query?querytext='DepartmentId:{e9ec1fb4-d316-4e96-bd8a-96d76ca26a5f} contentclass:STS_Site'&selectproperties='Title,Path,DepartmentId'
Setting up the solution
We'll be building a SharePoint Framework Application Customizer which will sit in place of the existing hub site navigation (or on top, your choice!). For this project, I am creating a new project based from an existing customizer built by community efforts which can be found on GitHub. I'm not going to show you every piece of code because frankly that would be hard to explain in a blog post, however, this application customizer is available for download at the end of this blog.
"The important stuff"
While I am not showing all of the code, I'd like to talk about the important pieces of functionality in the source code.
Application Customizer onInit function
The onInit function is the code that does all of the heavy lifting for our application customizer. It is in charge of creating the search service, putting the menu in cache, as well as rendering out the menu items on our hub site. The code flows as follows
- Set up caching
- Create new search service
- Use Search Search to query if current site is in a hub site
- If it is, does it exist in session cache?
- If not in cache, get a new site listing
public async onInit(): Promise {
//set up caching of the menu
pnp.setup({
defaultCachingStore: "session",
defaultCachingTimeoutSeconds: 900,
globalCacheDisable: false
});
//create a new search service passing in the current site url
let searchService: SPSearchService.SPSearchService = new SPSearchService.SPSearchService({
spHttpClient: this.context.spHttpClient,
siteURL: this.context.pageContext.web.absoluteUrl
});
//figure out if this site belongs in a hub, if not, return null
this._currentHubSiteData = await searchService.getHubSiteData().then((hubData: IAssociatedSite) => {
if(hubData){
return searchService.getHubID(hubData.url).then((hub: IHubSiteData) => {
return hub;
});
}
else{
return null;
}
});
//if site is not in a hub, forget it and do nothing
// otherwise, see if the current hubsite ID is in session storage
// if not in cache, let's find all sites in our hub site
if (this._currentHubSiteData != null) {
let cachedMenu = pnp.storage.session.get("HUBNAV_" + this._currentHubSiteData.ID);
if (cachedMenu != null) {
this._currentHubSiteData = cachedMenu;
}
else {
this._currentHubSiteData.Sites = await searchService.getSitesInHub(this._currentHubSiteData.ID);
pnp.storage.session.put("HUBNAV_" + this._currentHubSiteData.ID, this._currentHubSiteData);
}
this._renderPlaceHolders();
}
return Promise.resolve();
}
Search Service Functions
getHubSiteData
This function queries the current site to figure out if it belongs to a hub.
public async getHubSiteData(): Promise {
let url = this.siteURL + "/_api/web/HubSiteData";
return this.spHttpClient.get(url, SPHttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': '',
}
}).then((response: SPHttpClientResponse) => {
return response.json();
}).then((responseJSON: IGetAssociatedSite) => {
let responseItem:IAssociatedSite;
if(responseJSON.value){
responseItem = JSON.parse(responseJSON.value);
}
return responseItem;
});
}
getHubID
There are probably better ways of doing this, but I am querying for all hub sites where the URL matches my current hub. You'll notice I am setting the Title and URL of this hub site as it will be used as the root navigation element for my listing of sites.
public async getHubID(hubURL: string): Promise {
let url = this.siteURL + "/_api/HubSites?filter=SiteUrl eq '" + hubURL + "'";
return this.spHttpClient.get(url, SPHttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': '',
}
}).then((response: SPHttpClientResponse) => {
return response.json();
}).then((responseJSON: IGetHubSiteData) => {
let result: IHubSiteData = {
Title: "Sites",
URL: "#",
ID: null,
Sites: []
};
var responseItems = responseJSON.value
if (responseItems.length > 0) {
result.ID = responseItems[0].ID
}
return result;
});
}
getSitesInHub
Once I have found the Hub ID, I can use this to query all sites in my environment that are associated with the hub, using the DepartmentId managed property.
public async getSitesInHub(hubID: string): Promise<ihubsitedata[]> {
let url = this.siteURL + "/_api/search/query?querytext='DepartmentId:{" + hubID + "} contentclass:STS_Site'&selectproperties='Title,Path,DepartmentId,SiteId'";
return this.spHttpClient.get(url, SPHttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': '',
}
}).then((response: SPHttpClientResponse) => {
return response.json();
}).then((responseJSON: ISearchResults) => {
let result: IHubSiteData[] = [];
var responseItems = responseJSON.PrimaryQueryResult.RelevantResults.Table.Rows;
for (let site of responseItems) {
//filter out hubsite root
if (site.Cells[6].Value != hubID) {
result.push({
ID: null,
URL: site.Cells[3].Value,
Title: site.Cells[2].Value,
Sites: []
});
}
}
return result;
});
}
Rendering out the menu
Now that we have officially found sites within our hub site using search, all we need to do is render out the menu item. We are going to be passing our result (this._currentHubSiteData) from our onInit function to a new Office UI Fabric React component that renders out a CommandBar Control. The component loops through all the sites in my _currentHubSiteData object and renders out links in the command bar.
The End Result
If you've made it this far, we should be able to render out a navigation that looks like this. Underneath our static hub navigation, we have a listing of security trimmed sites that belong in the "Intranet Hub" site.
My apologies if this post is a bit hard to follow as I struggle to show the important information that makes all of this functionality possible. I will be updating the code on GitHub to support adding the existing Hub Navigation elements into the navigation bar. That way we can still set navigation components manually to supplement this implementation.
A big thanks to the community for building out the SharePoint Framework example which allowed me to create this without re-inventing the wheel.
Github: https://bit.ly/2JbNM2Z
Share