Modern AEM can serve content in a headless manner across multiple channels, be they React or Vue apps, native mobile apps, or some other non-AEM platform. This is accomplished through AEM’s built-in headless services. Here are content fragments of importance that provide the building blocks for exposing content in a headless way. These work in tandem with the GraphQL API, enabling external channels to access the content.
A hybrid architecture is one in which your AEM project serves content via rendered HTL components (headful) and serves content as JSON, using AEM headless features.
When establishing or enhancing a brand, consistency is important! This post dives into the details of a hybrid use case in which a brand’s primary site navigation is used across the main AEM site and a React app. Thus, streamlining the usage of the brand’s logo and sharing of a common set of navigation elements. Customers across the main site and React app see a familiar look, with common navigation elements placed alongside channel-specific ones.
There are a lot of code and AEM configuration materials to cover, so this is divided into multiple parts. In part 1, we will discuss the setup of the HTL component, supporting services, and the content fragment model. Part 2 will take us outside of AEM to build the React app which consumes the navigation JSON. Without further delay, let’s get into it!
AEM Project and Local Instance
I generated my project using the AEM archetype for Maven. I used the default options. Note, we will not be deploying any of the React code to AEM (not using ui.frontend). To simulate a truly decoupled React app, we will be running it locally as a standalone app.
You should have at least a local AEM author available to follow along. For the AEM code, we’ll only be deploying to a local author.
AEM Code
Building the Component
HTL
The component HTL is simple, just a data-sly-use variable declaration for the model, a section for the logo, search and mobile nav button, a statically authored home button, and a generated list of navigation buttons. This uses Sling to build the links list from the site structure instead of manually creating each button link.
Two key items to note below.
The model variable declaration:
data-sly-use.navigation="com.perficient.pocs.core.models.Navigation"
The data-sly-list call using the model’s getNavLinks method:
<sly data-sly-list.link="${navigation.navLinks}"> <li class="site-menu-item"><a class="link" href="${link.path}.html">${link.title}</a></li> </sly>
Dialog
The dialog consists of four fields:
- An asset picker (configured with rootPath of /content/dam) for the Brand logo
- Home page picker (configured with rootPath of /content) for selecting the homepage
- Home page link text field for defining a friendly homepage link title
- Starting page path picker (configured with rootPath of /content) for defining the starting point in the site structure from which to generate the links.
There is also an extraClientlibs property with a string multi (array) value of nav.authoring, set on the cq:dialog node. Including this client library category is needed to include and execute JavaScript in an authoring-only client library and only for instances of this component.
Authoring Clientlib
The authoring client library consists of a single script, navJsonUpdater.js.
This script attaches a listener to the document.
$(document).on("click", ".cq-dialog-submit", function (e) {
When the click event is triggered, a call is made to retrieve the component instance properties as JSON. The authored logo path and navigation start path properties are extracted:
var componentPath = $(this).closest(".cq-dialog").attr("action"); type: 'GET', url: componentPath + ".json", success: function(data) { // Extract the logo and startPath property values from the JSON response var brandLogo = data["brandLogo"]; var startPath = data["startPath"];
Then a POST call is made to a navigationJsonUpdateServlet (more details on this below), with the logo and start path as request parameters.
type: 'POST', url: '/bin/navigationJsonUpdateServlet', data: { // Add the logo and startPath properties to the data object. brandLogo: brandLogo, startPath: startPath },
The servlet invokes the NavigationService’s getNavigationAsJson method to persist the navigation elements to a content fragment.
Building the Bundle/Sling classes
Navigation Component Model
The Navigation model utilizes the brandLogo and startPath dialog values to call the NavigationService’s getNavigation method. This generates a list of the pages for the component to render via data-sly-list.link=”${navigation.navLinks}” as mentioned above.
@PostConstruct protected void init() { navLinks = navigationService.getNavigation(request, startPath); } public List<NavigationItem> getNavLinks() { return navLinks; }
This model passes a request object to the navigation service to retrieve the page resources under the startPath.
Navigation Service
The NavigationService class interface is self-explanatory, so I will only briefly mention that it is exposing the get navigation methods.
Navigation Service Implementation
The NavigationServiceImpl class contains the getNavigation method. This method iterates over the first level child resources of the startPath’s value (as a Sling resource). It adds each child resource’s page title, page node name, and path to an array list, as a NavigationItem (more on this in a moment).
This class also contains the getNavigationAsJson method. It is similar to the getNavigation method, with the important distinction that the output is a JSON object containing an array of the same child resource properties.
The NavigationItem class is a simple pojo, defining the object properties of each item in the navigation array:
public NavigationItem(String title, String name, String path) { this.title = title; this.name = name; this.path = path; }
Navigation JSON Update Servlet
Finally, the bundle code is the NavigationJsonUpdateServlet. This servlet is invoked via the aforementioned navJsonUpdater.js script. The servlet writes the brand logo DAM path and JSON array of navigation items to a content fragment:
String brandLogo = ""; if (Objects.nonNull(request.getParameter("brandLogo"))) { brandLogo = request.getParameter("brandLogo"); } String startPath = request.getParameter("startPath"); ArrayNode navigationJsonArray = NavigationService.getNavigationAsJson(request, startPath); ResourceResolver resourceResolver = request.getResourceResolver(); String navFragmentPath = "/content/dam/perficientpocs/navigation-fragment-demo/jcr:content/data/master"; Resource navFragmentResource = resourceResolver.getResource(navFragmentPath); try { if (Objects.nonNull(navFragmentResource)) { ModifiableValueMap map = navFragmentResource.adaptTo(ModifiableValueMap.class); map.put("brandLogo", brandLogo); map.put("generatedNavigationList", navigationJsonArray.toString()); resourceResolver.commit(); }
Rendered AEM Component
With the component and service classes above, you should add your new navigation component to template policies, preferably to the allowed components list for an experience fragment (XF) template. Then you can add the component to a navigation XF for use across multiple pages in your site structure. For styling, I added a component client library with some CSS and JS (I will not cover their contents here, but they are available in the repository).
Here is what the component looks like in my test instance, when added to a page and authored:
Desktop View
Mobile Views
Hopefully, you won’t judge my design too harshly. Apart from the Home link, the rest of these links were generated by the component for me. All I had to do was provide a startPath!
(I also selected the logo image from the DAM).
The Content Fragment Model and…Fragment
You will want to create a content fragment model. The model should contain a content reference data type field for referencing the brand logo and a JSON object data type field for the navigation JSON.
Then you will need to create a content fragment using your new model.
The brand logo and navigation list fields can be left blank since they are populated via the servlet:
In the NavigationJsonUpdateServlet, be sure to update the navFragmentPath value with the path to the /jcr:content/data/master node for your fragment.
Closing Thoughts
Well, that was a lot of AEM code. At this point, you should have a component that generates navigation items for use on your AEM site and as a JSON object for use elsewhere. As mentioned, we’ll step outside of AEM for part 2, to build an app that consumes the content fragment and is automatically kept up to date when navigation elements change. More on that soon!