Navigation Objects and Custom Helpers
This module looks at two of the most useful tools you have when you need to generate markup at publish time in Terminalfour (T4). The first is the Navigation Object: a configurable, publish-time generator that T4 uses to build navigation output — breadcrumb trails, link menus, site maps and the like — from the structure of your site. The second is the Custom Helper: a reusable Handlebars helper, a small JavaScript function you write once and then call from your layouts to produce or transform output.
By the end of the module you will be able to say clearly what each mechanism is, decide which one fits a given job, and put both into practice through two short, guided exercises — one that composes two Navigation Objects into a single navigation region, and one that writes a Custom Helper which adapts its output to the section being published.
This material is written for T4 administrators who are already comfortable building and configuring content. If you have defined a Content Type — the set of fields that make up a content item — and worked with a Page Layout, the template that wraps content items to produce a finished page, you are in the right place. We build on that footing rather than re-explaining it, and we keep the tone practical: concepts first, then hands-on practice.
Concepts
Both mechanisms run on the server while T4 publishes your site, and both can contribute markup to a page. Where they differ is in what they know about and how you drive them. This section defines each one and then draws the line between them.
What is a Navigation Object?
A Navigation Object is a server-side generator, configured in T4's Navigation Objects area, whose job is to walk the site structure and emit navigation markup during a publish. You do not write its logic line by line; instead you pick a type — such as breadcrumbs, a link menu, or a site map — and configure its behaviour and output through the object's settings. Once configured, the object is identified by a numeric id, and you reference that id from a layout (or invoke it through the platform) to drop its generated markup into a page.
The key idea is that a Navigation Object is structure-aware: it derives its output from where a section sits in the tree and how sections relate to one another. That is exactly what makes it the natural choice for anything that answers "where am I?" or "where can I go from here?".
What is a Custom Helper?
A Custom Helper is a reusable Handlebars helper you author as a JavaScript function and register in T4, then call by name from a layout. You can call it from a Content Layout — the template attached to a Content Type that renders one content item — or from a Page Layout, which wraps the page. In both, a helper lets you factor out logic — formatting, conditionals, lookups — so the layout stays readable and the logic lives in one place.
A helper's contract is a function of the form function(context, options). It reaches T4's data and services through the Publish API — the platform interface exposed to helper code through the apis object (for example apis.getSection() or apis.getNavigation()). Two further ideas matter for the exercises ahead. A block expression is a helper called with an inner body, written {{#helperName}}…{{/helperName}}, so the helper can decide whether and how to render the content between its tags. And the section context is the information about the section currently being published — its name, its depth in the tree, its parent — which a helper reads through the Publish API to make that kind of decision.
When to use each
Reach for a Navigation Object when the output is navigation derived from site structure and you want it configured rather than coded. Breadcrumbs, section menus, sitemaps and next/previous links are all a good fit: the object already knows how to traverse the tree, and configuration is quicker and more maintainable than hand-written traversal.
Reach for a Custom Helper when you need custom logic or transformation inside a layout — conditional wrapping, formatting a field, computing a value, or shaping output based on the section context. A helper gives you the full expressiveness of JavaScript and the Publish API, which a configured object cannot offer.
- Structure-driven navigation, configured not coded: use a Navigation Object.
- Custom logic, conditionals, or transformation in a layout: use a Custom Helper.
- Both at once: a helper can even invoke a Navigation Object through the Publish API, which is exactly what the first exercise builds on.
Decision criteria
Concepts drew the line in principle; this section turns that line into a decision you can make quickly at your desk. When you are about to build a piece of publish-time markup, work through the questions below in order and stop at the first clear answer.
Ask these questions in order
- Is the output navigation derived from site structure? If you are producing breadcrumbs, a section menu, a sitemap, or next/previous links — anything whose shape comes from where sections sit in the tree — lean towards a Navigation Object. It already knows how to walk the tree, so you configure rather than code.
- Can a configured object produce it without custom logic? If a Navigation Object type covers the job with its own settings, prefer it: configuration is faster to build and easier for the next administrator to maintain than hand-written traversal.
- Do you need a decision, a transformation, or a computed value? If the output depends on conditionals, formatting, lookups, or the section context — for example "wrap this block only on top-level sections" — reach for a Custom Helper. A helper gives you the full expressiveness of JavaScript and the Publish API, which a configured object cannot offer.
- Do you need to reuse the same logic across many layouts? A Custom Helper is written once and called by name from any Content Layout or Page Layout, which keeps that logic in one place.
Rules of thumb
- Structure-driven navigation, configured not coded: use a Navigation Object.
- Custom logic, conditionals, or transformation in a layout: use a Custom Helper.
- Both: the two are not mutually exclusive. A Custom Helper can invoke a Navigation Object through the Publish API, letting you wrap or post-process configured navigation with your own logic — the pattern the first exercise builds on.
Comparison
The table below sets the two mechanisms side by side across the attributes that usually decide a choice. Read down a column to characterise one mechanism, or across a row to see how they differ on a single attribute.
| Attribute | Navigation Object | Custom Helper |
|---|---|---|
| What it is | A configured, publish-time generator that emits navigation markup from the site structure. | A reusable Handlebars helper — a JavaScript function you author and register in T4. |
| How you build it | By choosing a type and adjusting its settings; little or no code. | By writing JavaScript with the signature function(context, options). |
| Primary purpose | Structure-aware navigation: breadcrumbs, menus, sitemaps, next/previous. | Custom logic and transformation: conditionals, formatting, computed output. |
| Input it works from | The site tree and the relationships between sections. | Parameters plus any platform data reached through the Publish API, including the section context. |
| How you invoke it | By its numeric id, referenced from a layout or through the Publish API. | By name from a Content Layout or Page Layout, optionally as a block expression. |
| Flexibility | Bounded by the type's configuration options. | Open-ended — the full expressiveness of JavaScript and the Publish API. |
| Maintenance | Adjusted through settings; approachable for any administrator. | Maintained as code; needs someone comfortable editing JavaScript. |
Advantages and disadvantages
Neither mechanism is strictly better; each trades away something for what it gives you.
- Navigation Object — advantage: it is configured rather than coded, so structure-aware navigation is quick to build and easy for the next administrator to maintain.
- Navigation Object — disadvantage: it is bounded by its type's settings, so anything beyond the shapes the type supports is out of reach without another mechanism.
- Custom Helper — advantage: it offers the full expressiveness of JavaScript and the Publish API, so it can implement logic, transformations, and section-context decisions a configured object cannot.
- Custom Helper — disadvantage: it is code you own and must maintain, which asks more up front and calls for someone comfortable writing and debugging JavaScript.
Exercise 1 — Combining two Navigation Objects
Time to put Concepts to work. In this first exercise you write a small Custom Helper — call it combineNav — that processes two separate Navigation Objects and merges their output into a single wrapped block. This is useful for stitching together, say, a "primary nav" and a "utility nav" into one navigation region. It is also something the native single-object navigation reference cannot do on its own, because that only handles one Navigation Object per call. Recall from Concepts that a Navigation Object is a publish-time, structure-aware generator you drive by its numeric id, and that a Custom Helper can invoke one through the Publish API with apis.getNavigation().process(id).
The two objects you will combine
Both of these are ready-made in the training instance; you are composing them, not building them from scratch:
- T201 Training Site Example: Left/Side Link Menu — id
259, typelink-menu. This serves as the primary nav: links across the current branch to child and sibling sections. - T201 Training Site Example: Breadcrumbs — id
260, typebreadcrumbs. This serves as the utility nav: the trail from the current page back up to the homepage.
The helper takes each object's id as a named parameter, processes each one independently, and merges the two resulting HTML strings into one wrapper.
The helper — complete the two TODOs
Register a new Custom Helper and start from the code below. It is almost complete: two small TODO markers are left for you to finish. Both parameters come in as named (hash) parameters via options.hash(name), and each Navigation Object is processed on its own with apis.getNavigation().process(id).
function (context, options) {
// TODO (1): read the "primaryId" named parameter, mirroring the utility line below.
var primaryNavId = /* TODO: options.hash("primaryId") */;
var utilityNavId = options.hash("utilityId");
if (!primaryNavId || !utilityNavId) {
log.warn("combineNav helper requires both 'primaryId' and 'utilityId' parameters");
return "";
}
try {
// TODO (2): pass primaryNavId into process(...), mirroring the utility line below.
var primaryHtml = apis.getNavigation().process(parseInt(/* TODO: primaryNavId */, 10));
var utilityHtml = apis.getNavigation().process(parseInt(utilityNavId, 10));
return "<nav class=\"site-nav\">"
+ "<div class=\"utility-nav\">" + utilityHtml + "</div>"
+ "<div class=\"primary-nav\">" + primaryHtml + "</div>"
+ "</nav>";
} catch (e) {
log.error("Error processing navigation objects: " + e);
return "";
}
}
Using it in a template
Call the helper by name, passing each Navigation Object's id as a named parameter. Here the Left/Side Link Menu (259) is the primary nav and Breadcrumbs (260) is the utility nav:
{{combineNav primaryId="259" utilityId="260"}}
Both Navigation Object ids are passed in as named parameters, each is processed independently via apis.getNavigation().process(id), and the two resulting HTML strings are merged into one wrapped structure — something the native single-object navigation reference cannot do on its own, since it only handles one Navigation Object per call.
Steps
- In your instance, open the Navigation Objects list and confirm the two objects and their ids: Left/Side Link Menu (
259, the primary nav) and Breadcrumbs (260, the utility nav). - Create a new Custom Helper named
yournameCombineNavusing the signaturefunction (context, options)and paste in the code above. - Complete TODO (1): read the primary id with
options.hash("primaryId"), mirroring theutilityNavIdline just below it. - Complete TODO (2): pass
primaryNavIdintoapis.getNavigation().process(parseInt(…, 10)), mirroring theutilityHtmlline. - Call the helper from a Content Layout or Page Layout with
{{combineNav primaryId="259" utilityId="260"}}. - Publish a section within the branch and inspect the output: confirm both navigations render together inside the one
site-navwrapper.
Expected published outcome
When you publish, the page shows a single <nav class="site-nav"> wrapper containing two blocks: a utility-nav holding the breadcrumb trail and a primary-nav holding the link menu of the current branch's child and sibling sections. Seeing both navigations merged inside one wrapper — produced by a single helper call — tells you the two Navigation Objects have been combined correctly. If either named parameter is missing, the helper logs a warning and returns an empty string rather than breaking the publish.
Exercise 2 — A conditional-wrapping Custom Helper based on section context
In the second exercise you write a Custom Helper that adapts its output to the section being published. The goal is a sectionAwareNav helper that processes one Navigation Object and then chooses the wrapping markup based on where the current page sits in the site structure — for example, applying a different class to top-level sections than to deeper ones. The layout call stays the same everywhere; the helper reads the section context and decides how to wrap.
Recall the helper contract
From Concepts, a Custom Helper is a JavaScript function with the signature function(context, options). You read a named parameter with options.hash(name), and you reach any T4 data or service through the Publish API apis object. Wrapping the apis calls in try/catch, logging failures with log.error, and returning a safe empty string on error keeps a helper from ever breaking a publish.
Introducing section context
What makes this helper context-aware is that it uses apis.getSection() — the Publish API's window onto the section currently being published. This exercise reads the current section and then keys off its level: a small level number means a shallow, prominent section near the top of the tree; a larger one means a section buried deeper in a branch. The accessors that matter here are:
apis.getSection().get()— resolves the current section object being published.currentSection.exists()— confirms the section resolved, so you can fall back gracefully if it did not.currentSection.getLevel()— the section's depth in the tree, where the channel root / top level is a small number and each step down increases it.
Because the helper knows the current section, the very same layout call can produce different wrapping markup on different pages — something a configured Navigation Object cannot decide on its own.
The helper — complete the three TODOs
Register a new Custom Helper and start from the code below. It is almost complete: three small TODO markers are left for you to finish. The navigation id comes in as a named parameter, the current section is read through apis.getSection(), and the wrapper class is chosen from the section's level.
function (context, options) {
// TODO (1): read the "id" named parameter with options.hash(...).
var navId = options.hash(/* TODO: "id" */);
if (!navId) {
log.warn("sectionAwareNav helper requires an 'id' parameter");
return "";
}
try {
var navHtml = apis.getNavigation().process(parseInt(navId, 10));
// TODO (2): resolve the current section being published via the Publish API.
var currentSection = apis.getSection()./* TODO: get() */;
if (!currentSection.exists()) {
log.warn("Current section could not be resolved");
return navHtml;
}
// TODO (3): read the current section's depth in the tree.
var level = currentSection./* TODO: getLevel() */;
var wrapperClass = level <= 2 ? "nav-top-level" : "nav-sub-level";
return "<div class=\"" + wrapperClass + "\">" + navHtml + "</div>";
} catch (e) {
log.error("Error processing section-aware navigation: " + e);
return "";
}
}
Using it in a template
Call the helper by name, passing the Navigation Object's id as a named parameter:
{{yournameSectionAwareNav id="259"}}
The helper processes the Navigation Object once, then inspects the current section through apis.getSection() and wraps the output in nav-top-level for shallow sections (level 2 or less) or nav-sub-level for deeper ones — all from a single, unchanging layout call.
Steps
- Create a new Custom Helper named
yournameSectionAwareNavusing the signaturefunction (context, options)and paste in the code above. - Complete TODO (1): read the navigation id with
options.hash("id"). - Complete TODO (2): resolve the current section with
apis.getSection().get(). - Complete TODO (3): read the section's depth with
currentSection.getLevel(). - Call the helper from a Content Layout or Page Layout with
{{yournameSectionAwareNav id="259"}}(or any Navigation Object id you like). - Publish sections at different depths and compare the output to confirm the wrapper class switches on section level.
Expected published outcome
When you publish, a top-level section (level 2 or less) wraps the navigation in <div class="nav-top-level">, while a section deeper in the branch wraps the same navigation in <div class="nav-sub-level">. The wrapper class is chosen entirely from the section context read through apis.getSection(), with no change to the layout call between pages. If the id parameter is missing the helper logs a warning and returns an empty string, and if the current section cannot be resolved it returns the unwrapped navigation rather than breaking the publish.
Summary
You now have both tools in hand and a sense of when each one earns its place. Before you close the module, here is the distinction in a nutshell, followed by the points worth carrying into your own work.
The distinction, in brief
A Navigation Object is configured, not coded. It is a structure-aware, publish-time generator: you pick a type — breadcrumbs, a link menu, a site map — adjust its settings, and reference it by id from a layout. It shines whenever the output is navigation derived from where sections sit in the tree, and it stays easy for the next administrator to maintain because there is no custom logic to read.
A Custom Helper is code you own. It is a reusable Handlebars function with the signature function(context, options) that you register once and call by name from a Content Layout or Page Layout. It reaches T4's data and services through the Publish API apis object, so it can make decisions, transform values, and read the section context — the kind of custom logic a configured object cannot offer. That power comes with the cost of maintaining JavaScript.
Choosing between them
- Reach for a Navigation Object when the output is structure-driven navigation you want configured rather than coded.
- Reach for a Custom Helper when you need conditional logic, transformation, or a computed value inside a layout — for example wrapping navigation based on the section context.
- Combine them when neither alone is enough: a Custom Helper can invoke a Navigation Object through the Publish API, letting you wrap or post-process configured navigation with your own logic.
Key takeaways
- The two mechanisms are complementary, not competing — the right choice follows from the job, not from preference.
- Exercise 1 showed that Navigation Objects compose: a
combineNavCustom Helper can process two Navigation Objects through the Publish API and merge their output into one wrapper — something the native single-object navigation reference cannot do on its own. - Exercise 2 showed that a Custom Helper can adapt output to the section context — a
sectionAwareNavhelper reads the current section withapis.getSection()and picks the wrapper class from the section's level, so the same layout call renders differently on top-level and deeper sections. - Whatever you build, keep helper code defensive: read platform data only through the
apisobject, wrap it intry/catch, and log failures withlog.errorso a helper never breaks a publish.
With the concepts, the decision criteria, and both exercises behind you, you are ready to decide between a Navigation Object and a Custom Helper on a real task — and to reach for both together when that is what the job needs.
Sorting with Custom Helpers
A practical guide for Terminalfour administrators. This manual assumes you are already comfortable creating and editing Content Types, Content Layouts, and Page Layouts, and that you have written or edited at least one Custom Handlebars Helper before. If Custom Helpers are new to you, review the "Navigation Objects and Custom Helpers" module first, then come back here.
What you'll learn
- Why sorting belongs in the Custom Helper, not in your markup
- How JavaScript's
Array.prototype.sort()and comparator functions work - Three hands-on exercises: an alphabetical sort, a date sort, and sorting into a custom (non-alphabetical) order
- The small gotchas that trip people up: default string sorting and date parsing
Why sort inside a Custom Helper?
Handlebars is deliberately logic-light. It has no built-in "sort this list" expression, and that is by design: templates are for presentation, not data manipulation. When you need a list rendered in a particular order (news by date, events by date, links A to Z), the clean place to do that work is a Custom Helper, where you have the full Publish API and plain JavaScript at your disposal.
The pattern is always the same three steps:
- Get your data (an array of items, usually from the Publish API via
apis). - Sort the array with
.sort()and a comparator that describes the order you want. - Build and return the output string.
The one rule of comparators
Every sort is driven by a comparator function that receives two items, a and b, and returns a number:
- a negative number means "a comes before b"
- a positive number means "a comes after b"
- zero means "leave their order unchanged"
Get that return value right and every kind of sort - text, number, date - follows the same shape. The exercises below each build one comparator.
A quick warning about the default sort. Calling .sort() with no comparator converts everything to strings and compares character codes. That is why [10, 2, 1].sort() gives you [1, 10, 2] - "10" sorts before "2" because the character "1" comes before "2". Uppercase letters also sort before lowercase ones. Always pass an explicit comparator.
Exercise 1 - Alphabetical sort (text)
Your first task is the most common sorting job of all: take the content items in a section and list them alphabetically by one of their elements. Rather than hard-code the order into the helper, you will pass in which element to sort on and which direction, so the same helper works for any content type.
Imagine a content type called Programme that already exists in your instance, with a Heading element (and others such as Name, Description). A section contains several Programme items. You want to output them A to Z by Heading. Because Heading is text, the reliable comparison tool is String.prototype.localeCompare(), which handles casing and accented characters correctly - far better than the < and > operators.
The helper reads two hash parameters so the call site stays in control:
element- the name of the element to sort on (for exampleHeading)order-ascfor A to Z,descfor Z to A
Create a Custom Helper named sortByElement and paste the code below. Complete the three TODOs. Each has its solution directly beneath it, commented out - uncomment those lines to reveal the answer.
function (context, options) {
// Read the call-site parameters. {{{sortByElement element="Heading" order="asc"}}}
var elementName = options.hash("element");
var order = options.hash("order") || "asc";
if (!elementName) {
log.warn("sortByElement: no 'element' parameter was passed");
return "";
}
// Get the content items in the current section (a Java list).
var contentList = apis.getSection().listContent();
// TODO 1: Build a plain JavaScript array of items, each holding the sort
// value (the chosen element, as text) and the heading to display.
// Read an element's text value with:
// apis.getContent().get(id).getElement(elementName).toTextElement().process()
// -------------------------------------------------------------------
// SOLUTION 1 (uncomment the block below):
// var items = [];
// for (var i = 0; i < contentList.length; i++) {
// var contentItem = apis.getContent().get(contentList[i].getId());
// var value = contentItem.getElement(elementName).toTextElement().process();
// items.push({ value: value, item: contentItem });
// }
// -------------------------------------------------------------------
// TODO 2: Sort the array alphabetically by value, honouring the order
// parameter. localeCompare with { sensitivity: "base" } gives a
// human-friendly A to Z; flip the sign when order is "desc".
// -------------------------------------------------------------------
// SOLUTION 2 (uncomment the block below):
// items.sort(function (a, b) {
// var result = a.value.localeCompare(b.value, undefined, { sensitivity: "base" });
// return order === "desc" ? -result : result;
// });
// -------------------------------------------------------------------
// TODO 3: Build an HTML list of the sorted values and return it.
// -------------------------------------------------------------------
// SOLUTION 3 (uncomment the two lines below):
// var rows = items.map(function (x) { return "<li>" + x.value + "</li>"; });
// return "<ul>" + rows.join("") + "</ul>";
// -------------------------------------------------------------------
// Remove this line once you have completed the TODOs above.
return "Not implemented yet";
}
Call it from a layout with:
{{{sortByElement element="Heading" order="asc"}}}
Expected result: the section's Programme items listed A to Z by their Heading value. Switch the call to order="desc" and the same list comes back Z to A - without touching the helper code.
What to notice:
- All the ordering logic lives in one comparator; the
orderparameter just flips its sign. That is the whole trick to making a sort reversible. - Element values from the Publish API arrive as text, so
localeCompareis exactly the right tool. The{ sensitivity: "base" }option keeps mixed-case headings in the order a human reader expects, rather than pushing lowercase entries to the end. - Because the element name is a parameter, the same
sortByElementhelper sorts byName,Heading, or any other text element - you decide at the call site.
Exercise 2 - Date sort (publish date / event date)
Now sort the section's content items by a Date element instead of a text one. Just like Exercise 1, you pass in which element to read and which direction to sort, so the same helper works for any content type that has a date field.
Sorting by date is the same comparator pattern as sorting text, with one extra step: you must turn each date value into something numeric before you compare. The easiest approach is to build a JavaScript Date from the element's value and call .getTime(), which gives you a millisecond number. Subtract those and you have your comparator.
Reading a Date element from the Publish API is slightly different from reading text: use getElement(name).toDateElement().getDate(). That returns the date value, and calling .toString() on it gives you a string you can hand to new Date(...).
The helper reads two hash parameters:
element- the name of the Date element to sort on (for exampleStart Date)order-ascfor soonest first,descfor most recent first
Create a Custom Helper named sortByDate and complete the three TODOs.
function (context, options) {
// Read the call-site parameters. {{{sortByDate element="Start Date" order="asc"}}}
var elementName = options.hash("element");
var order = options.hash("order") || "asc";
if (!elementName) {
log.warn("sortByDate: no 'element' parameter was passed");
return "";
}
// Get the content items in the current section (a Java list).
var contentList = apis.getSection().listContent();
// TODO 1: Build a plain JavaScript array of items, each holding the date value
// as a JS Date, plus a label to display. Read a Date element with:
// apis.getContent().get(id).getElement(elementName).toDateElement().getDate()
// Call .toString() on that value and pass it to new Date(...).
// For the label, read the item's Name element as text.
// -------------------------------------------------------------------
// SOLUTION 1 (uncomment the block below):
// var items = [];
// for (var i = 0; i < contentList.length; i++) {
// var contentItem = apis.getContent().get(contentList[i].getId());
// var rawDate = contentItem.getElement(elementName).toDateElement().getDate();
// var label = contentItem.getElement("Name").toTextElement().process();
// items.push({ date: new Date(rawDate.toString()), label: label });
// }
// -------------------------------------------------------------------
// TODO 2: Sort the array by date, honouring the order parameter.
// Subtract the two getTime() values for ascending (soonest first),
// and flip the sign when order is "desc".
// -------------------------------------------------------------------
// SOLUTION 2 (uncomment the block below):
// items.sort(function (a, b) {
// var result = a.date.getTime() - b.date.getTime();
// return order === "desc" ? -result : result;
// });
// -------------------------------------------------------------------
// TODO 3: Build an HTML list. Show each label next to its date.
// -------------------------------------------------------------------
// SOLUTION 3 (uncomment the two lines below):
// var rows = items.map(function (x) { return "<li>" + x.label + " - " + x.date.toDateString() + "</li>"; });
// return "<ul>" + rows.join("") + "</ul>";
// -------------------------------------------------------------------
// Remove this line once you have completed the TODOs above.
return "Not implemented yet";
}
Call it from a layout with:
{{{sortByDate element="Start Date" order="asc"}}}
Expected result: the section's items listed by their Start Date, soonest first. Switch the call to order="desc" and the most recent date comes first instead - without touching the helper code.
What to notice:
- Never compare dates with subtraction until they are numbers.
new Date(...).getTime()is what turns a date into a millisecond value the comparator can subtract. - A Date element is read with
toDateElement().getDate(), nottoTextElement(). That is the one difference from Exercise 1; everything else - the parameters, the reversible comparator, the list output - is the same shape. - As in Exercise 1, the
orderparameter simply flips the comparator's sign, so one helper covers both ascending and descending.
Exercise 3 - Custom order (reordering a content report's "Content" column)
The first two exercises put things in a calculated order - A to Z, or earliest date. But sometimes there is no rule to calculate; you simply want a specific, hand-picked order that suits how the content reads. This exercise tackles exactly that, using a real report helper.
The helper below builds a table of every content item in a section. Its Content column lists each element as Name of the element: value. Those element names come from contentItem.getElements(), which hands them back in alphabetical key order - so the column always reads like this:
Academic Program: ...
Date: ...
Description: ...
End Date: ...
Heading: ...
Main body: ...
Name: ...
Start Date: ...
Video: ...
Alphabetical is rarely the order a reader wants. "Name" and "Heading" probably belong at the top; "Start Date" should sit next to "End Date", not be separated by "Main body". The fix is to sort the keys array into an order you define before the loop that prints them.
The technique: keep an array that lists the element names in your preferred order, then sort keys by each key's index in that array. Comparing those index numbers is just a numeric sort - subtract one position from the other, exactly the a - b shape the comparator rule describes.
Paste the helper below into a new Custom Helper (for example contentReport), point it at a section that uses a content type with these elements, and complete the TODOs. The report-building TODOs (1-22) are already filled in so you can focus on the sorting TODOs, labelled S1 and S2.
function (context, options) {
//10. Add a new column labeled "Content".
let contentReport = `<table class="table-striped">
<thead>
<tr>
<th>Content Id</th>
<th>Content Type Id</th>
<th>Expiry Date</th>
<th>Last Modified Date</th>
<th>Publish Date</th>
<th>Version</th>
<th>Content</th>
</tr>
</thead>`;
//1. Get the content as a list by providing the section Id.
let contentList = apis.getSection().listContent(context);
// S1. TODO: Define the order you want the elements to appear in.
// List the element names exactly as they appear (spaces and casing matter).
// Any element NOT in this list will be handled by the comparator in S2.
// -------------------------------------------------------------------
// SOLUTION S1 (uncomment the block below):
// const elementOrder = [
// "Name",
// "Heading",
// "Description",
// "Main body",
// "Academic Program",
// "Start Date",
// "End Date",
// "Date",
// "Video"
// ];
// -------------------------------------------------------------------
for (let i = 0; i < contentList.length; i++) {
//2. Get the content Id.
let contentId = contentList[i].getId();
//3. Get the Content Item.
let contentItem = apis.getContent().get(contentId);
//4. Get the Content Type Id.
let contentTypeId = contentItem.getContentTypeId();
//5. Get the expiry date.
let expiryDate = contentItem.getExpiryDate();
//6. Get the last modified date.
let lastModified = contentItem.getLastModified();
//7. Get the publish date.
let publishDate = contentItem.getPublishDate();
//8. Get the version.
let version = contentItem.getVersion();
//11. Get the elements.
let elements = contentItem.getElements();
//12. Convert the keys of the elements to an array and store them in keys.
const keys = elements.keySet().toArray();
// S2. TODO: Sort the keys into your preferred order (from S1) instead of alphabetical.
// Sort by each key's position in elementOrder. indexOf returns the position,
// or -1 when the key is not listed. Subtracting the two positions is a plain
// numeric sort.
// Tip: the || fallback below keeps any unlisted elements in alphabetical order
// at the end, instead of jumping to the front because indexOf returned -1.
// -----------------------------------------------------------------
// SOLUTION S2 (uncomment the block below):
// keys.sort(function (a, b) {
// let posA = elementOrder.indexOf(a);
// let posB = elementOrder.indexOf(b);
// if (posA === -1) posA = elementOrder.length;
// if (posB === -1) posB = elementOrder.length;
// return (posA - posB) || a.localeCompare(b);
// });
// -----------------------------------------------------------------
//Variable used to store the content.
let content = '';
//Iterate through the elements in the (now sorted) key order.
for (let j = 0; j < keys.length; j++) {
//Look the element up by its key so it stays paired with the sorted order.
let element = elements.get(keys[j]);
//14. Check if the element is a date element.
if (element.isDateElement()) {
//15. Get the value of the Date element. Format: <p>Key: Value</p>
content += `<p>${keys[j]}: ${element.toDateElement().getDate().toString()}</p>`;
}
//16. Check if the element is a text element.
else if (element.isTextElement()) {
//17. Get the value of the text element. Format: <p>Key: Value</p>
content += `<p>${keys[j]}: ${element.toTextElement().process()}</p>`;
}
//19. Check if the element is a list element.
else if (element.isListElement()) {
//20. Get the list entries.
let entries = element.toListElement().getValue().getEntries();
//Variable used to store the selected item in the list.
let selected;
//21. Iterate through the list entries and store the selected value.
for (let k = 0; k < entries.length; k++) {
if (entries[k].isSelected()) {
selected = entries[k].getValue();
}
}
//22. Get the value of the list element. Format: <p>Key: Value</p>
content += `<p>${keys[j]}: ${selected}</p>`;
}
}
//18 + 9. Build the report row by inserting the variables.
contentReport += `<tr>
<td>${contentId}</td>
<td>${contentTypeId}</td>
<td>${expiryDate}</td>
<td>${lastModified}</td>
<td>${publishDate}</td>
<td>${version}</td>
<td>${content}</td>
</tr>`;
}
contentReport += `</table>`;
return contentReport;
}
Call it from a layout with (pass the section Id you want to report on):
{{{contentReport 8536}}}
Expected result: before you complete S1 and S2, the Content column reads alphabetically - Academic Program, Date, Description, End Date, Heading, Main body, Name, Start Date, Video. After uncommenting both solutions it follows your list instead: Name, Heading, Description, Main body, Academic Program, Start Date, End Date, Date, Video.
What to notice:
- The two nested loops now share the sorted
keysarray, and we fetch each element withelements.get(keys[j])so the value always matches the key we are printing. The original code readvalues[i]by position, which would fall out of step the moment you reorder the keys - this is the small but important change that makes sorting safe here. indexOfdrives the whole thing: it turns "where should this element go" into a number, and once you have numbers you are back to an ordinary numeric comparator.- The
|| a.localeCompare(b)fallback means you do not have to list every element name. Anything you leave out simply lands at the end in alphabetical order, so the report never breaks if a new element is added to the content type later.
Taking it further
All three exercises read live content from the Publish API and sort it with a comparator you control. The comparator is always the part that decides the order - swap it out and the same helper produces a different arrangement. Here is the Exercise 1 shape again for quick reference, sorting the current section's items alphabetically by their Name element:
function (context, options) {
// 1. Get the content items in the current section.
var list = apis.getSection().listContent();
// 2. Map each one to a small object holding just what you need to sort and display.
var items = [];
for (var i = 0; i < list.length; i++) {
var item = apis.getContent().get(list[i].getId());
var title = item.getElement("Name").toTextElement().process();
items.push({ title: title, raw: item });
}
// 3. Sort with the same comparator pattern you practised above.
items.sort(function (a, b) {
return a.title.localeCompare(b.title, undefined, { sensitivity: "base" });
});
// 4. Build and return your markup.
var rows = items.map(function (x) { return "<li>" + x.title + "</li>"; });
return "<ul>" + rows.join("") + "</ul>";
}
Change the comparator and you change the order - everything else stays the same.
Quick reference
- Text, A to Z:
a.localeCompare(b, undefined, { sensitivity: "base" }) - Date, soonest first:
new Date(a).getTime() - new Date(b).getTime() - Custom order:
orderArray.indexOf(a) - orderArray.indexOf(b)(guard the -1 case) - Reverse any sort: negate the comparator result (as the
orderparameter does), swapaandb, or call.reverse()afterwards - Never rely on a bare
.sort()- always pass a comparator