Building a Dynamic Navigation Component in AEM using Sling Models and the Resource API
In our previous posts, we explored the basics of the Resource
ResourceResolver
In AEM, and even used the Resource API to list PDF files from a DAM folder using a simple servlet.
Now, let’s put that knowledge to practical use with a real-world component.
In this blog, we’ll walk through how to build a dynamic navigation component in AEM using Sling Models and the Resource API.
This component will allow authors to select a root page path and dynamically generate a list of child pages with their titles and links — no hardcoding, just clean, flexible logic.
Requirement
We want to build a component where authors can configure a starting path. The component will automatically display the titles and links of all child pages under that path.
If the author sets the root path as /content/my-site/en
The navigation will show all subpages, like
/content/my-site/en/about
/content/my-site/en/contact
/content/my-site/en/products
And it should work dynamically — no hardcoded values.
Implementation
1. Component Dialog
Create a dialog in your component with a path field
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Dynamic Navigation "
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<properties
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<rootPath
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Navigation Root Path"
name="./rootPath"
rootPath="/content"
required="true"/>
</items>
</column>
</items>
</columns>
</items>
</properties>
</items>
</tabs>
</items>
</content>
</jcr:root>
2. Sling Model interface.
package com.debug.code.core.models;
import java.util.List;
public interface NavigationModel {
String getRootPath();
List<NavigationItem> getNavigationItems();
class NavigationItem {
private final String title;
private final String path;
public NavigationItem(String title, String path) {
this.title = title;
this.path = path;
}
public String getTitle() {
return title;
}
public String getPath() {
return path;
}
}
}
3. Sling Model Implementation
package com.debug.code.core.models.impl;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.debug.code.core.models.NavigationModel;
import com.debug.code.core.models.NavigationModel.NavigationItem;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Model(
adaptables = Resource.class,
adapters = NavigationModel.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class NavigationModelImpl implements NavigationModel {
@ValueMapValue
private String rootPath;
@Inject
private ResourceResolver resourceResolver;
@Override
public String getRootPath() {
return rootPath;
}
@Override
public List<NavigationItem> getNavigationItems() {
List<NavigationItem> items = new ArrayList<>();
if (StringUtils.isNotBlank(rootPath)) {
PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
if (pageManager != null) {
Page rootPage = pageManager.getPage(rootPath);
if (rootPage != null) {
Iterator<Page> childPages = rootPage.listChildren();
while (childPages.hasNext()) {
Page child = childPages.next();
String title = StringUtils.defaultIfBlank(child.getTitle(), child.getName());
items.add(new NavigationItem(title, child.getPath()));
}
}
}
}
return items;
}
}
4. HTL Code
<sly data-sly-use.model="com.debug.code.core.models.NavigationModel">
<p>Root Path: ${model.rootPath}</p>
<p>Items Count: ${model.navigationItems.size}</p>
<ul data-sly-list="${model.navigationItems}">
<li>
<a href="${item.path}">${item.title}</a>
</li>
</ul>
</sly>
How it Works
- We inject the
rootPath
from the dialog. - We adapt the
ResourceResolver
to aPageManager
- We fetch the root page using
pageManager.getPage(rootPath)
. - We iterate over its children and get their titles and paths.
- We return a list of
NavigationItem
objects, which is rendered in the HTL.
The upcoming blog will explore more of the Content Fragment and Headless Approach in detail.
Every great discussion starts with a simple thought! If you enjoyed this article, found it useful, or have any questions, let’s talk! I’d love to hear from you.
For more updates, tips, and engaging conversations, connect with me on Medium, LinkedIn, and RealCodeWorks. Let’s keep learning together! 🚀✨
Thank you 🙏 !