When it comes to browsing a website, the navigation plays a crucial role in ensuring that users can easily find what they're looking for. In many content management systems the content is organised in a hierarchical content tree and a common pattern is to derive the navigational structure from that content tree. This pattern is observed in some popular CMSs such as Umbraco, Sitecore, and Optimizely.
The hierarchical content tree offers an advantage by resembling the familiar folder structure on computers. This resonates with many content editors who find this form of organising content more intuitive. The navigational structure is also "out of the box" – that is a double-edged sword. On one hand the navigational structure can simply be derived from the content tree, but on the other hand not all content in the CMS should be visible in the website's navigation.
How to implement this pattern
If you are in a headless environment, you need to create API endpoints that allow your frontend to fetch the navigation data. These endpoints can return the data in JSON format, making it easy for the frontend to consume.
On the frontend, use JavaScript to fetch the hierarchical content data from the API endpoints. You can then use JavaScript logic to build the navigation structure dynamically. This might involve iterating through the data and generating HTML elements based on the hierarchical relationships.
Consider implementing caching mechanisms to store the navigation data on the frontend. This reduces the need to fetch the data from the server repeatedly, improving performance. As you add, update, or remove content items, ensure that the cache is kept in sync or invalidated.
How to implement with Enterspeed
The navigation can, for instance, consist of all your pages or pages with a property called "Include in navigation" that you created in your CMS.
To do this, you must create two schemas: One for the individual navigation item and one for the entire navigation.
Let's start with the individual navigation item.
Schema: Navigation item
1// navigationItem schema with action to reproces parent navigationItem
2
3/** @type {Enterspeed.FullSchema} */
4export default {
5 triggers: function(context) {
6 return context.triggers('cms', ['contentPage'])
7 },
8 action: function(context) {
9 return context.reprocess('navigationItem').parent()
10 },
11 properties: function ({url, properties: p}, context) {
12 return {
13 label: p.pageTitle,
14 href: url,
15 sortOrder: p.metaData.sortOrder,
16 children: context.reference("navigationItem").children()
17 }
18 }
19}
A lot is happening here, so let's split the code up and look at it one block at a time.
First, we specify the source groups you want this schema to trigger on. A source group should contain one or more source entity types (array). In our example, we simply choose the type "contentPage", which refers to our "normal pages" and not our blog posts, products, etc.
1// Trigger that processes the contentPage type from the cms source group
2
3triggers: function(context) {
4 return context.triggers('cms', ['contentPage'])
5}
Next, we define our actions. Actions are used when a new view has been generated from a schema. It defines which specific actions to take following the newly generated view. In our case, we tell it to trigger the "process" of another schema.
1// Action to reprocess the parent navigationItem
2
3action: function(context) {
4 return context.reprocessParent().schema('navigationItem')
5}
Next, we have the properties object. The properties object is where we map all the data.
1// Mapping of properties needed in the view
2
3properties: function ({url, properties: p}, context) {
4 return {
5 label: p.pageTitle, // string
6 href: url, // string
7 sortOrder: p.metaData.sortOrder // number
8 children: context.reference("navigationItem").children() // array
9 }
10}
Schema: Navigation
Now it's time to create the navigation schema, which is a collection of navigation items.
1// navigation schema with a list of root navigationItems
2
3/** @type {Enterspeed.FullSchema} */
4export default {
5 triggers: function(context) {
6 return context.triggers('cms', ['home'])
7 },
8 routes: function(sourceEntity, context) {
9 return context.handle('navigation')
10 },
11 properties: function (sourceEntity, context) {
12 return {
13 navigationItems: context.reference("navigationItem").children()
14 }
15 }
16}
We start by setting up triggers. We use the same source group as before ('cms'), but for types, we use 'home'. The reason why we use 'home' is that it's the top level of the hierarchy in our CMS, where our 'contentPages' (that we used in the navigationItem schema) are its children.
In order to reprocess these, we need to set up a trigger on its parent, which in this case is 'home'.
Then we define what the route should be for the view generated. Since this is a component on our website, we use a handle (instead of a URL), which we call 'navigation'.
Last but not least, we define the data that should be available in the generated view. In the properties object, we return a property called navigationItems, which is a reference to our single navigation items.
Now we have created a navigation, which dynamically updates based on the pages in our CMS.
You can make this even more advanced if you feel like it, e.g. by looking at a specific property (e.g., 'includeInNavigation') or only showing pages/products which meet a specific criteria that you set. Since the schemas are written in JavaScript, you are only limited by your own imagination.