In Grav, the most common type of collection is a list of pages that can be defined either in the page's frontmatter or in the twig itself. The most common is to define a collection in the frontmatter. With a collection defined, it is available in the Twig of the page to do with as you wish. By using page collection methods or looping through each page object and using the page methods or properties you can do powerful things. Common examples of this include displaying a list of blog posts or displaying modular sub-pages to render a complex page design.
When you define a collection in the page header, you are dynamically creating a Grav Collection that is available in the page's Twig. This Collection object is iterable and can be treated like an array which allows you to do things such as:
{{ dump(page.collection[page.path]) }}
An example collection defined in the page's frontmatter:
content:
items: '@self.children'
order:
by: date
dir: desc
limit: 10
pagination: true
The content.items
value in the page's frontmatter tells Grav to gather up a collection of items and information passed to this defines how the collection is to be built.
This definition creates a collection for the page that consists of all child pages sorted by date in descending order with pagination showing 10 items per-page.
Pagination links are added only if Pagination plugin is installed and enabled.
When this collection is defined in the header, Grav creates a collection page.collection that you can access in a twig template with:
{% for p in page.collection %}
<h2>{{ p.title|e }}</h2>
{{ p.summary|raw }}
{% endfor %}
This simply loops over the pages in the collection displaying the title and summary.
You can also include an order parameter to change the default ordering of pages:
{% for p in page.collection.order('folder','asc') %}
<h2>{{ p.title|e }}</h2>
{{ p.summary|raw }}
{% endfor %}
To tell Grav that a specific page should be a listing page and contain child-pages, there are a number of variables that can be used:
String | Result |
---|---|
'@root.pages' |
Get the top level pages |
'@root.descendants' |
Get all the pages of the site |
'@root.all' |
Get all the pages and modules of the site |
'@self.page' |
Get a collection with only the current page |
'@self.parent' |
Get a collection with only the parent of the current page |
'@self.siblings' |
Get siblings of the current page |
'@self.children' |
Get children of the current page |
'@self.modules' |
Get modules of the current page |
'@self.all' |
Get both children and modules of the current page |
'@self.descendants' |
Recurse through all the children of the current page |
'@page.page': '/fruit' |
Get a collection with only the page /fruit |
'@page.parent': '/fruit' |
Get a collection with only the parent of the page /fruit |
'@page.siblings': '/fruit' |
Get siblings of the page /fruit |
'@page.children': '/fruit' |
Get children of the page /fruit |
'@page.modules': '/fruit' |
Get modules of the page /fruit |
'@page.all': '/fruit' |
Get both children and modules of the page /fruit |
'@page.descendants': '/fruit' |
Get and recurse through all the children of page /fruit |
'@taxonomy.tag': photography |
taxonomy with tag=photography |
'@taxonomy': {tag: birds, category: blog} |
taxonomy with tag=birds && category=blog |
This document outlines the use of @page
, @taxonomy.category
etc, but a more YAML-safe alternative format is page@
, [email protected]
. All the @
commands can be written in either prefix or postfix format.
Collection options have been improved and changed since Grav 1.6. The old versions will still work, but are not recommended to use.
We will cover these more in detail.
This can be used to retrieve the top/root level published pages of a site. Particular useful for getting the items that make up the primary navigation for example:
content:
items: '@root.pages'
An alias of '@root.children'
is also valid. Using '@root'
is deprecated as its meaning can change in the future.
This will effectively get every page in your site as it recursively navigates through all the children from the root page down, and builds a collection of all the published pages of a site:
content:
items: '@root.descendants'
This will do the same as above, but it includes all the published pages and modules of the site.
content:
items: '@root.all'
This will return a collection containing only the current page.
content:
items: '@self.page'
An alias of '@self.self'
is also valid.
Empty collection will be returned if the page has not been published.
This is a special case collection because it will always return just the parent of the current page:
content:
items: '@self.parent'
Empty collection will be returned if the page is in the top level.
This collection will collect all the published pages at the same level of the current page, excluding the current page:
content:
items: '@self.siblings'
This is used to list the published children of the current page:
content:
items: '@self.children'
An alias of '@self.pages'
is also valid. Using '@self'
is deprecated as its meaning can change in the future.
This method retrieves only published modules of the current page (_features
, _showcase
, etc.):
content:
items: '@self.modules'
Using alias of '@self.modular'
is deprecated.
This method retrieves only published children and modules of the current page:
content:
items: '@self.all'
Similar to .children
, the .descendants
collection will retrieve all the published children but continue to recurse through all their children:
content:
items: '@self.descendants'
This collection takes a slug route of a page as an argument and will return collection containing that page (if it is a published page):
content:
items:
'@page.page': '/blog'
An alias of '@page.self': '/blog'
is also valid.
Empty collection will be returned if the page has not been published.
This is a special case collection because it will always return just the parent of the current page:
content:
items:
'@page.parent': '/blog'
@
Empty collection will be returned if the page is in the top level.
This collection will collect all the published pages at the same level of the page, excluding the page itself:
content:
items:
'@page.siblings': '/blog'
This collection takes a slug route of a page as an argument and will return all the published children of that page:
content:
items:
'@page.children': '/blog'
An alias of '@page.pages': '/blog'
is also valid. Using '@page': '/blog'
is deprecated as its meaning can change in the future.
This collection takes a slug route of a page as an argument and will return all the published modules of that page:
content:
items:
'@page.modules': '/blog'
Using alias of '@page.modular': '/blog'
is deprecated.
This method retrieves only published children and modules of the specific page:
content:
items:
'@page.all': '/blog'
This collection takes a slug route of a page as an argument and will return all the published children and all their descendants of that page:
content:
items:
'@page.descendants': '/blog'
content:
items:
'@taxonomy.tag': foo
Using the @taxonomy
option, you can utilize Grav's powerful taxonomy functionality. This is where the taxonomy
variable in the Site Configuration file comes into play. There must be a definition for the taxonomy defined in that configuration file for Grav to interpret a page reference to it as valid.
By setting @taxonomy.tag: foo
, Grav will find all the published pages in the /user/pages
folder that have themselves set tag: foo
in their taxonomy variable:
content:
items:
'@taxonomy.tag': [foo, bar]
The content.items
variable can take an array of taxonomies and it will gather up all pages that satisfy these rules. Published pages that have both foo
and bar
tags will be collected. The Taxonomy chapter will cover this concept in more detail.
If you wish to place multiple variables inline, you will need to separate sub-variables from their parents with {}
brackets. You can then separate individual variables on that level with a comma. For example: '@taxonomy': {category: [blog, featured], tag: [foo, bar]}
. In this example, the category
and tag
sub-variables are placed under @taxonomy
in the hierarchy, each with listed values placed within []
brackets. Pages must meet all these requirements to be found.
If you have multiple variables in a single parent to set, you can do this using the inline method, but for simplicity, we recommend using the standard method. Here is an example:
content:
items:
'@taxonomy':
category: [blog, featured]
tag: [foo, bar]
Each level in the hierarchy adds two whitespaces before the variable. YAML will allow you to use as many spaces as you want here, but two is standard practice. In the above example, both the category
and tag
variables are set under @taxonomy
.
Page collections will automatically be filtered by taxonomy when one has been set in the URL (e.g. /archive/category:news). This enables you to build a single blog archive template but filter the collection dynamically using the URL. If your page collection should ignore the taxonomy set in the URL use the flag url_taxonomy_filters:false
to disable this feature.
You can also provide multiple complex collection definitions and the resulting collection will be the sum of all the pages found from each of the collection definitions. For example:
content:
items:
- '@self.children'
- '@taxonomy':
category: [blog, featured]
Additionally, you can filter the collection by using filter: type: value
. The type can be any of the following: published
, visible
, page
, module
, routable
. These correspond to the Collection-specific methods, and you can use several to filter your collection. They are all either true
or false
. Additionally, there is type
which takes a single template-name, types
which takes an array of template-names, and access
which takes an array of access-levels. For example:
content:
items: '@self.siblings'
filter:
visible: true
type: 'blog'
access: ['site.login']
The type can also be negative: non-published
, non-visible
, non-page
(=module), non-module
(=page) and non-routable
, but it preferred if you use the positive version with the value false
.
content:
items: '@self.children'
filter:
published: false
Collection filters have been simplified since Grav 1.6. The old modular
and non-modular
variants will still work, but are not recommended to use. Use module
and page
instead.
content:
order:
by: date
dir: desc
limit: 5
pagination: true
Ordering of sub-pages follows the same rules as ordering of folders, the available options are:
Ordering | Details |
---|---|
default |
The order is based on the file system, i.e. 01.home before 02.advark |
title |
The order is based on the title as defined in each page |
basename |
The order is based on the alphabetic folder name after it has been processed by the basename() PHP function |
date |
The order is based on the date as defined in each page |
modified |
The order is based on the modified timestamp of the page |
folder |
The order is based on the folder name with any numerical prefix, i.e. 01. , removed |
header.x |
The order is based on any page header field. i.e. header.taxonomy.year . Also a default can be added via a pipe. i.e. header.taxonomy.year|2015 |
random |
The order is randomized |
custom |
The order is based on the content.order.custom variable |
manual |
The order is based on the order_manual variable. DEPRECATED |
sort_flags |
Allow to override sorting flags for page header-based or default ordering. If the intl PHP extension is loaded, only these flags are available. Otherwise, you can use the PHP standard sorting flags. |
The content.order.dir
variable controls which direction the ordering should be in. Valid values are either desc
or asc
.
content:
order:
by: default
custom:
- _showcase
- _highlights
- _callout
- _features
limit: 5
pagination: true
In the above configuration, you can see that content.order.custom
is defining a custom manual ordering to ensure the page is constructed with the showcase first, highlights section second etc. Please note that if a page is not specified in the custom ordering list, then Grav falls back on the content.order.by
for the unspecified pages.
If a page has a custom slug, you must use that slug in the content.order.custom
list.
The content.pagination
is a simple boolean flag to be used by plugins etc to know if pagination should be initialized for this collection. content.limit
is the number of items displayed per-page when pagination is enabled.
There is also an ability to filter pages by a date range:
content:
items: '@self.children'
dateRange:
start: 1/1/2014
end: 1/1/2015
You can use any string date format supported by strtotime() such as -6 weeks
or last Monday
as well as more traditional dates such as 01/23/2014
or 23 January 2014
. The dateRange will filter out any pages that have a date outside the provided dateRange. Both start and end dates are optional, but at least one should be provided.
When you create a collection with content: items:
in your YAML, you are defining a single collection based on several conditions. However, Grav does let you create an arbitrary set of collections per page, you just need to create another one:
content:
items: '@self.children'
order:
by: date
dir: desc
limit: 10
pagination: true
fruit:
items:
'@taxonomy.tag': [fruit]
This sets up 2 collections for this page, the first uses the default content
collection, but the second one defines a taxonomy-based collection called fruit
. To access these two collections via Twig you can use the following syntax:
{% set default_collection = page.collection %}
{% set fruit_collection = page.collection('fruit') %}
Iterable methods include:
Property | Description |
---|---|
Collection::append($items) |
Add another collection or array |
Collection::first() |
Get the first item in the collection |
Collection::last() |
Get the last item in the collection |
Collection::random($num) |
Pick $num random items from the collection |
Collection::reverse() |
Reverse the order of the collection |
Collection::shuffle() |
Randomize the entire collection |
Collection::slice($offset, $length) |
Slice the list |
Also has several useful Collection-specific methods:
Property | Description |
---|---|
Collection::addPage($page) |
You can append another page to this collection |
Collection::copy() |
Creates a copy of the current collection |
Collection::current() |
Gets the current item in the collection |
Collection::key() |
Returns the slug of the current item |
Collection::remove($path) |
Removes a specific page in the collection, or current if $path = null |
Collection::order($by, $dir, $manual) |
Orders the current collection |
Collection::intersect($collection2) |
Merge two collections, keeping items that occur in both collections (like an "AND" condition) |
Collection::merge($collection2) |
Merge two collections, keeping items that occur in either collection (like an "OR" condition) |
Collection::isFirst($path) |
Determines if the page identified by path is first |
Collection::isLast($path) |
Determines if the page identified by path is last |
Collection::prevSibling($path) |
Returns the previous sibling page if possible |
Collection::nextSibling($path) |
Returns the next sibling page if possible |
Collection::currentPosition($path) |
Returns the current index |
Collection::dateRange($startDate, $endDate, $field) |
Filters the current collection with dates |
Collection::visible() |
Filters the current collection to include only visible pages |
Collection::nonVisible() |
Filters the current collection to include only non-visible pages |
Collection::pages() |
Filters the current collection to include only pages (and not modules) |
Collection::modules() |
Filters the current collection to include only modules (and not pages) |
Collection::published() |
Filters the current collection to include only published pages |
Collection::nonPublished() |
Filters the current collection to include only non-published pages |
Collection::routable() |
Filters the current collection to include only routable pages |
Collection::nonRoutable() |
Filters the current collection to include only non-routable pages |
Collection::ofType($type) |
Filters the current collection to include only pages where template = $type |
Collection::ofOneOfTheseTypes($types) |
Filters the current collection to include only pages where template is in the array $types |
Collection::ofOneOfTheseAccessLevels($levels) |
Filters the current collection to include only pages where page access is in the array of $levels |
The following methods have been deprecated in Grav 1.7: Collection::modular()
and Collection::nonModular()
. Use Collection::modules()
and Collection::pages()
respectively.
Here is an example taken from the Learn2 theme's docs.html.twig that defines a collection based on taxonomy (and optionally tags if they exist) and uses the Collection::isFirst
and Collection::isLast
methods to conditionally add page navigation:
{% set tags = page.taxonomy.tag %}
{% if tags %}
{% set progress = page.collection({'items':{'@taxonomy':{'category': 'docs', 'tag': tags}},'order': {'by': 'default', 'dir': 'asc'}}) %}
{% else %}
{% set progress = page.collection({'items':{'@taxonomy':{'category': 'docs'}},'order': {'by': 'default', 'dir': 'asc'}}) %}
{% endif %}
{% block navigation %}
<div id="navigation">
{% if not progress.isFirst(page.path) %}
<a class="nav nav-prev" href="{{ progress.nextSibling(page.path).url|e }}"> <i class="fa fa-chevron-left"></i></a>
{% endif %}
{% if not progress.isLast(page.path) %}
<a class="nav nav-next" href="{{ progress.prevSibling(page.path).url|e }}"><i class="fa fa-chevron-right"></i></a>
{% endif %}
</div>
{% endblock %}
nextSibling()
is up the list and prevSibling()
is down the list, this is how it works:
Assuming you have the pages:
Project A
Project B
Project C
You are on Project A, the previous page is Project B. If you are on Project B, the previous page is Project C and next is Project A
You can take full control of collections directly from PHP in Grav plugins, themes, or even from Twig. This is a more hard-coded approach compared to defining them in your page frontmatter, but it also allows for more complex and flexible collections logic.
You can perform advanced collection logic with PHP, for example:
$collection = new Collection($pages);
$collection->setParams(['taxonomies' => ['tag' => ['dog', 'cat']]])->dateRange('01/01/2016', '12/31/2016')->published()->ofType('blog-item')->order('date', 'desc');
$titles = [];
foreach ($collection as $page) {
$titles[] = $page->title();
}
The order()
-function can also, in addition to the by
- and dir
-parameters, take a manual
- and sort_flags
-parameter. These are documented above. You can also use the same evaluate()
method that the frontmatter-based page collections make use of:
$page = Grav::instance()['page'];
$collection = $page->evaluate(['@page.children' => '/blog', '@taxonomy.tag' => 'photography']);
$ordered_collection = $collection->order('date', 'desc');
And another example of custom ordering would be:
$ordered_collection = $collection->order('header.price','asc',null,SORT_NUMERIC);
You can also do similar directly in Twig Templates:
{% set collection = page.evaluate([{'@page.children':'/blog', '@taxonomy.tag':'photography'}]) %}
{% set ordered_collection = collection.order('date','desc') %}
By default when you call page.collection()
in the Twig of a page that has a collection defined in the header, Grav looks for a collection called content
. This allows the ability to define multiple collections, but you can even take this a step further.
If you need to programmatically generate a collection, you can do so by calling page.collection()
and passing in an array in the same format as the page header collection definition. For example:
{% set options = { items: {'@page.children': '/my/pages'}, 'limit': 5, 'order': {'by': 'date', 'dir': 'desc'}, 'pagination': true } %}
{% set my_collection = page.collection(options) %}
<ul>
{% for p in my_collection %}
<li>{{ p.title|e }}</li>
{% endfor %}
</ul>
Generating menu for the whole site (you need to set menu property in the page's frontmatter):
---
title: Home
menu: Home
---
{% set options = { items: {'@root.descendants':''}, 'order': {'by': 'folder', 'dir': 'asc'}} %}
{% set my_collection = page.collection(options) %}
{% for p in my_collection %}
{% if p.header.menu %}
<ul>
{% if page.slug == p.slug %}
<li class="{{ p.slug|e }} active"><span>{{ p.menu|e }}</span></li>
{% else %}
<li class="{{ p.slug|e }}"><a href="{{ p.url|e }}">{{ p.menu|e }}</a></li>
{% endif %}
</ul>
{% endif %}
{% endfor %}
A common question we hear is regarding how to enable pagiation for custom collections. Pagination is a plugin that can be installed via GPM with the name pagination
. Once installed it works "out-of-the-box" with page configured collections, but knows nothing about custom collections created in Twig. To make this process easier, pagination comes with it's own Twig function called paginate()
that will provide the pagination functionality we need.
After we pass the collection and the limit to the paginate()
function, we also need to pass the pagination information directly to the partials/pagination.html.twig
template to render properly.
{% set options = { items: {'@root.descendants':''}, 'order': {'by': 'folder', 'dir': 'asc'}} %}
{% set my_collection = page.collection(options) %}
{% do paginate( my_collection, 5 ) %}
{% for p in my_collection %}
<ul>
{% if page.slug == p.slug %}
<li class="{{ p.slug|e }} active"><span>{{ p.menu|e }}</span></li>
{% else %}
<li class="{{ p.slug|e }}"><a href="{{ p.url|e }}">{{ p.menu|e }}</a></li>
{% endif %}
</ul>
{% endfor %}
{% include 'partials/pagination.html.twig' with {'base_url':page.url, 'pagination':my_collection.params.pagination} %}
onCollectionProcessed()
EventThere are times when the event options are just not enough. Times when you want to get a collection but then further manipulate the collection based on something very custom. Imagine if you will, a use case where you have what seems like a rather bog-standard blog listing, but your client wants to have fine grain control over what displays in the listing. They want to have a custom toggle on every blog item that lets them remove it from the listing, but still have it published and available via a direct link.
To make this happen, we can simply add a custom display_in_listing: false
option in the page header for the item:
---
title: 'My Story'
date: '13:34 04/14/2020'
taxonomy:
tag:
- journal
display_in_listing: false
---
...
The problem is that there is no way to define or include this filter when defining a collection in the listing page. It probably is defined something like this:
---
menu: News
title: 'My Blog'
content:
items:
- [email protected]
order:
by: date
dir: desc
limit: 8
pagination: true
url_taxonomy_filters: true
---
...
So the collection is simply defined by the [email protected]
directive to get all the published children of the current page. So what about those pages that have the display_in_listing: false
set? We need to do some extra work on that collection before it is returned to ensure we remove any items that we don't want to see. To do this we can use the onCollectionProcessed()
event in a custom plugin. We need to add the listener:
public static function getSubscribedEvents(): array
{
return [
['autoload', 100000],
'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onCollectionProcessed' => ['onCollectionProcessed', 10]
];
}
Then, we need to define the method and loop over the collection items, looking for any pages with that display_in_listing:
field set, then remove it if it is false
:
/**
* Remove any page from collection with display_in_listing: false|0
*
* @param Event $event
*/
public function onCollectionProcessed(Event $event): void
{
/** @var Collection $collection */
$collection = $event['collection'];
foreach ($collection as $item) {
$display_in_listing = $item->header()->display_in_listing ?? true;
if ((bool) $display_in_listing === false) {
$collection->remove($item->path());
}
}
}
Now your collection has the correct items, and all other plugins or Twig templates that rely on that collection will see this modified collection so things like pagination will work as expected.
Found errors? Think you can improve this documentation? Simply click the Edit link at the top of the page, and then the icon on Github to make your changes.
Powered by Grav + with by Trilby Media.