Collections and Pagination Data

You never write a collection. The build generates one set per page type, one per visible label and one per visible author, and a template’s whole job is to ask for the right name. Nothing is registered, nothing is configured, and no collection you can reach was defined by hand.

There is a seam worth naming before anything else. What a label is — who creates one, what it does to access control, how a series is configured — lives at Labels and Series. What a label renders as, and which collection name carries it, is here.

The collections Leed generates

Every page-type collection is keyed pageTypeId-<id>, optionally with a :suffix. The <id> is the page type’s id, not its slug — pageTypeId-100111, not pageTypeId-blog. Get that wrong and you get undefined back with no error, so it is worth looking the id up rather than guessing it.

flowchart LR
    PT["One page type"] --> BASE["pageTypeId-&lt;id&gt;<br/>flat, generated pages included"]
    PT --> PAGED["pageTypeId-&lt;id&gt;:paged<br/>chunked into PageData"]
    PT --> UNPAGED["pageTypeId-&lt;id&gt;:unpaged<br/>flat, generated pages excluded"]
    PT --> LBL["pageTypeId-&lt;id&gt;:labels<br/>one PageData set per visible label"]
    PT --> AUTH["pageTypeId-&lt;id&gt;:authors<br/>one PageData set per visible slugged author"]
    PT --> ONELBL["pageTypeId-&lt;id&gt;:labelId-&lt;labelId&gt;<br/>flat, one label — what a series reads"]
    ALL["allPages<br/>every page in the build"] --> MAP["renderedPageMap<br/>built as a side effect"]
NameContentsGenerated pages included?SortTypical template
allPagesEvery item in the build with a non-empty pageId, across every page typeYesPublished date, then URLNone — it exists to build renderedPageMap
pageTypeId-<id>Every page of one page type, as a flat arrayYesPublished date, then URLA hand-rolled listing that wants no chunking
pageTypeId-<id>:pagedThe same pages chunked into PageData objectsNoPublished date, then URLThe scaffolded <slug>-index.hbs card grid
pageTypeId-<id>:unpagedThe same pages, flatNoPublished date, then URLA noRender page type’s list template
pageTypeId-<id>:labelId-<labelId>Pages of that type carrying one specific label, flatNoPublished date, then URLSeries rendering, via the series helpers
pageTypeId-<id>:labelsOne PageData set per visible labelOnly if the page carries the labelPublished date, then URLThe scaffolded label index
pageTypeId-<id>:authorsOne PageData set per visible author that has a slugOnly if the page carries the authorPublished date, then URLThe scaffolded authors index
pageTypeIdsThe ids of every page type, as strings—Definition orderIterating every page type once
aiSupportPluginCollectorInternal. Feed-eligible pages, used to generate llms.txtNo—None — do not paginate it

Four of them do the real work.

:paged — a card grid

A PageData[]: the page type’s pages chunked into fixed-size groups, each group wrapped in an object that also carries its own position. Chunk size comes from your company’s paging settings — paging.size, default 10 — and paging.minimum, default 0, prevents a stranded final chunk: if the last chunk would hold fewer than the minimum, its items are folded back into the one before it rather than getting a page of their own. Both reach the build through src.11tydata.json; see Global Site Data and the Data Cascade.

:unpaged — a flat list

The same pages, unchunked, with generated pages excluded. This is what a noRender page type’s list template paginates over with alias: "item" — one output page per item, at a path the template computes. It is also what redirectIndex: first and redirectIndex: last resolve against.

:labels and :authors — two-layer sets

Both produce one PageData set per key rather than one per chunk, which is why templates that use them paginate with alias: "list" and then loop the set inside.

A label appears only if it is visible — an internal-only label generates no collection. An author appears only if they are visible and have a slug; see Authors and Contributors. One asymmetry between the two: the authors sets are built with a chunk size of Number.MAX_SAFE_INTEGER, so an author’s page never splits no matter how much they have written, while a label’s set chunks normally.

allPages, and the side effect that matters

allPages is every item in the build carrying a non-empty pageId, sorted deterministically. Building it also builds renderedPageMap — a map from page id to the path that page was written to.

That map is why a template can answer “did this page actually render in this build?”. Three helpers consult it: pageIdExists, hrefIsRenderable and menuItemShouldRender. It is what lets the CMS pre-build navigation for pages that are not published yet — a menu leaf pointing at an unrendered page has its markup dropped instead of emitting a dead link.

The PageData shape

This is the object a paginated template receives as list — or as whatever you named in alias.

FieldTypeMeaning1-based?
namestringThe key this set belongs to: the page type id, <pageTypeId>:<labelId>, or the label or author id for a two-layer set—
totalnumberItems in the whole set, not this chunk—
sizenumberItems in this chunk—
page.firstbooleanThis is the first chunk—
page.numbernumberThis chunk’s indexNo — 0-based
page.nextbooleanAnother chunk follows—
page.previousbooleanAnother chunk precedes—
page.lastbooleanThis is the last chunk—
page.rangeStartnumberPosition of this chunk’s first item within the setYes
page.rangeEndnumberPosition of its last itemYes
pagingSizenumberThe chunk size used — paging.size, default 10—
minimumItemsnumberpaging.minimum, default 0—
startingIndexnumberOffset of this set’s first chunk in the whole collection array; the pagination helpers slice pagination.hrefs with it—
pageCountnumberChunks in this set—
pageDataarrayThe pages in this chunk—

