Proper Solution: Using Apex + LWC
Step 1: Create Apex Controller
NavigationMenuController.cls
apex
public with sharing class NavigationMenuController {
@AuraEnabled(cacheable=true)
public static List<NavigationMenuItemWrapper> getNavigationMenuItems(String menuName) {
List<NavigationMenuItemWrapper> menuItemsList = new List<NavigationMenuItemWrapper>();
try {
// Get the current site's Network ID
String networkId = Network.getNetworkId();
// Query NavigationLinkSet to get the menu
List<NavigationLinkSet> navLinkSets = [
SELECT Id, MasterLabel
FROM NavigationLinkSet
WHERE MasterLabel = :menuName
AND NetworkId = :networkId
LIMIT 1
];
if (navLinkSets.isEmpty()) {
return menuItemsList;
}
// Query NavigationMenuItem records
List<NavigationMenuItem> menuItems = [
SELECT Id, Label, Target, Type, Position,
DefaultListViewId, AccessRestriction, ParentId
FROM NavigationMenuItem
WHERE NavigationLinkSetId = :navLinkSets[0].Id
ORDER BY Position ASC
];
// Convert to wrapper objects
for (NavigationMenuItem item : menuItems) {
NavigationMenuItemWrapper wrapper = new NavigationMenuItemWrapper();
wrapper.id = item.Id;
wrapper.label = item.Label;
wrapper.target = item.Target;
wrapper.type = item.Type;
wrapper.defaultListViewId = item.DefaultListViewId;
wrapper.accessRestriction = item.AccessRestriction;
wrapper.parentId = item.ParentId;
menuItemsList.add(wrapper);
}
} catch (Exception e) {
throw new AuraHandledException('Error fetching navigation: ' + e.getMessage());
}
return menuItemsList;
}
// Wrapper class to return navigation items
public class NavigationMenuItemWrapper {
@AuraEnabled public String id;
@AuraEnabled public String label;
@AuraEnabled public String target;
@AuraEnabled public String type;
@AuraEnabled public String defaultListViewId;
@AuraEnabled public String accessRestriction;
@AuraEnabled public String parentId;
}
}
Step 2: Create LWC Component
customNavigation.js
javascript
import { LightningElement, wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import getNavigationMenuItems from '@salesforce/apex/NavigationMenuController.getNavigationMenuItems';
export default class CustomNavigation extends NavigationMixin(LightningElement) {
navItems = [];
error;
isLoading = true;
// Wire the Apex method
@wire(getNavigationMenuItems, { menuName: 'Default_Navigation' })
wiredNavItems({ error, data }) {
this.isLoading = false;
if (data) {
this.navItems = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.navItems = [];
console.error('Navigation error:', error);
}
}
handleNavigation(event) {
event.preventDefault();
const menuItemId = event.currentTarget.dataset.id;
const menuItem = this.navItems.find(item => item.id === menuItemId);
if (!menuItem) return;
// Handle different navigation types
let navigationConfig = this.buildNavigationConfig(menuItem);
this[NavigationMixin.Navigate](navigationConfig);
}
buildNavigationConfig(menuItem) {
// Handle different menu item types
switch(menuItem.type) {
case 'SalesforceObject':
return {
type: 'standard__objectPage',
attributes: {
objectApiName: menuItem.target,
actionName: 'home'
}
};
case 'InternalLink':
return {
type: 'comm__namedPage',
attributes: {
name: menuItem.target
}
};
case 'ExternalLink':
return {
type: 'standard__webPage',
attributes: {
url: menuItem.target
}
};
case 'Event':
// Handle custom event if needed
return null;
default:
// Default to named page
return {
type: 'comm__namedPage',
attributes: {
name: menuItem.target
}
};
}
}
get hasNavItems() {
return this.navItems && this.navItems.length > 0;
}
}
customNavigation.html
html
<template>
<nav class="nav-container">
<!-- Loading State -->
<template if:true={isLoading}>
<div class="loading">Loading navigation...</div>
</template>
<!-- Navigation Items -->
<template if:true={hasNavItems}>
<ul class="nav-list">
<template for:each={navItems} for:item="item">
<li key={item.id} class="nav-item">
<a
href="javascript:void(0);"
data-id={item.id}
onclick={handleNavigation}
class="nav-link">
{item.label}
</a>
</li>
</template>
</ul>
</template>
<!-- Error State -->
<template if:true={error}>
<div class="error-message">
<p>Unable to load navigation menu</p>
<p class="error-detail">{error.body.message}</p>
</div>
</template>
</nav>
</template>
customNavigation.css
css
.nav-container {
background-color: #f3f3f3;
padding: 1rem;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.nav-item {
display: inline-block;
}
.nav-link {
color: #0176d3;
text-decoration: none;
padding: 0.5rem 1rem;
display: block;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-link:hover {
color: #014486;
background-color: #e5e5e5;
border-radius: 4px;
}
.loading {
text-align: center;
padding: 1rem;
color: #706e6b;
}
.error-message {
background-color: #feded8;
border-left: 4px solid #c23934;
padding: 1rem;
color: #3e3e3c;
}
.error-detail {
font-size: 0.875rem;
margin-top: 0.5rem;
color: #706e6b;
}
customNavigation.js-meta.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightningCommunity__Default">
<property name="menuName" type="String" default="Default_Navigation"
label="Navigation Menu Name"
description="API Name of the navigation menu to display"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Setup Instructions:
- Find Your Navigation Menu API Name:
- Go to Setup → Digital Experiences → Settings → Navigation Menus
- Find your menu and note the API name (e.g.,
Default_Navigation)
- Deploy the Apex class with test coverage
- Deploy the LWC component
- Add to Experience Builder:
- Drag the component onto your page
- Configure the
menuNameproperty to match your navigation menu
This solution properly queries the NavigationMenuItem object through Apex and handles different navigation types correctly!