Blogs
Okay, that is a mouthful. To rephrase my compound title, we'll be talking about an approach that
- uses search to
- update a content item that
- follows a structure tailored to a
- template that renders a megamenu.
There! Thats the flow in a nutshell. If that already has the gears turning in your head, then you're welcome. Ta-ta! But if you need more, read on.
Note that I'm discussing a design here, resorting to low-level implementations only where needed. Undertanding the moving parts in this design/approach is the real value of this post.
What is a megamenu?
You've seen them. Megamenus are everywhere. A megamenu is basically a beautiful and useful kink in your navigation. Most navigation HTML provides a hierarchical menu reflecting your sitemap. But every so often, a business may want to break away from that mundaneness and give the user a rich navigational experience.
For a quick example, visit amazon.com and hover over any item under Shop by Department. Each of those is a megamenu.

Our Simple Course Megamenu
Consider the below megamenu which we will use as a case study for the purposes of this post.

It features 4 tabs. Each tab shows a list of courses arranged alphabetically. Clicking a course takes you to the course content page. All courses are Liferay journal articles of a custom subtype called Course.
The Megamenu Content Structure
More often than not, the information we display in a megamenu has a well-defined structure. For the above example, we can quickly define a content structure for our megamenu that would look something like this:
Tab (Text/Repeats) Course Letter (Text/Repeats) Course Title (Text) Course URL (Text)
Using the above structure, you could manually create all the content necessary for our courses megamenu. Sounds crazy! Right. And we don't want to do anything crazy! So hang in there.
The Megamenu Content Template
Here is the stripped-down megamenu template that uses the structure above to render the megamenu.
<div class="mega-dropdown-nav-container">
<ul class="nav nav-pills nav-justified block-light" role="tablist">
#foreach ($studyLevel in $txtQualificationLevel.getSiblings())
<li role="presentation">
<a data-target="#$studyLevel.txtIdentifier.getData()" aria-controls="#
$studyLevel.txtIdentifier.getData()" role="tab" data-toggle="pill"><strong>
$studyLevel.getData()</strong></a>
</li>
#end
</ul>
</div>
<div class="mega-dropdown-tab-container">
<div class="tab-content">
#foreach ($studyLevel in $txtQualificationLevel.getSiblings())
<div role="tabpanel" class="tab-pane $active" id="$studyLevel.txtIdentifier.getData()">
<div class="mega-dropdown-menu-inner">
#foreach ($letter in $studyLevel.txtLetter.getSiblings())
<ul>
<li class="dropdown-header">$letter.getData()</li>
#foreach ($course in $letter.txtCourseTitle.getSiblings())
<li><a href="/-/$course.txtFriendlyUrl.data">$course.getData()</a></li>
#end
</ul>
#end
</div>
</div>
#end
</div>
</div>
Theme Navigation Trigger
navigation.vm
#foreach ($nav_item in $nav_items)
#if ($velocityCount == 3) ### drop the COURSES megamenu item here
#parse ("$full_templates_path/megamenu.vm")
#end
...
megamenu.vm
What we have so far
- A megamenu content structure
- A megamenu content template
- A megamenu content item (journal article) tied to that structure and template
The Megamenu XSD
The Megamenu Content XML
<?xml version="1.0"?>
<root available-locales="en_US" default-locale="en_US">
<dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="0">
<dynamic-element name="txtIdentifier" index="0" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[all-courses]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="0" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="0" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="0" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-management]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Advanced Certificate in Management]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtCourseTitle" index="1" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="1" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-financial-planning]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Advanced Certificate In Financial Planning]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[A]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="1" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="2" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="2" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[bachelor-of-commerce-bcomm-]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Bachelor of Commerce (BCOM)]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[B]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[All Courses]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="1">
<dynamic-element name="txtIdentifier" index="1" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[undergraduates]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="2" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="3" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="3" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-management]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Advanced Certificate in Management]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[A]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Undergraduates]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="2">
<dynamic-element name="txtIdentifier" index="2" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[postgraduates]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="3" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="4" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="4" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[higher-certificate-in-management]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Higher Certificate In Management]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[H]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="4" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="5" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="5" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[master-of-business-administration-mba-]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Master of Business Administration (MBA)]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[M]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Postgraduates]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="3">
<dynamic-element name="txtIdentifier" index="3" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[ce]]></dynamic-content>
</dynamic-element>
<dynamic-element name="txtLetter" index="5" type="text" index-type="keyword">
<dynamic-element name="txtCourseTitle" index="6" type="text" index-type="keyword">
<dynamic-element name="txtFriendlyUrl" index="6" type="text" index-type="keyword">
<dynamic-content language-id="en_US"><![CDATA[postgraduate-diploma-in-banking]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Postgraduate Diploma in Banking]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[P]]></dynamic-content>
</dynamic-element>
<dynamic-content language-id="en_US"><![CDATA[Continuing education]]></dynamic-content>
</dynamic-element>
</root>
- The root element has one or more dynamic-element elements.
- Each dynamic-element can have one dynamic-content element and one or more dyamic-element elements.
- And recursion. That's it.
The XML Schema Definition
Putting Search to work
- Fire a search to bring back all content of type course
- Sort the courses
- Partition the courses into four buckets corresponding to the four tabs on the megamenu. We use a custom field called studyLevel for each course that serves as the discriminator for the tabs
- Organize the courses into the object model that is convertible to our Megamenu content XML document.
- Convert the object model to XML
- Retrieve the megamenu content item
- Update it with the XML
- Done.
So why even do it this way?
- The content that populates the megamenu is web content, so updating the megamenu is as simple as updating a content item. Happens on the fly.
- We wanted our megamenu to be extremely performant, so we preferred to just load a content item and thus reuse any Liferay caching that already services existing content
- We wanted a way to refresh the content on demand. E.g. fire a search and refresh the content item after adding one or more courses.
- We wanted to leverage Liferay's cotnent versioning so we can examine the content evolution of our megamenu and use as needed.
Some useful code snippets follow. I hope someone finds this approach helpful.
Useful Code Snippets
Faceted Search Configuration
{"facets": [
{
"displayStyle": "asset_entries",
"static": true,
"weight": 0.25,
"order": "OrderHitsDesc",
"data": {
"values": ["com.liferay.portlet.journal.model.JournalArticle"],
"frequencyThreshold": 0
},
"className": "com.liferay.portal.kernel.search.facet.AssetEntriesFacet",
"label": "asset-type",
"fieldName": "entryClassName"
},
{
"displayStyle": "asset_tags",
"weight": 0.25,
"static": true,
"order": "OrderHitsDesc",
"data": {
"values": ["course"],
"frequencyThreshold": 0
},
"label": "asset-type",
"className": "com.liferay.portal.kernel.search.facet.MultiValueFacet",
"fieldName": "type"
}
]}
Calling into the Search API
String searchConfiguration = portletPreferences.getValue("jsonFacetedSearchConfigurationString", StringPool.BLANK);
import com.liferay.portal.kernel.search.Document
import com.liferay.portal.kernel.search.SearchContext
import com.liferay.portal.kernel.search.QueryConfig
import com.liferay.portal.kernel.search.FacetedSearcher
import com.liferay.portal.kernel.search.facet.AssetEntriesFacet
import com.liferay.portal.kernel.search.facet.Facet
import com.liferay.portal.kernel.search.facet.ScopeFacet
import com.liferay.portal.kernel.search.facet.config.FacetConfiguration
import com.liferay.portal.kernel.search.facet.config.FacetConfigurationUtil
import com.liferay.portal.kernel.search.facet.util.FacetFactoryUtil
import com.liferay.portal.kernel.search.Field
import com.liferay.portal.kernel.search.Hits
import com.liferay.portal.kernel.search.Indexer
import com.liferay.portal.kernel.search.IndexerRegistryUtil
import com.liferay.portal.kernel.search.QueryConfig
import com.liferay.portal.kernel.search.SearchContext
import com.liferay.portal.kernel.search.SearchContextFactory
import com.liferay.portal.kernel.search.SearchResultUtil
List<Document> documents = null;
SearchContext searchContext = SearchContextFactory.getInstance(request);
searchContext.setAttribute("paginationType", "none");
QueryConfig queryConfig = new QueryConfig();
searchContext.setQueryConfig(queryConfig);
searchContext.setStart(1);
//searchContext.setEnd(); // I am hoping NOT setting this would work as paginationType is set to "none"
Facet assetEntriesFacet = new AssetEntriesFacet(searchContext);
assetEntriesFacet.setStatic(true);
searchContext.addFacet(assetEntriesFacet);
Facet scopeFacet = new ScopeFacet(searchContext);
scopeFacet.setStatic(true);
searchContext.addFacet(scopeFacet);
List<FacetConfiguration> facetConfigurations = FacetConfigurationUtil.load(undergraduateSearchConfiguration);
for (FacetConfiguration facetConfiguration : facetConfigurations) {
Facet facet = FacetFactoryUtil.create(searchContext, facetConfiguration);
searchContext.addFacet(facet);
}
Indexer indexer = FacetedSearcher.getInstance();
Hits hits = indexer.search(searchContext);
documents = hits.toList()

