Hashmaps and arrays
Overview
Velocity provides two mutable collection types: lists (ordered sequences) and hashmaps (key-value pairs). In Cascade CMS, these are the primary tools for aggregating data across query results, grouping pages by category, building JSON output, and collecting filtered results—any scenario where you need to accumulate or organize more than one value.
- Create an empty list with bracket syntax or a pre-populated literal list.
-
Append items with
.add(), access by position with.get(0)or$list[0]. -
Check size with
.size()and iterate with#foreach. - Create a map with literal key-value pairs or start empty and populate it in a loop.
-
Add, update, and read values by key using
.put()and.get(). -
Iterate all entries with
#foreach ($entry in $map.entrySet()). -
Check for key existence with
.containsKey(). - Nest lists inside maps and maps inside lists for grouped data structures.
Lists
Lists are ordered, zero-indexed collections. Use them when you need to accumulate items, iterate a sequence, or collect filtered results from a query.
Creating a list
You can create a list with literal values or start empty.
## Empty list - growable, you can .add() to this
#set ($pages = [])
## Literal list with values
#set ($colors = ["red", "green", "blue"])
## Read from a literal list
$colors.get(0) ## red
$colors[1] ## green
$colors.size() ## 3
Adding items
.add("value") appends to the end of a
list and returns true. Suppress the
return value with $void.
#set ($titles = [])
#set ($void = $titles.add("Spring Newsletter"))
#set ($void = $titles.add("Fall Events"))
#set ($void = $titles.add("Annual Report"))
$titles.size() ## 3
Tip
Methods like .add() and .put() return a value—true for .add(), or the previous value for .put(). If you call them on their own, Velocity prints that return value straight into your page output. Wrapping the call in #set ($void = ...) captures the return into a throwaway variable so nothing is rendered. The variable name $void is just a convention—any name works—but it signals that you are intentionally discarding the result.
Collect titles from query results that match a condition:
#set ($featuredTitles = [])
#set ($results = $_.query().byContentType("News").preloadDynamicMetadata().maxResults(100).execute())
#foreach ($page in $results)
#if ($page.metadata.getDynamicField("featured").value == "Yes")
#set ($void = $featuredTitles.add($page.metadata.title))
#end
#end
## featuredTitles now contains only the featured page titles
Accessing by index
Two equivalent syntaxes. Use
.get() when the index is held in a
variable.
| Syntax | Example | Notes |
|---|---|---|
.get(n) |
$list.get(0) |
Works with variable index |
| Bracket notation | $list[0] |
Concise shorthand |
#set ($tags = [])
#set ($void = $tags.add("velocity"))
#set ($void = $tags.add("cascade"))
#set ($void = $tags.add("formats"))
$tags.get(0) ## velocity
$tags[1] ## cascade
$tags.size() ## 3
## Last item
#set ($last = $tags.size() - 1)
$tags.get($last) ## formats
Iterating a list
Standard #foreach works as expected.
Use $foreach.count for 1-based
numbering, $foreach.index for 0-based,
and $foreach.hasNext for comma
separation or last-item logic.
#set ($sections = [])
#set ($void = $sections.add("Introduction"))
#set ($void = $sections.add("Methods"))
#set ($void = $sections.add("Results"))
<ol>
#foreach ($section in $sections)
<li>$section</li>
#end
</ol>
Comma-separated output:
#set ($tags = ["velocity", "cascade", "formats"])
#foreach ($tag in $tags)$tag#if ($foreach.hasNext), #end
#end
## Output: velocity, cascade, formats
Build a filtered navigation list from
$_.query() results:
#set ($navPages = [])
#set ($results = $_.query().byContentType("e").bySiteName($currentPage.siteName).sortBy("name").sortDirection("asc").maxResults(50).execute())
#foreach ($page in $results)
#if ($page.metadata.getDynamicField("show-in-nav").value == "Yes")
#set ($void = $navPages.add($page))
#end
#end
<ul>
#foreach ($page in $navPages)
<li><a href="$page.link">$page.metadata.displayName</a></li>
#end
</ul>
Hashmaps
A hashmap stores key-value pairs. Velocity uses
Java’s LinkedHashMap under the
hood, so insertion order is preserved when you
iterate.
Creating a hashmap
You can create a map inline with values or start empty and populate it later.
## Empty map
#set ($map = {})
## Pre-populated map
#set ($labels = {
"news": "News",
"events": "Events",
"about": "About"
})
## Map with numeric values
#set ($counts = {
"published": 0,
"draft": 0,
"archived": 0
})
A common use case is a lookup table that maps machine names to human-readable labels:
## Map content type names to display labels
#set ($typeLabels = {
"news-article": "News",
"event-page": "Events",
"staff-bio": "People",
"press-release": "Press"
})
## Use the map inside a loop
#foreach ($page in $results)
#set ($label = $typeLabels.get($page.contentTypeName))
<span class="badge">$!label</span>
#end
Tip
Insertion order is preserved, so iterating a map you defined inline produces entries in the order you wrote them—useful for ordered navigation or display.
Adding and updating entries
.put("key", "value") adds a new entry
or overwrites an existing one. Because
.put() returns the previous value (or
null), wrapping it in
#set ($void = ...) suppresses that
output—this is the standard Cascade pattern.
#set ($map = {})
## Add a new entry
#set ($void = $map.put("theme", "blue"))
## Overwrite an existing entry
#set ($void = $map.put("theme", "green"))
## Variable as the key
#set ($key = "layout")
#set ($void = $map.put($key, "two-column"))
Accumulate page counts by section while walking query results:
#set ($sectionCounts = {})
#set ($results = $_.query().byContentType("Article").preloadDynamicMetadata().maxResults(-1).execute())
#foreach ($page in $results)
#set ($section = $page.metadata.getDynamicField("section").value)
## Get existing count, default to 0
#set ($current = $sectionCounts.get($section))
#if (!$current)
#set ($current = 0)
#end
## Compute the new value first, then put
#set ($n = $current + 1)
#set ($void = $sectionCounts.put($section, $n))
#end
Note
Arithmetic only works inside
#set, not inside method
arguments. Always compute the new value
first with
#set ($n = $old + 1), then call
.put() with $n.
Reading values
Three syntaxes exist for reading a value from a map.
Use .get() when the key is held in a
variable.
| Syntax | Example | When to use |
|---|---|---|
.get("key") |
$map.get("theme") |
Always works; required when key is a variable |
| Bracket notation | $map["theme"] |
Concise for literal string keys |
| Dot notation | $map.theme |
Concise, but fails for keys with hyphens or spaces |
#set ($config = {
"color": "blue",
"columns": "3",
"show-nav": "true"
})
## All three work for simple keys
$config.get("color") ## blue
$config["color"] ## blue
$config.color ## blue
## Required when the key is in a variable
#set ($field = "columns")
$config.get($field) ## 3
## Dot notation fails for hyphenated keys
## $config.show-nav ## interpreted as subtraction - wrong
$config.get("show-nav") ## true
$config["show-nav"] ## true
Note
Keys with hyphens (common in Cascade field
names like "show-nav") cannot
be accessed with dot notation. Use
.get() or bracket notation
instead.
Iterating a hashmap
Use
#foreach ($entry in $map.entrySet()) to
walk all entries. Each $entry has
.key and
.value properties. For keys only, use
.keySet(); for values only, use
.values().
#set ($nav = {
"Home": "/index",
"News": "/news/index",
"Events": "/events/index"
})
<ul>
#foreach ($entry in $nav.entrySet())
<li><a href="$entry.value">$entry.key</a></li>
#end
</ul>
To sort keys alphabetically, sort the key set first
with $_SortTool:
## Keys come back in insertion order by default
## Sort alphabetically with $_SortTool
#set ($sortedKeys = $_SortTool.sort($nav.keySet()))
<ul>
#foreach ($key in $sortedKeys)
<li><a href="$nav[$key]">$key</a></li>
#end
</ul>
Checking for keys
Use .containsKey("key") before reading
when the key might not exist. Accessing a missing
key returns null, which Velocity
renders as the literal variable reference text
unless you use $! to suppress it.
#set ($labelMap = {
"news": "News",
"events": "Events"
})
#set ($type = $currentPage.getStructuredDataNode("content-type").textValue)
#if ($labelMap.containsKey($type))
<span class="label">$labelMap.get($type)</span>
#else
<span class="label">$type</span>
#end
## Safe read with $! (outputs nothing if key is missing)
$!config.get("theme")
Quick reference
List operations
| Operation | Syntax | Notes |
|---|---|---|
| Empty list | #set ($list = []) |
Growable |
| Literal list |
#set ($list = ["a", "b"])
|
Pre-populated |
| Add item |
#set ($void =
$list.add("x"))
|
Appends; returns true
|
| Add all |
#set ($void =
$list.addAll($other))
|
Merges another list in |
| Read by index |
$list.get(0) or
$list[0]
|
Zero-based |
| Size | $list.size() |
|
| Iterate |
#foreach ($item in $list)
|
$foreach.hasNext,
$foreach.count
|
| Contains check |
$list.contains("x")
|
Returns boolean |
| Remove |
$list.remove($item)
|
|
| Sort |
$_SortTool.sort($list)
|
Returns new sorted list |
Hashmap operations
| Operation | Syntax | Notes |
|---|---|---|
| Empty map | #set ($map = {}) |
|
| Inline map |
#set ($map = {"k": "v"})
|
Preserves insertion order |
| Add / update |
#set ($void = $map.put("k",
"v"))
|
Returns old value; suppress with
$void
|
| Read (literal key) |
$map.get("k") or
$map["k"]
|
|
| Read (variable key) | $map.get($key) |
|
| Dot notation | $map.key |
Fails for hyphenated keys |
| Iterate entries |
#foreach ($e in
$map.entrySet())
|
$e.key,
$e.value
|
| Iterate keys only |
#foreach ($k in
$map.keySet())
|
|
| Check key exists |
$map.containsKey("k")
|
Returns boolean |
| Size | $map.size() |
Building collections in loops
The most common use of hashmaps and lists in Cascade is building them up inside a loop over query results, then rendering the collected data separately.
Using a list to collect filtered results
Filter results before rendering by accumulating matches into a list. This decouples data collection from presentation.
#set ($upcomingEvents = [])
#set ($now = $_DateTool.getDate())
#set ($results = $_.query().byContentType("Event").preloadDynamicMetadata().sortBy("startDate").sortDirection("asc").maxResults(100).execute())
#foreach ($page in $results)
#set ($startMillis = $page.metadata.startDate)
#if ($startMillis && $startMillis > 0)
#set ($startDate = $_DateTool.getDate($startMillis))
#set ($diff = $_DateTool.difference($now, $startDate))
#if ($diff.days >= 0)
#set ($void = $upcomingEvents.add($page))
#end
#end
#end
## Render after filtering
#if ($upcomingEvents.size() > 0)
<ul>
#foreach ($event in $upcomingEvents)
<li>$event.metadata.title</li>
#end
</ul>
#else
<p>No upcoming events.</p>
#end
Collect featured items from a query, then limit display to the first three:
#set ($featured = [])
#set ($all = $_.query().byContentType("News").preloadStructuredData().sortBy("startDate").sortDirection("desc").maxResults(100).execute())
#foreach ($page in $all)
#if ($page.getStructuredDataNode("is-featured").textValue == "Yes")
#set ($void = $featured.add($page))
#end
#end
## Show only the first 3 featured items
#foreach ($page in $featured)
#if ($foreach.count > 3)#break#end
<article>
<h3>$page.metadata.title</h3>
<p>$page.metadata.summary</p>
</article>
#end
Building a hashmap dynamically
Count pages per content type from a query:
#set ($typeCounts = {})
#set ($results = $_.query().byFolderPath("/news").bySiteName($currentPage.siteName).maxResults(-1).execute())
#foreach ($page in $results)
#set ($type = $page.contentTypeName)
## Get current count, default to 0
#set ($count = $typeCounts.get($type))
#if (!$count)
#set ($count = 0)
#end
## Increment and store
#set ($n = $count + 1)
#set ($void = $typeCounts.put($type, $n))
#end
## Output the breakdown
<ul>
#foreach ($entry in $typeCounts.entrySet())
<li>$entry.key: $entry.value</li>
#end
</ul>
Build a slug-to-page lookup map for fast retrieval later in the template:
## Build a map of slug -> page for O(1) lookup
#set ($pageBySlug = {})
#set ($results = $_.query().byContentType("Department").preloadStructuredData().maxResults(100).execute())
#foreach ($page in $results)
#set ($slug = $page.getStructuredDataNode("slug").textValue)
#set ($void = $pageBySlug.put($slug, $page))
#end
## Later: look up a page by slug without another query
#set ($dept = $pageBySlug.get("engineering"))
#if ($dept)
<a href="$dept.link">$dept.metadata.title</a>
#end
Nesting
You can nest lists inside maps and maps inside lists to create richer data structures. These patterns are the Velocity equivalent of JSON arrays of objects and grouped records.
List of hashmaps
Each item in the list is itself a map. This is the natural structure when collecting structured records—equivalent to an array of objects.
#set ($staff = [])
#set ($results = $_.query().byContentType("Staff-Bio").preloadStructuredData().sortBy("name").sortDirection("asc").execute())
#foreach ($page in $results)
#set ($person = {
"name": $page.getStructuredDataNode("last-name").textValue,
"title": $page.getStructuredDataNode("job-title").textValue,
"department": $page.metadata.getDynamicField("department").value,
"link": $page.link
})
#set ($void = $staff.add($person))
#end
## Render
<ul>
#foreach ($person in $staff)
<li>
<a href="$person.get('link')">$person.get('name')</a>
— $person.get('title')
</li>
#end
</ul>
Hashmap with list values (grouping pattern)
The most powerful nesting pattern: a map where each value is a list. Use it to group pages by department, content type, tag, or any categorical field.
## Group events by department
#set ($departments = ["College of Arts", "College of Sciences", "College of Engineering"])
## Initialize each department key with an empty list
#set ($grouped = {})
#foreach ($dept in $departments)
#set ($void = $grouped.put($dept, []))
#end
## Populate from query
#set ($events = $_.query().byContentType("Event").preloadDynamicMetadata().preloadStructuredData().maxResults(-1).execute())
#foreach ($event in $events)
#set ($dept = $event.metadata.getDynamicField("department").value)
#if ($grouped.containsKey($dept))
#set ($deptList = $grouped.get($dept))
#set ($void = $deptList.add($event))
#end
#end
## Render grouped output
#foreach ($entry in $grouped.entrySet())
#if ($entry.value.size() > 0)
<section>
<h2>$entry.key</h2>
<ul>
#foreach ($event in $entry.value)
<li>
<a href="$event.link">$event.metadata.title</a>
</li>
#end
</ul>
</section>
#end
#end
Tip
.add() works but also returns a boolean. Without #set, Velocity prints that value straight into your page output. Wrapping the call in #set ($void = ...) captures the return into a throwaway variable so nothing is rendered. The variable name $void is just a convention—any name works—but it signals that you are intentionally discarding the result.
Tips and gotchas
Suppressing output from .put() and .add()
Both .put() and
.add() return a value. If you call them
without capturing that return, Velocity prints it
into your output.
## Wrong - prints "true" or the old value into your page
$map.put("key", "value")
$list.add("item")
## Correct - capture the return with $void
#set ($void = $map.put("key", "value"))
#set ($void = $list.add("item"))
Dot notation fails for hyphenated keys
#set ($map = {"show-nav": "true"})
## Fails - Velocity interprets this as ($map.show) minus (nav)
## $map.show-nav
## Correct
$map.get("show-nav") ## true
$map["show-nav"] ## true
Null access and the quiet reference
Accessing a key that does not exist returns
null. Velocity renders
null references as the literal variable
text (e.g. $map.get("missing") prints
that string). Use the quiet reference
$! to suppress null output.
#set ($map = {"color": "blue"})
## Prints literal "$map.get("missing")" because the key doesn't exist
$map.get("missing")
## Prints nothing - quiet reference suppresses null
$!map.get("missing")
## Guard with containsKey for conditional logic
#if ($map.containsKey("color"))
Color is: $map.get("color")
#end
Arithmetic inside method arguments
Velocity does not evaluate arithmetic expressions
inside method arguments. Always compute the value
with #set first.
## Wrong - arithmetic inside .put() does not work
#set ($void = $map.put("count", $old + 1))
## Correct - compute first, then store
#set ($n = $old + 1)
#set ($void = $map.put("count", $n))
#set does not unset
Once a variable is set,
#set ($var = null) does
not clear it in Velocity. The
variable retains its previous value. If you need to
reset a variable inside a loop, set it to an empty
string or an appropriate default before each
iteration.
#set ($label = "default")
## This does NOT clear $label - it still equals "default"
#set ($label = $nullReference)
## Instead, reset to a known value
#set ($label = "")