Practical patterns for fetching, filtering, looping, and paginating collections in templates.
Fetch CMS lists with shortcuts or entities, run a search, or build a small literal list.
{%- datastore_items var featured_houses = datastore:"houses" query:"is_featured = true" -%}
{%- datastore_items var houses_by_folder = datastore:"houses" folder:entity.folder -%}Concatenate and uniq the easy way
{% datastore_items var houses = featured_houses houses_by_folder unique %}Concatenate manually
{% var houses = featured_houses | concat: houses_by_folder %}Unique manually
{% var houses = houses | uniq %}Get one random item from the list
{% var random_house = houses | rand %}Get a random item from the list. Prevents the page from being fast-cached, and every time the page is loaded a new random item will be chosen.
Get one random item from the list and allow fast-caching
{% var random_house = houses | rand:1, false, false %}Get a random item from the list and allow the page to be fast-cached, resulting in much faster pageload speeds but the same random item will be used until the fast cache expires.
Sort the full list randomly
{% set houses = houses | shuffle %}Sort the full list randomly. The page will not be able to be fast-cached, and every time the page is loaded a new random order will be chosen. To allow the page to be fast-cached, pass false to the shuffle command.
Various ways to filter and slice
{%- var one_rooms = houses | where: "rooms", "1" | sort: "price" -%}
{%- var cheapest = one_rooms | first -%}
{%- var mid_houses = one_rooms | slice: 1, 6 -%}
{%- var expensive_houses = one_rooms | slice: 7 -%}
{%- var num_expensive_houses = expensive_houses | size -%}Advanced filter
{% var few_rooms = houses | where_exp: "house", "house.rooms.value < 3" %}Grouping
{%- var grouped_by_rooms = houses | group_by: "rooms" | sort: "Key" -%}
{%- for list in grouped_by_rooms -%}
<p>{{list.Key | default: "Unknown"}} Rooms ({{list.Value | size }} houses)</p>
{%- endfor -%}Mapping and Compact
{%- var mapped_by_rooms = grouped_by_rooms | map: "Value" -%}
{%- for list in mapped_by_rooms -%}
<h4>{{list[0].rooms | default: "Unknown"}} Rooms</h4>
<ul>
<li>{{list | map: "description" | compact | join: "</li><li>" }}</li>
</ul>
{%- endfor -%}{%- var query = "movein_ready = true" -%}
{%- if request.query_params.beds is_int -%}
{%- set query = query | append: " and beds = " | append: request.query_params.beds -%}
{%- endif -%}
{%- if request.query_params.baths is_int -%}
{%- set query = query | append: " and baths = " | append: request.query_params.baths -%}
{%- endif -%}
{%- datastore_items collection = datastore:entity query:query -%}Simple query: {% datastore_items items = datastore:"Hotels" query:"number_of_rooms > 100 AND average_room_price <= 100" -%}
Complex query:
{%- capture query -%}
is_vegan
OR
(
number_of_ingredients < 6
AND (
NOT ingredients contains chicken
-ingredients contains pork
- ingredients LIKE 'beef'
{%- if other_meat_to_avoid is_valid -%}
AND !(ingredients CONTAINS "{{other_meat_to_avoid | replace: '"', '""' }}")
{%- endif -%}
)
AND NOT (
'contains_dairy'
OR ingredients contains egg
)
)
{%- endcapture -%}
{%- datastore_items items = datastore:'Recipes' query:query -%}
Query multiple configurable fields:
{%- set query = "" -%}
{%- set fieldvalue = request.query_params['fieldvalue'] | urldecode -%}
{%- if request.query_params['fieldnames'] is_valid and fieldvalue is_valid -%}
{%- var fieldnames = request.query_params['fieldnames'] | urldecode | split: ',' -%}
{%- var queryparts = '' | compact -%}
{%- for field in fieldnames -%}
{%- capture querypart %}{{field}} = "{{fieldvalue | replace: '"', '""'}}"{% endcapture -%}
{%- set queryparts = queryparts | concat: querypart -%}
{%- endfor -%}
{%- set query = queryparts | join: ' OR ' -%}
{%- endif -%}
{%- datastore_items items = datastore:entity query:query -%}{% "a" | concat:"b" | json_encode %}["a", "b"]
Filter, sort, slice, and map a collection before you loop so the template only sees what it needs.
{% var arr = existing_list | where: 'priority', 1 %}{% var arr = existing_list | where: 'position', 'top', true %}{% var with_rooms = houses | where_exp: 'house', 'house.rooms.value >= 2' %}{%- var stringinputs = 'JucLPXeHBgaZokTRsymNqwFViD' | split: '' -%}
{%- var numberinputs = (1..10) | shuffle -%}
{{- numberinputs | json_encode }}[7,6,5,9,2,3,8,1,10,4]
Sort strings
{{ stringinputs | sort | join: '' }}BDFHJLNPRTVXZacegikmoqsuwy
The sort filter sorts the strings in ascending order, with capital letters coming before lowercase letters.
Sort and ignore case
{{ stringinputs | sort: null, true | join: '' }}aBcDeFgHiJkLmNoPqRsTuVwXyZ
By passing true as the second argument, the sort filter will ignore capitalization when comparing strings.
Sort and ignore case using sort_natural
{{ stringinputs | sort_natural | join: '' }}aBcDeFgHiJkLmNoPqRsTuVwXyZ
The sort_natural filter is the same as the sort filter with the second argument set to true.
Sort numbers
{{ numberinputs | sort | join: ' ' }}1 2 3 4 5 6 7 8 9 10
The sort filter sorts the numbers in ascending order.
Sort objects
{% var sorted = houses | sort: 'number_of_rooms' %}To sort objects, pass in the property to sort by. In this example, the list of houses will be sorted by the "number_of_rooms" property.
Sort objects by string property and ignore case
{% var sorted = houses | sort: 'name', true %}When sorting objects by a string property, remember to set the second argument to true or else uppercase letters will be sorted higher before lowercase letters.
Sort randomly
{{ stringinputs | shuffle | join: '' }}JDgRoPNTFckXsmBHZqawVeyuLi
The shuffle filter sorts the strings in a random order. The page will not be able to be fast-cached, and every time the page is loaded a new random order will be chosen.
Sort randomly and allow fast-caching
{{ stringinputs | shuffle: false | join: '' }}kjFQDXZTVgEruhPbHnCwIYMzASO
Pass false to the shuffle command to specify that the page may still be fast-cached, resulting in much faster pageload times but the same random result will be chosen on every pageload until the fast cache expires.
Sort randomly using the sort filter with the special string "random" as the property
{{ numberinputs | sort: 'random' | join }}9 5 4 10 6 2 1 3 7 8
Passing "random" as the first argument to the sort filter is functionally identical to using the shuffle filter. In most cases, the shuffle filter should be preferred for clarity.
{% var list = "abcabcd" | split %}index
{{ list | index: "a" }}0
{{ list | index: "c" }}2
{{ list | index: "A" }}-1
{{ list | index: "a", 1 }}3
{{ list | index: "A", 1, true }}0
last_index
{{ list | last_index: "a" }}3
{{ list | last_index: "a", 2 }}0
{{ list | last_index: "A" }}-1
{{ list | last_index: "A", -1, true }}3
{%- blog_posts posts = limit: 10 sort_by: 'post_date' sort_direction:'desc' -%}
{{- posts | map: 'linked_title' | join: '<br />' }}Iterate with for and use forloop when you need position, first/last styling, or separators.
{%- for item in collection -%}
<div class="item-{{forloop.index0}}{% if forloop.first %} first{% elsif forloop.last %} last{% endif %} {% cycle forloop.name: "even", "odd" %}">
<span class="itemcount">Item {{forloop.index}} out of {{forloop.length}}</span><br />
<span class="itemvalue">{{ item }}</span><br />
<span class="itemsleft">{{forloop.rindex0}} items remaining</span>
</div>
{%- endfor -%}Check is_valid or size before emitting list markup so empty results do not leave blank wrappers.
{%- if random_object is_valid -%}
{%- include "display_random_object" object:random_object -%}
{%- endif -%}If field.is_valid
{%- if field.is_valid -%}
{{-field.value-}}
{%- endif -%}Every field and most objects have an is_valid property that can be used to check if the field or object has a valid value (non-null, non-empty, non-false, published entity, etc.). If you expect the field to be an object with an is_valid property, then this is the best way to check if it has a valid value.
If variable is_valid
{%- if varname is_valid -%}
{{-varname-}}
{%- endif -%}The is_valid conditional expression checks to see if a variable currently holds a valid value (non-null, non-empty, object with is_valid == true, etc.). This works for most cases, whether or not the variable is an object or a simple value
Unless variable is_valid
{%- unless varname is_valid -%}
... do something if varname is NOT valid...
{%- endunless -%}The unless conditional makes it easy to do something if the is_valid condition does NOT evaluate to true
is_valid conditional when used on false values
{%- var falsevar = false -%}
{%- if falsevar is_valid -%}
false is_valid evaluates to TRUE!
{%- else -%}
this will never be output
{%- endif -%}When using the is_valid condition on a false value, it will evaluate to true. This is by design, allowing the is_valid condition to be used to check whether or not a boolean variable has been defined
When used on null input
{{null | size}}0
When used on a string
{{"string" | size}}6
When used on a list
{{"item1,item2,item3" | split:"," | size}}3
When used on anything else
{{request.date | size}}0
Use start, limit, or page when fetching, then build paging links from total_count and request parameters.
{%- assign var postsPerPage = 10 -%}
{%- assign pageParam = 1 -%}
<!-- Did a page param get passed in the url? -->
{%- if request.query_params['page'] -%}
{%- assign pageParam = request.query_params['page'] | to_int -%}
{%- endif -%}
<!-- Did a tag param get passed in the url? -->
{%- if request.query_params['tag'] and request.query_params['tag'] != "" -%}
{%- assign tagFilter = request.query_params['tag'] | url_decode | downcase -%}
{%- endif -%}
{%- blog_posts assign posts = blog:"The Kitchen Essentials" tag:tagFilter limit:postsPerPage page:pageParam sort_by:"post_date" sort_direction:"desc" -%}
{%- for post in posts -%}
{{-post.title-}}
{%- endfor -%}{%- if collection.page > 1 -%}
{%- set_canonical_url entity.full_url | append: '?page=' | append: collection.page -%}
{%- else -%}
{%- unless entity.canonical_url is_valid -%}
{%- set_canonical_url entity.full_url -%}
{%- endunless -%}
{%- endif -%}Fetch, narrow, store with var, then loop once so query logic stays separate from markup.