Modeling Structured Content with Jekyll Collections: A Practical Guide
Jekyll Collections let you model structured content—like releases or products—with custom folders and permalinks. Learn how to configure them, implement pagination with jekyll-paginate-v2, and choose between Collections and Data Files.
23 Sept 2025, 18:28 UTC

The Problem: Blog Posts Aren't Enough
Jekyll's _posts directory works well for chronological articles, but real sites need more: product catalogs, release notes, documentation sections, or author bios. Stuffing everything into posts forces awkward front matter and makes querying painful. Collections solve this by letting you define custom content types with their own folders, permalinks, and Liquid access patterns—without leaving the static-site workflow.
Collections vs. Posts and Data Files
A collection is a named group of Markdown files living in a folder prefixed with an underscore (e.g., _releases/). Each file is a document with front matter and body content. The key differences are:
- Output Control: In
_config.yml, setoutput: trueto render each document as a standalone page with a permalink, oroutput: falsefor internal-only data (like author bios used only on other pages). - Front Matter Defaults: You can scope defaults to the collection via
defaults.scope.type: <collection_name>, removing the need to repeat layouts or categories in every file. - Liquid Access:
site.releasesreturns an array of documents;site.collections.releasesprovides metadata like the label and relative directory. Each document exposes.content,.url,.path, and.date.
Data Files (_data/*.yml) remain better for small, frequently shared reference tables (like navigation menus) because they load once and don't trigger per-document rebuilds. Collections add filesystem overhead—one file per document—and by default, rebuild the entire collection on any change.
Worked Example: Versioned Release Notes with Pagination
Let's build a releases collection for a project changelog. Each release gets a version, date, and changelog link. We'll render an index page with pagination and a sidebar showing the five most recent releases.
1. Configure the Collection
# _config.yml
collections:
releases:
output: true
permalink: /releases/:title/
defaults:
- scope:
path: ""
type: releases
values:
layout: release
category: changelog
Note: Collection names cannot contain spaces or special characters—use underscores (e.g., release_notes). The permalink template supports :collection, :path, :name, :title, and :output_ext.
2. Create Sample Documents
# _releases/v1-2-0.md
---
title: "Version 1.2.0"
version: "1.2.0"
release_date: 2024-03-15
changelog_url: "https://github.com/example/project/blob/main/CHANGELOG.md#120"
---
## Highlights
- New API endpoint for batch exports
- Fixed memory leak in background worker
# _releases/v1-1-0.md
---
title: "Version 1.1.0"
version: "1.1.0"
release_date: 2024-01-20
changelog_url: "https://github.com/example/project/blob/main/CHANGELOG.md#110"
---
## Highlights
- Dark mode support
- Improved mobile navigation
3. Enable Pagination (jekyll-paginate-v2)
The built-in jekyll-paginate (v1) only handles site.posts. For collections, you must use the jekyll-paginate-v2 gem.
# Gemfile
gem "jekyll-paginate-v2"
# _config.yml
plugins:
- jekyll-paginate-v2
pagination:
enabled: true
collection: releases
per_page: 10
permalink: /releases/page/:num/
title: ":title - Page :num of :max"
sort_field: release_date
sort_reverse: true
Run bundle install after adding the gem. The paginator object then provides .posts (aliased to the collection), .total_pages, and .next_page.
4. Build the Index Page
# releases.html
---
layout: default
title: Release History
---
<h1>Release History</h1>
<ul class="release-list">
{% for release in paginator.posts %}
<li>
<h2><a href="{{ release.url }}">{{ release.title }}</a></h2>
<time datetime="{{ release.release_date | date_to_xmlschema }}">{{ release.release_date | date: "%B %d, %Y" }}</time>
<p>{{ release.content | strip_html | truncatewords: 30 }}</p>
<a href="{{ release.changelog_url }}">Full changelog →</a>
</li>
{% endfor %}
</ul>
{% if paginator.total_pages > 1 %}
<nav class="pagination">
{% if paginator.previous_page %}
<a href="{{ paginator.previous_page_path }}">← Newer</a>
{% endif %}
<span>Page {{ paginator.page }} of {{ paginator.total_pages }}</span>
{% if paginator.next_page %}
<a href="{{ paginator.next_page_path }}">Older →</a>
{% endif %}
</nav>
{% endif %}
<aside>
<h3>Recent Releases</h3>
<ul>
{% for release in site.releases | sort: "release_date" | reverse | limit: 5 %}
<li><a href="{{ release.url }}">{{ release.title }}</a></li>
{% endfor %}
</ul>
</aside>
5. Verification
Run bundle exec jekyll serve --incremental. Visit /releases/ to verify pagination. Edit a release file and check the _site/releases/ folder timestamps to confirm only that document regenerated. Warning: Incremental regeneration in Jekyll 4.x is experimental and may miss dependency changes; always run a clean build (jekyll clean && jekyll build) before deploying.
Trade-offs and Limitations
- GitHub Pages: Does not support
jekyll-paginate-v2or custom plugins. You must pre-build locally or use GitHub Actions. - Scale: For 500+ documents, consider splitting into multiple collections or using a headless CMS to avoid slow build times.
- Defaults: Scoped defaults for
type: releasesdo not cascade to subfolders unless you also match thepath.
When to Choose Collections Over Data Files
| Scenario | Recommended Approach |
|---|---|
| Content needs individual URLs, rich Markdown, and per-item metadata | Collection (output: true) |
| Small reference tables shared across many pages (e.g., nav menus) | Data Files (_data/*.yml) |
| Internal-only records used by templates but never linked directly | Collection (output: false) |
| Frequently updated data that shouldn't trigger full-site rebuilds | Data Files |
Next Steps
- Initialize a test site:
jekyll new demo && cd demo. - Add a
productscollection in_config.ymlwithoutput: true. - Create
_products/folder with three sample Markdown files containingpriceandcategory. - Install
jekyll-paginate-v2and configure thepagination.collectionkey. - Build an index page using
paginator.poststo test the flow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.