Content Queries in Groovy and FreeMarker Templates¶
This article shows you how to do content queries in Groovy and use the results in FreeMarker.
We will walk through a templated project using the out-of-the-box blueprint Website Editorial, where groovy scripts in page and component controllers, queries content, and puts the results on the templateModel variable, which is used for rendering the HTML.
There are two common ways to query content from Groovy:
Search index (
searchClient) — filter and sort by content type, fields, dates, and targeting. Use this for listings, related content, and search-driven pages.Repository / XML (
siteItemService) — load a specific item or walk the site tree, then read fields with XPath. Use this for navigation, taxonomies, and a known path.
For the Search API itself, see Search. For controller variables, see Page and Component Controllers. For template variables, see FreeMarker (Templating) API.
How Groovy Reaches the View¶
A page or component controller (Groovy script) does not return HTML. It populates templateModel. Any property you set there is a FreeMarker variable with the same name.
Variable
|
Role
|
|---|---|
contentModel |
The current page or component XML (a SiteItem). Read fields the author edited.
|
templateModel |
The map passed to FreeMarker. Set
templateModel.articles = ... then use${articles} / <#list articles as article> in the template. |
Bind a script in one of two ways (see Page and Component Controllers):
By content type name. For content type
/page/home, place the script atscripts/pages/home.groovy.By item selector. Add a
scripts(orscripts_o) item selector on the content type and pick Groovy files underscripts/pagesorscripts/components.
Engine also injects searchClient, siteItemService, and urlTransformationService into scripts. Convert a content store path (for example /site/website/articles/.../index.xml) to a public URL with:
urlTransformationService.transform("storeUrlToRenderUrl", doc.localId)
Indexed field names follow the content-type suffixes authors see in Studio (subject_t, image_s, date_dt, featured_b, categories_o.item.key, and so on).
Example: Featured Articles on the Home Page (Search)¶
The home page content type is bound to scripts/pages/home.groovy. The script searches for featured articles and stores the list on the template model.
1import org.craftercms.sites.editorial.SearchHelper
2import org.craftercms.sites.editorial.ProfileUtils
3
4def segment = ProfileUtils.getSegment(profile, siteItemService)
5def searchHelper = new SearchHelper(searchClient, urlTransformationService)
6def articles = searchHelper.searchArticles(true, null, segment)
7
8templateModel.articles = articles
searchArticles(true, ...) limits the query to items whose featured_b field is true. segment is optional targeting; it can be null.
The home template iterates articles. Each map has url, title, summary, and image — values the helper copied from the search hit. $model=article tells Experience Builder which content item the markup belongs to.
1<#list articles as article>
2 <@crafter.article $model=article>
3 <a href="${article.url}" class="image">
4 <@crafter.img
5 $model=article
6 $field="image_s"
7 src=article.image???then(article.image, "/static-assets/images/placeholder.png")
8 alt=""
9 />
10 </a>
11 <h3>
12 <@crafter.a $model=article $field="subject_t" href="${article.url}">
13 ${article.title}
14 </@crafter.a>
15 </h3>
16 <@crafter.p $model=article $field="summary_t">
17 ${article.summary}
18 </@crafter.p>
19 <ul class="actions">
20 <li><a href="${article.url}" class="button">More</a></li>
21 </ul>
22 </@crafter.article>
23</#list>
The same listing pattern is used on category landing pages: scripts/pages/category-landing.groovy reads contentModel.category_s and contentModel.max_articles_i, then calls searchHelper.searchArticles(false, category, segment, 0, maxArticles).
The Search Query (What SearchHelper Builds)¶
You do not have to use a helper class. Any Groovy controller can call searchClient directly. The editorial SearchHelper.searchArticles method is equivalent to a bool query that:
Filters
content-typeto/page/articleOptionally filters featured, categories, segments, and extra query-string criteria
Sorts by
date_dtdescendingMaps each hit into a simple map for FreeMarker
A self-contained version of that query in a page or component script:
1import org.opensearch.client.opensearch._types.SortOrder
2import org.opensearch.client.opensearch.core.SearchRequest
3
4def request = SearchRequest.of(r -> r
5 .query(q -> q
6 .bool(b -> b
7 .filter(f -> f
8 .match(m -> m
9 .field("content-type")
10 .query(v -> v.stringValue("/page/article"))
11 )
12 )
13 .filter(f -> f
14 .term(t -> t
15 .field("featured_b")
16 .value(v -> v.booleanValue(true))
17 )
18 )
19 )
20 )
21 .from(0)
22 .size(10)
23 .sort(s -> s
24 .field(f -> f
25 .field("date_dt")
26 .order(SortOrder.Desc)
27 )
28 )
29)
30
31def result = searchClient.search(request, Map)
32def articles = []
33
34result.hits().hits()*.source().each { doc ->
35 articles << [
36 id : doc.objectId,
37 path : doc.localId,
38 title : doc.subject_t,
39 summary : doc.summary_t,
40 image : doc.image_s,
41 url : urlTransformationService.transform("storeUrlToRenderUrl", doc.localId)
42 ]
43}
44
45templateModel.articles = articles
Put reusable query logic under scripts/classes (package path must match the folder path). The editorial helper lives at scripts/classes/org/craftercms/sites/editorial/SearchHelper.groovy.
For Query DSL vs builder APIs, aggregations, and type-ahead, see Search.
Example: Load a Taxonomy with Site Item Service¶
Search is not required when you already know the path. The search-results page controller loads the categories taxonomy selected on the page and exposes the items to FreeMarker for the refine-by checkboxes.
1def categoriesItem = siteItemService.getSiteItem(contentModel.categories_o.item.key.text)
2templateModel.categories = categoriesItem.items.item
1<#list categories as category>
2 <div class="3u 6u(medium) 12u$(small)">
3 <input type="checkbox" id="${category.key}" name="${category.key}" value="${category.key}">
4 <label for="${category.key}">${category.value}</label>
5 </div>
6</#list>
getSiteItem returns a SiteItem. Nested XML becomes properties you can walk in Groovy or FreeMarker (items.item, key, value). You can also run XPath against the current or loaded item:
def title = siteItemService.getSiteItem("/site/website/index.xml").queryValue("internal-name")
queryValue / queryValues work in FreeMarker on contentModel as well, as shown in the article template above.
For tree-based queries (for example building navigation from /site/website), use siteItemService.getSiteTree(...). Examples are in Examples.
When to Use Which¶
Use ``searchClient`` when the set of items is defined by fields (type, category, date, featured, full text) rather than a fixed folder listing.
Use ``siteItemService`` when you have a store path, need the full XML (including values that are not indexed the way you need), or are walking the repository tree.
Prefer putting query logic in Groovy, not FreeMarker. Templates should loop and print; scripts should filter, sort, and map fields.
Always map search hits to a small structure (title, url, image, …) before the view. That keeps templates independent of index field names.
Guard empty results in FreeMarker with
<#if articles?? && articles?size gt 0>.
See also Targeting for how the editorial blueprint combines these queries with profile segments.