Jekyll Collections: The Complete Guide
Learn how Jekyll collections work β create custom content types, configure output, use front matter defaults, and build filtered collection pages.
Collections are one of Jekyllβs most powerful features β and one of the most underused. They let you create custom content types beyond posts and pages: products, team members, themes, recipes, portfolio items, or anything you need. This guide covers everything.
What Are Collections?
Jekyll has three built-in content types: posts, pages, and data. Collections let you create your own.
A collection is a folder of Markdown files (prefixed with _) that Jekyll processes as a group. Each file in the collection becomes a document with its own URL and template.
Examples of what people build with collections:
_themes/β a theme marketplace (like this site)_products/β an e-commerce catalogue_team/β staff or contributor profiles_courses/β educational content modules_recipes/β a cookbook
Creating a Collection
Step 1: Declare the Collection in _config.yml
# _config.yml
collections:
projects:
output: true
permalink: /projects/:name/
output: trueβ generates a dedicated page for each documentpermalinkβ controls the URL structure
Step 2: Create the Collection Folder
Create a folder named with an underscore prefix: _projects/
Step 3: Add Documents
Create Markdown files in _projects/:
<!-- _projects/my-app.md -->
---
title: "My App"
description: "A mobile app for tracking habits."
year: 2026
status: "live"
url: "https://myapp.com"
image: /assets/images/projects/my-app.jpg
---
My App is a cross-platform habit tracker built with React Native...
Each file gets a URL based on your permalink setting: /projects/my-app/
Collection Configuration Options
collections:
projects:
output: true # Generate a page for each document
permalink: /projects/:name/ # URL pattern
sort_by: year # Default sort field
order: # Explicit ordering (optional)
- my-featured-project.md
- another-project.md
Permalink Variables
| Variable | Value |
|---|---|
:name |
Filename without extension |
:title |
title from front matter (falls back to :name) |
:path |
Path relative to collection folder |
:output_ext |
Output file extension (.html) |
:categories |
Front matter categories joined by / |
Front Matter Defaults for Collections
Avoid repeating common front matter in every document using defaults:
# _config.yml
defaults:
- scope:
path: ""
type: "projects"
values:
layout: "project"
author: Marcus Webb
published: true
Now every document in _projects/ automatically gets layout: project and author: Marcus Webb without you writing it in each file.
Accessing Collections in Templates
List All Documents in a Collection
{% for project in site.projects %}
<article>
<a href="{{ project.url }}">{{ project.title }}</a>
<p>{{ project.description }}</p>
</article>
{% endfor %}
Sort and Filter
<!-- Sort by year, newest first -->
{% assign sorted_projects = site.projects | sort: "year" | reverse %}
{% for project in sorted_projects %}
...
{% endfor %}
<!-- Filter by status -->
{% assign live_projects = site.projects | where: "status", "live" %}
{% for project in live_projects %}
...
{% endfor %}
<!-- Filter by multiple values -->
{% assign featured = site.projects | where_exp: "item", "item.featured == true" %}
Group by a Field
{% assign by_year = site.projects | group_by: "year" %}
{% for group in by_year %}
<h2>{{ group.name }}</h2>
{% for project in group.items %}
<p>{{ project.title }}</p>
{% endfor %}
{% endfor %}
Building a Collection Index Page
Create projects.md (or _pages/projects.md) with a layout that lists all projects:
---
layout: collection-index
title: Projects
permalink: /projects/
---
In _layouts/collection-index.html:
---
layout: default
---
<h1>{{ page.title }}</h1>
<div class="projects-grid">
{% assign projects = site.projects | sort: "year" | reverse %}
{% for project in projects %}
{% include project-card.html project=project %}
{% endfor %}
</div>
{{ content }}
Collection Document Templates
Create _layouts/project.html for individual project pages:
---
layout: default
---
<article class="project">
<header>
<h1>{{ page.title }}</h1>
{% if page.description %}
<p class="lead">{{ page.description }}</p>
{% endif %}
{% if page.url %}
<a href="{{ page.url }}" class="btn" target="_blank">Visit Project β</a>
{% endif %}
</header>
{% if page.image %}
<img src="{{ page.image }}" alt="{{ page.title }}" class="project-image">
{% endif %}
<div class="project-content">
{{ content }}
</div>
<footer class="project-meta">
<span>Year: {{ page.year }}</span>
<span>Status: {{ page.status }}</span>
</footer>
</article>
Collections Without Output Pages
Sometimes you want a collection just for data β not individual pages. Set output: false:
collections:
team:
output: false
Now site.team gives you access to all team members in your templates, but no individual URLs are generated. Great for team pages, testimonials, and similar content.
<!-- In your about page template -->
<div class="team-grid">
{% for member in site.team %}
<div class="team-card">
<img src="{{ member.avatar }}" alt="{{ member.name }}">
<h3>{{ member.name }}</h3>
<p>{{ member.role }}</p>
</div>
{% endfor %}
</div>
Collections vs Posts vs Data Files
| Feature | Posts | Collections | Data Files |
|---|---|---|---|
| Chronological ordering | Built-in | Manual | N/A |
| Individual pages | Yes | Optional | No |
| Liquid access | site.posts |
site.collection_name |
site.data.filename |
| Front matter | Yes | Yes | No (YAML/JSON/CSV) |
| Drafts | Yes | No | No |
| Best for | Blog posts | Custom content types | Configuration, simple lists |
Real-World Example: A Team Directory
# _config.yml
collections:
team:
output: false
defaults:
- scope:
type: "team"
values:
layout: "team-member"
<!-- _team/sarah-jones.md -->
---
name: Sarah Jones
role: Lead Developer
avatar: /assets/images/team/sarah.jpg
github: sarahjones
twitter: sarahjones
---
Sarah has 10 years of experience building Jekyll themes and static sites.
<!-- In _pages/about.md -->
{% for member in site.team %}
<div class="team-card">
<img src="{{ member.avatar }}" alt="{{ member.name }}">
<h3>{{ member.name }}</h3>
<p class="role">{{ member.role }}</p>
{{ member.content }}
{% if member.github %}
<a href="https://github.com/{{ member.github }}">GitHub</a>
{% endif %}
</div>
{% endfor %}
Collections are the building block behind theme directories (like this one), product catalogues, and any site that needs to manage structured content at scale.
Explore Jekyll themes on JekyllHub β our entire theme collection is built on Jekyll collections.
Why collections instead of posts?
The natural question when starting with Jekyll is why to use a collection at all when posts already work. The answer comes down to semantics and organisation. Posts are fundamentally chronological β they have dates, they belong to an RSS feed, and Jekyllβs built-in site.posts variable sorts them by date descending. When your content is not chronological β products, team members, portfolio items, documentation pages β posts are the wrong tool. You end up fighting Jekyllβs assumptions rather than working with them.
Collections let you define the structure that fits your content. A _themes/ collection can have front matter fields like stars, license, demo_url, and github_url without any of these feeling like workarounds. The content type declares its own schema. You can sort it by stars, filter it by license, and group it by category β all with standard Liquid filters, no plugins required.
There is also a practical benefit for site maintenance. When all your theme reviews live in _themes/ rather than _posts/, it is immediately clear to anyone reading the repository what the content is and how it is organised. The folder name is the documentation.
Advanced filtering with where_exp
The where filter matches exact values, but where_exp lets you write arbitrary conditions:
{% comment %} Themes with more than 5000 stars {% endcomment %}
{% assign popular = site.themes | where_exp: "item", "item.stars > 5000" %}
{% comment %} Projects completed in the last 2 years {% endcomment %}
{% assign recent = site.projects | where_exp: "item", "item.year >= 2024" %}
{% comment %} Posts that have both a tag and are featured {% endcomment %}
{% assign featured_tutorials = site.posts | where_exp: "item", "item.featured == true and item.tags contains 'tutorial'" %}
{% comment %} Team members with at least one social link {% endcomment %}
{% assign social_members = site.team | where_exp: "item", "item.twitter or item.github or item.linkedin" %}
where_exp takes two arguments: a variable name (used inside the expression to refer to the current item) and a Liquid expression string. The expression can use and, or, comparison operators, and the contains test.
Combining multiple collections on one page
A common pattern on theme marketplaces, portfolio sites, and documentation sites is showing content from multiple collections together β for example, a search results page that queries both themes and blog posts, or a homepage that shows featured items from several collections.
{% comment %} Merge two collections into one array {% endcomment %}
{% assign all_content = site.themes | concat: site.posts %}
{% assign sorted_content = all_content | sort: "date" | reverse %}
{% for item in sorted_content limit: 12 %}
<div class="card card--{{ item.collection }}">
<a href="{{ item.url }}">{{ item.title }}</a>
<span class="type">{{ item.collection }}</span>
</div>
{% endfor %}
The concat filter joins two arrays. Every document in a collection has a collection variable that holds the collection name as a string β useful for rendering type-specific UI in a mixed list.
Collection ordering strategies
Collections do not have a natural sort order the way posts do. Documents are accessed in the order the file system returns them (often alphabetical by filename), unless you explicitly sort them in your template.
Three common ordering approaches: front matter field ordering, filename-based ordering, and explicit ordering in _config.yml.
Front matter field ordering is the most flexible. Add an order or weight numeric field to each document and sort by it:
{% assign ordered_projects = site.projects | sort: "order" %}
Filename-based ordering works well for content with a natural sequential structure, like documentation chapters. Name files 01-introduction.md, 02-installation.md, 03-configuration.md and sort by the filename:
{% assign chapters = site.docs | sort: "name" %}
Explicit ordering in _config.yml is useful for small collections where the order matters precisely and you want it declared in one place:
collections:
projects:
output: true
order:
- featured-client-work.md
- open-source-library.md
- side-project.md
- older-work.md
This order key lists filenames in the desired sequence. Documents not in the list appear at the end in filesystem order.
SEO considerations for collection pages
When output: true, each collection document generates a URL and can be indexed by search engines. Making those pages discoverable requires the same SEO practices as any other page.
Set a descriptive title and description in every documentβs front matter β jekyll-seo-tag uses these for <title> and <meta name="description"> tags. If your collection documents share a layout, ensure that layout includes {% seo %} or the equivalent meta tag block.
For collections with many documents (a theme directory, a product catalogue), consider whether paginating the collection index page makes sense. A single page listing 200 products renders slowly in the browser and is harder for users to navigate than a paginated or filterable list. Jekyll does not natively paginate collections, but jekyll-paginate-v2 supports collection pagination.
Add a sitemap.xml via jekyll-sitemap β it automatically includes all collection pages with output: true, ensuring search engines can find them. Verify the sitemap in Google Search Console after deployment to confirm all collection documents are being crawled.
Validation and CI checks for collections
As a collection grows, it becomes easy to forget required front matter fields or introduce typos in field names. A simple Ruby or Python validation script run in CI catches these issues before they reach production.
# scripts/validate_collections.rb
require 'yaml'
require 'find'
errors = []
required_fields = %w[title description date author]
Find.find('_themes') do |path|
next unless path.end_with?('.md')
content = File.read(path)
front_matter = YAML.safe_load(content.split('---')[1])
required_fields.each do |field|
unless front_matter&.key?(field)
errors << "#{path}: missing required field '#{field}'"
end
end
end
if errors.any?
puts errors.join("\n")
exit 1
end
puts "All collection documents are valid."
Add this to your GitHub Actions workflow:
- name: Validate collections
run: ruby scripts/validate_collections.rb
The build fails if any document is missing a required field, preventing incomplete entries from reaching the live site. Adapt the required_fields array and directory list for each collection with different requirements.
Collections are the foundation of any Jekyll site that manages more than a simple blog. Once you understand how to define, query, sort, and filter them, you can model virtually any content structure inside Jekyll β from a straightforward portfolio to a fully featured marketplace directory. Explore JekyllHubβs theme collection to see a live example of Jekyll collections powering a real product catalogue.
Using collections to power a theme marketplace
This site β JekyllHub β uses Jekyll collections to power its entire theme directory. Each theme is a Markdown file in _themes/ with front matter fields for stars, license, demo URL, price, tags, screenshots, and a written description. The collection is configured with output: true so every theme gets its own detail page.
The homepage pulls featured themes with where: "featured", true, the themes listing page loops over all themes with site.themes, and individual category pages filter by site.themes | where: "category", "Blog". All of this runs entirely at build time β no database, no backend, no API. Every page is a static HTML file served from a CDN.
This is the core power of Jekyll collections for content-driven sites: structured data in Markdown files, queried in Liquid, rendered to HTML at build time. The same pattern applies to any site with a collection of things β a restaurant menu, a property listing, a plugin directory, a recipe archive. Define your front matter schema, write your Liquid templates, add your content files, and Jekyll handles everything else.
For reference, the full collection configuration for a theme marketplace looks like:
# _config.yml
collections:
themes:
output: true
permalink: /themes/:name/
sort_by: stars
defaults:
- scope:
type: themes
values:
layout: theme
published: true
With this setup, adding a new theme to the directory is as simple as creating a new Markdown file in _themes/ with the appropriate front matter. No database migrations, no admin interface, no deployment pipeline beyond the standard Jekyll build. The simplicity of this approach β and the resulting site performance, since every page is pre-rendered HTML β is why Jekyll collections are worth learning properly.
Beyond themes, collections unlock countless other structured content patterns. Documentation sites use collections for versioned content, sorted by chapter number. Recipe blogs use them to separate recipes from regular posts, enabling nutrition-field filtering. Event calendars use collections to display upcoming events sorted by date rather than by publication timestamp. Any time you have content that is defined by its type rather than by when it was written, a collection is the correct choice. Jekyllβs collection system is flexible enough to model nearly any content structure, and the Liquid filters give you the querying power to surface exactly the right content on every page.
Collections as a long-term investment
The time you put into learning Jekyll collections pays dividends across every future project. Once you understand the pattern β define the collection, create the files, query with Liquid β you can model almost any content structure without reaching for a database or a CMS. That makes your site faster, cheaper to host, and easier to maintain for years to come.