Skip to main content

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.

Velocity
## 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.

Velocity
#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:

Velocity
#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 options for reading list values.
Syntax Example Notes
.get(n) $list.get(0) Works with variable index
Bracket notation $list[0] Concise shorthand
Velocity
#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.

Velocity
#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:

Velocity
#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:

Velocity
#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.

Velocity
## 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:

Velocity
## 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.

Velocity
#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:

Velocity
#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 options for reading map values.
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
Velocity
#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().

Velocity
#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:

Velocity
## 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.

Velocity
#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
Velocity
## Safe read with $! (outputs nothing if key is missing)
$!config.get("theme")

Quick reference

List operations

List operations at a glance.
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

Hashmap operations at a glance.
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.

Velocity
#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:

Velocity
#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:

Velocity
#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:

Velocity
## 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.

Velocity
#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>
        &mdash; $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.

Velocity
## 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.

Velocity
## 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

Velocity
#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.

Velocity
#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.

Velocity
## 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.

Velocity
#set ($label = "default")

## This does NOT clear $label - it still equals "default"
#set ($label = $nullReference)

## Instead, reset to a known value
#set ($label = "")