Ordering

Every collection above arrives sorted the same way: ascending by publishedAt, tie-broken by ascending page URL. The URL tiebreak is what makes the order stable across builds when two pages share a timestamp — without it, a rebuild could reshuffle them and produce a spurious diff in every paginated output page.

Ascending means oldest first. A blog index that reads newest-first gets there by reversing at render time, which is what the shipped scaffold does.

To sort by something else, use sorted:

{{#each (sorted (collection "pageTypeId-100111") "data.title") }}
    <li><a href="{{ this.url }}">{{ this.data.title }}</a></li>
{{/each}}

{{#each (sorted list.pageData "data.publishedAt" reverse=true) }}
    ...
{{/each}}

It is locale-aware, case-insensitive and numeric, so i18n sorts among the letters rather than after Z, and Chapter 10 follows Chapter 9 instead of Chapter 1. Entries missing the field sort to the end in both directions — reversing does not float them to the top.

Rendering a series

A series is a label, and its pages come from the collection pageTypeId-<the current page's page type>:labelId-<the label>. The series helpers — Series, MultipartSeries, AnnouncementSeries — take a label id string and read exactly that name.

The consequence is the one fact a template author needs from this page: a series only ever contains pages of the current page’s page type. A label applied to both a blog post and a resource does not join them into one series; each page type gets its own. That is not a rule about series, it is a rule about collection names — the page type id is baked into the name the helper builds.

The rest of it — the verified each → with → Series calling pattern, the per-iteration block variables, and what happens when the label id does not resolve — belongs to Collection, Lookup and Series Helpers, which carries the worked snippet.

Worked example: a paginated index

The shipped index scaffold paginates the :paged collection. Its front matter is the interesting half:

{
    "title": "Blog List",
    "publishedAt": "list",
    "modifiedAt": "list",
    "pageTypeId": "100111",
    "pagination": {
        "data": "collections.pageTypeId-100111:paged",
        "size": 1,
        "alias": "list",
        "addAllPagesToCollections": true,
        "generatePageOnEmptyData": false,
        "reverse": false
    },
    "pageId": "0Ab3xQ9k",
    "permalink": "{{ pageType this.pageTypeId 'slug' }}{{#unless list.page.last }}/page/{{ add list.page.number 1 }}{{/unless}}/index.html"
}

Four of those lines do something non-obvious:

  • size: 1. The collection is already chunked into PageData objects, so Eleventy is paginating chunks, not pages — one chunk per output page. A larger size would put several chunks on one page.
  • alias: "list". Names the PageData object inside the template. Everything on this page called list. assumes this line.
  • generatePageOnEmptyData: false. A page type with no published pages produces no index file at all, rather than an empty grid.
  • The computed permalink. The last chunk is written to the page type’s root (/blog/) and every other chunk to /blog/page/<n>/. Because collections sort oldest-first, the last chunk holds the newest pages — so the root URL is the newest content, and the template then reverses within the chunk and reverses the numbering so the reader sees it as page 1.

publishedAt and modifiedAt are set to the literal string "list", which matches the pagination alias. That is the trigger for the date resolution described at Layouts and Page Types — the list page ends up carrying a real date from its chunk instead of the word list.

The full shipped front matter, tokens and all

This is the scaffold as Leed commits it, before token substitution:

{
    "title": "###PAGE_TYPE_NAME### List",
    "publishedAt": "list",
    "modifiedAt": "list",
    "pageTypeId": "###PAGE_TYPE_ID###",
    "pagination": {
        "data": "collections.pageTypeId-###PAGE_TYPE_ID###:paged",
        "size": 1,
        "alias": "list",
        "addAllPagesToCollections": true,
        "generatePageOnEmptyData": false,
        "reverse": false
    },
    "pageId": "###NEW_SHORT_ID###",
    "permalink": "{{ pageType this.pageTypeId 'slug' }}{{#unless list.page.last }}/page/{{ add list.page.number 1 }}{{/unless}}/index.html"
}

Every ###TOKEN### is replaced once, at commit time, so none of them survives into your repository. The full token list is at Page-Type Scaffolding Templates.

Rendering the control

The scaffold ships its own two-link Newer/Older control. For the full numbered control, call the drop-in component instead:

{{> leed/components/pagination }}

It reads list from the page root by default, or takes data=myAlias when you paginated under a different name. It renders nothing at all when pageCount is 1 or less, so an index that fits on one page needs no {{#if}} around it.

A rendered list page showing the pagination control: a range line reading Showing 1 to 10 of 42 results, numbered page links with the current page styled active, and both arrow controls

The component’s context requirements are at Head and Component Partials, and the three helpers it is built from — PaginationRange, PaginationLinks and SmartPaginationLinks — have full signatures at Pagination Helpers. Every helper that reads a collection, including collection, sorted, filterObject, Authors and Labels, is at Collection, Lookup and Series Helpers. The index, authors and label templates that consume all of this are at Page-Type Scaffolding Templates.

ESC