Skip to main content


Dev Tutorial: Building a Shared Navigation With Help from AEM Headless Services | Part 2

Building Shared Navigation With AEM Content Services

In part 1 of this series, I walked through the setup of a simple navigation component (HTL). This component’s model uses a service to provide a list of navigation link elements. This results in the author only needing to select a starting path, without having to add each link manually. What’s more, we set up the component so that upon saving the dialog, a servlet is invoked to write the generated navigation links to a content fragment (along with other authored fields).  

Here in part 2, we will step outside of AEM and see how we can use this content fragment in a React navigation component. The goal is to have shared navigation elements between the AEM site and React app.  

You can find the project code here. 

React Project Setup

This is a NodeJS app, so it needs to be installed along with NPM (at least version 5.2.0 or newer). I wanted this to be a minimal setup, so npx was used to create the project as it’s now bundled with npm. If this were a project destined for production, I would advise setting this up using vite (a top-choice build tool for React and other related framework-based projects). 

With NodeJS and NPM available, the following should be run from a terminal/CLI:  

npx create-react-app aem-navigation-app
cd aem-navigation-app

Axios is used to retrieve the headless navigation content from AEM. I would usually go with the built-in fetch function for this, but Axios has some advantages for the use case, namely its automatic data conversion to JSON and a simpler response object. React-router-dom is used to render the response as links. React-router-dom and Axios were installed within the project via: 

npm –i -S react-router-dom axios

Let’s look at the app and navigation component, file by file. 

React Code

Building the Component


When the project is initialized, the id in the div container for the app is “root”. This is too generic for my taste. It was changed to “nav-container”. 

<div id="nav-container"></div>

The title tag value was also changed. If you build this, you may want to change meta tags and other elements. 


Changing the id in the HTML requires corresponding changes in the index script. I changed references from root to container. Importantly the value in document.getElementById to “nav-container”. 

const container = ReactDOM.createRoot(document.getElementById('nav-container')); 



    <App /> 




AEM content services data may be accessed by multiple components. It’s ideal to put the request code into a dedicated utility function. 

A folder was created in the root of the project called “utils”, in that folder the client.js file contains a function for making calls to AEM. It accepts 3 parameters, an endpoint, any headers that need to be passed, and a boolean indicating if the request includes credentials or not. 

import axios from 'axios'; 


export async function getTheData(endpoint, customHeaders = {}, credentials) { 

    try { 

            const response = await axios.get(endpoint, { 

                headers: { 



                withCredentials: credentials, 



        } catch (error) { 

             throw new Error(`Client could not retrieve data: ${error.message}`); 



The endpoint passed to this function points to a GraphQL persisted query, stored (cacheable) in AEM (http://localhost:4502/graphql/execute.json/perficientpocs/get-logo-and-nav-links). This follows the best practice of storing and running GraphQL queries service side. The client app simply makes a request for the content. Here is the query: 


navigationCfModelList(filter: {_variation: {_expressions: [{value: "master", _ignoreCase: true}]}})  


items { 

brandLogo { 

                ... on ImageRef { 








An authentication header is also passed to the getTheData function, this header should not be necessary for content that is publicly exposed through dispatcher and CDN layers. It’s only needed for this sample app. 


A folder was created in the root of the project called “components”. In that folder, the Navigation.js file contains the navigation component. This leverages the useState and useEffect hooks to set state variables related to the AEM request and setup that request.  

It uses the utility function in client.js from above and then renders the response by mapping the navigation elements from the response JSON to list elements: 

const result = await getTheData(endpoint, headers, true); 


…{[0] => ( 

     <li className="app-menu-item" key={}> 

<Link to={item.path} className="app-menu-link" style={{ textDecoration: 'none' }}>{item.title}</Link> 



Note, that this uses React Router Link elements instead of anchor elements. 

Along with the navigation links, the logo is retrieved from the AEM service response and rendered in the app header. 


Everything comes together in the main app script. The Navigation component is included, and the shared links are rendered alongside app-specific ones:



            <Navigation /> 



Other files 

I added some component CSS and an Article component just to add some sample content to our app.  

Rendered React App

 Here is what the app looks like in my local instance: 

Local Rendered React App Instance

If you read part 1 of this series, this should look familiar. As mentioned, the app uses the same logo as the navigation component in the AEM site. Also, see that the shared navigation elements are available in line with the app’s specific links: 

Shared Navigation Components

When the logo or any of the page links are updated in AEM and published, if the navigation component is also updated and published, the app navigation component is updated automatically: 

Shared Navigation Components Updates With AEM

Wins and API considerations

With this setup, the following have been achieved. 

  1. AEM content is used across channels 
  2. Consistent branding is maintained across the website and app 
  3. App-specific elements are rendered cohesively with shared elements 

Much of the work to enable the AEM service is done for us, via out-of-the-box JSON serialization.  

Any good service API provides a stable contract indicating how it should be used, and how content should be requested. This is important to consider when building out content fragment models.  

For a project like the one in this series, changing fragment model structures down the road may break existing content in consuming channels, as the service response JSON would also change. For this reason, careful thought should be given early in a project to the desired state of its information architecture. 

Take time to build out content models and schemas based on them before implementing them in components. It will promote a better, well-planned, and executed project. 

Closing Thoughts

This concludes the series on shared navigation. My goal was to highlight the power of AEM content services in a practical manner. I hope it was inspiring and sparked some ideas for how to share content across channels. I encourage you to share those ideas in the comments! 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Josiah Huckins

I have years of experience in web application support and development, with emphasis on the AEM Framework and Java. I like to write about anything AEM related (Sling, OSGi, the JCR, Dispatcher, etc.).

More from this Author

Follow Us