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
Index.html
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.
Index.js
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')); container.render( <React.StrictMode> <App /> </React.StrictMode> );
Client.js
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: { ...customHeaders, }, withCredentials: credentials, }); return response.data; } 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 { _path } } generatedNavigationList } } }
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.
Navigation.js
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); …{data.data.navigationCfModelList.items[0].generatedNavigationList.map((item) => ( <li className="app-menu-item" key={item.id}> <Link to={item.path} className="app-menu-link" style={{ textDecoration: 'none' }}>{item.title}</Link> </li> ))}
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.
App.js
Everything comes together in the main app script. The Navigation component is included, and the shared links are rendered alongside app-specific ones:
<Router> <header> <Navigation /> </header> </Router>
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:
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:
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:
Wins and API considerations
With this setup, the following have been achieved.
- AEM content is used across channels
- Consistent branding is maintained across the website and app
- 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!