Skip to documentation content

Creating and Using Collections

Creating and Using Collections

Practical patterns for fetching, filtering, looping, and paginating collections in templates.

Use this guide when a template needs a list of content: blog posts, gallery items, search hits, or any other collection. Working with Lists describes the shared list model. Each section below is one step: fetch a list, narrow it, loop, handle empties, paginate, and keep query logic out of the markup.

Obtain a Collection

Fetch CMS lists with shortcuts or entities, run a search, or build a small literal list.

Start by getting a list into the template. Fetch CMS content with an object list shortcut (for example {% blog_posts %} or {% datastore_items %}) or with the entities method. Those methods live under List methods. For site search, use the search method and work with the resulting collection.

You can also build small literal lists with range markup such as (1..5), or combine lists you already have with filters like concat. Store the result with {% var %} so later steps (narrow, loop, paginate) read one name.

Entity under Content is the reference for CMS-backed page lists. {% search %} is documented with List methods. Working with Lists describes the shared list model those sources all follow.
Example Getting and manipulating entity listsUse the list methods and filters to create and manage lists of data in your templates
Liquid
{%- 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

Liquid
{% datastore_items var houses = featured_houses houses_by_folder unique %}

Concatenate manually

Liquid
{% var houses = featured_houses | concat: houses_by_folder %}

Unique manually

Liquid
{% var houses = houses | uniq %}

Get one random item from the list

Liquid
{% 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

Liquid
{% 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

Liquid
{% 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

Liquid
{%- 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

Liquid
{% var few_rooms = houses | where_exp: "house", "house.rooms.value < 3" %}

Grouping

Liquid
{%- 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

Liquid
{%- 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 -%}
Example Get datastore items using an advanced queryBuild a query string from request params and pass it to datastore_items to filter by custom fields (e.g. beds, baths).
Liquid
{%- 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 -%}
Example Sample datastore item queriesExample datastore_item queries showing common filters, sort, and limit patterns.
Liquid
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 -%}
Example Joining lists togetherUse the concat filter to join multiple lists together
Liquid
{% "a" | concat:"b" | json_encode %}
Output
["a", "b"]

Narrow and Order

Filter, sort, slice, and map a collection before you loop so the template only sees what it needs.

Apply list filters on the collection (or on entities.items) so the loop only sees what you need. Use where to keep matching items, sort to order them, slice for a window of results (including “first N”), and map when you need a derived list of property values. Chain filters as needed, then iterate.

Prefer filtering in Liquid after a reasonably specific fetch, rather than looping over a huge set and skipping items with {% if %} inside the loop. For the full filter list, see List Filters. Lists and Limits under Common Template Patterns covers first, last, and index access when you need a single item instead of a loop.
Example Filtering listsUse the where and where_exp filters to get a new list containing only the items from the input list that match the provided condition.
Liquid
{% var arr = existing_list | where: 'priority', 1 %}
Liquid
{% var arr = existing_list | where: 'position', 'top', true %}
Liquid
{% var with_rooms = houses | where_exp: 'house', 'house.rooms.value >= 2' %}
Example Sort or shuffle lists with sort, sort_natural, and shuffleUse the sort, sort_natural, and shuffle filters to sort lists of objects. For consistency and readability, the sort filter should be preferred to the sort_natural filter.
Liquid
{%- var stringinputs = 'JucLPXeHBgaZokTRsymNqwFViD' | split: '' -%}
{%- var numberinputs = (1..10) | shuffle -%}
{{- numberinputs | json_encode }}
Output
[7,6,5,9,2,3,8,1,10,4]

Sort strings

Liquid
{{ stringinputs | sort | join: '' }}
Output
BDFHJLNPRTVXZacegikmoqsuwy

The sort filter sorts the strings in ascending order, with capital letters coming before lowercase letters.

Sort and ignore case

Liquid
{{ stringinputs | sort: null, true | join: '' }}
Output
aBcDeFgHiJkLmNoPqRsTuVwXyZ

By passing true as the second argument, the sort filter will ignore capitalization when comparing strings.

Sort and ignore case using sort_natural

Liquid
{{ stringinputs | sort_natural | join: '' }}
Output
aBcDeFgHiJkLmNoPqRsTuVwXyZ

The sort_natural filter is the same as the sort filter with the second argument set to true.

Sort numbers

Liquid
{{ numberinputs | sort | join: ' ' }}
Output
1 2 3 4 5 6 7 8 9 10

The sort filter sorts the numbers in ascending order.

Sort objects

Liquid
{% 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

Liquid
{% 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

Liquid
{{ stringinputs | shuffle | join: '' }}
Output
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

Liquid
{{ stringinputs | shuffle: false | join: '' }}
Output
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

Liquid
{{ numberinputs | sort: 'random' | join }}
Output
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.

Example Find something in a listUse the index and last_index filters to find a specific item in a list.
Liquid
{% var list = "abcabcd" | split %}

index

Liquid
{{ list | index: "a" }}
Output
0
Liquid
{{ list | index: "c" }}
Output
2
Liquid
{{ list | index: "A" }}
Output
-1
Liquid
{{ list | index: "a", 1 }}
Output
3
Liquid
{{ list | index: "A", 1, true }}
Output
0

last_index

Liquid
{{ list | last_index: "a" }}
Output
3
Liquid
{{ list | last_index: "a", 2 }}
Output
0
Liquid
{{ list | last_index: "A" }}
Output
-1
Liquid
{{ list | last_index: "A", -1, true }}
Output
3
Example Map a list of blog posts to their linked titlesUse the map filter to map blog posts to their linked title. The map filter could just as easily be used for any other property as well.
Liquid
{%- blog_posts posts = limit: 10 sort_by: 'post_date' sort_direction:'desc' -%}
{{- posts | map: 'linked_title' | join: '<br />' }}

Loop and Render

Iterate with for and use forloop when you need position, first/last styling, or separators.

Use {% for item in collection %} … {% endfor %} to render each item. Inside the loop, output item properties the same way you would on an entity-driven page: print built-in fields such as title directly, and guard optional custom fields.

Use forloop when you need position: first and last for styling or separators, index or index0 for numbering, length for “n of m”. If the item row is large, include a partial and pass the current item (for example item:item). The partial should guard that input with {% if item.is_valid %}, because it is not the reserved root entity.

Control Flow documents {% for %} and the forloop object.
Example Loop over a collection and use forloop propertiesLoop over a collection and use forloop properties (first, last, index, length, etc.) for conditional output.
Liquid
{%- 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 -%}

Handle Empty Results

Check is_valid or size before emitting list markup so empty results do not leave blank wrappers.

Before emitting list markup, check that the collection is usable: {% if collection is_valid %} or compare size to zero. Skip empty wrappers so you do not render blank lists, empty <ul> tags, or a heading with nothing under it.

This is a check on the collection variable you fetched or filtered, not on root entity. Root entity is always set; the list you queried may still be empty or invalid.

Safe Output covers is_valid and default in more detail.
Example Check if an object or value is valid with is_validCheck whether an object or value is valid (non-empty, present) with is_valid before using it.
Liquid
{%- if random_object is_valid -%}
	{%- include "display_random_object" object:random_object -%}
{%- endif -%}

If field.is_valid

Liquid
{%- 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

Liquid
{%- 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

Liquid
{%- 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

Liquid
{%- 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

Example Using the size filterUse the size filter to get the size of a string or a list

When used on null input

Liquid
{{null | size}}
Output
0

When used on a string

Liquid
{{"string" | size}}
Output
6

When used on a list

Liquid
{{"item1,item2,item3" | split:"," | size}}
Output
3

When used on anything else

Liquid
{{request.date | size}}
Output
0

Paginate a Collection

Use start, limit, or page when fetching, then build paging links from total_count and request parameters.

When the set is large, page it at fetch time. For entities-backed lists, use start and limit (or page) on the query, then read total_count and total_pages to build previous/next (or numbered) links from request query parameters. Keep sort and filters stable so page 2 is the next window of the same list.

Keep the current page’s canonical URL stable. On page 1, prefer the entity’s canonical or full URL; on later pages, append the page query parameter.

Pagination under Common Template Patterns is the short recipe for query parameters and canonical URLs. Complete Examples includes a pagination partial you can adapt.
Example Get request parameters and fetch blog_posts with pagination and tag filterRead page and tag from query params, then fetch blog_posts with limit and sort.
Liquid
{%- 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 -%}
Example Set the canonical URL for paginated pagesSet the canonical URL for paginated pages so search engines understand the preferred page.
Liquid
{%- 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 -%}

A Clear Pipeline

Fetch, narrow, store with var, then loop once so query logic stays separate from markup.

A maintainable pattern is: fetch → filter/sort/slice → store in a variable with {% var %} → loop once in the template (or include a partial for the item row). That keeps query logic separate from markup and makes the same collection reusable in more than one place on the page.

Avoid repeating the same entities or datastore_items call in two loops. Fetch once, narrow once, then iterate the stored list. If the row markup is reused on other pages, pass the current item into a partial and guard that input there.

Using Includes and Snippets covers passing data into partials. Template Best Practices covers keeping {% var %} on the current scope.