PBS 189 of X: Jekyll Collections (GitHub Pages)
At this point in our journey, we know Jekyll has three top-level taxonomies — pages, posts, and collections. We’ve been using pages since the very start of our exploration of Jekyll, and in the previous instalment we explored how posts can be used to add a blog or news feed to a Jekyll site. That leaves just one unexplored taxonomy — collections. This is Jekyll’s general-purpose taxonomy — it’s used to structure any organised content that’s not a page or a post. Because you can add whatever custom attributes you like, you can use collections to represent just about anything.
The possible uses of collections are countless and infinite, so this instalment will be an overview of the concepts rather than a complete review of everything possible.
Matching Podcast Episode
You can also Download the MP3
Read an unedited, auto-generated transcript with chapter marks: PBS_2026_11_07
Worked Examples
Possible Gem Errors
If you check out the example code at a named tag and Jekyll fails to install or run using your preferred method (native Ruby or Docker) with an error containing the following:
jekyll 3.10.0 | Error: unknown keyword: create_additionsReapply the fix instalment 187 by running the following command in the checked-out folder:
bundle add json --version "< 2.8.0"
As a reminder, the worked examples for these Jekyll instalments are built around a demo site versioned in the pbs-jekyll-demoSite GitHub repo. The starting point for this instalment is the commit tagged pbs189-startingPoint. As usual, you can download a ZIP of this commit from GitHub, or fork the repo and then check out the tagged commit on your fork.
Content Collections
Collections allow us to define our own named taxonomies.
To create a collection:
- Define it in your site config file (
_config.yml) - Create a specially named folder to hold the collection’s content
The folder’s name is not arbitrary; it’s the collection name prefixed with an _. If you’ve ever wondered why the instalments for this series are saved in ./docs/_pbs in the series Git Repo, that’s because the instalments belong to a collection named pbs that’s defined within ./docs/_config.yml.
Collections are defined in the sensibly named config variable site.collections, which is a dictionary indexed by collection name.
Finally, documents that belong to a collection get a Liquid variable named page.collection which contains the collection’s name. Note that all blog posts have their page.collection set to blog, while regular pages don’t get a page.collection value at all.
Two Kinds of Collections
Collections come in two flavours; those that are rendered, and those that aren’t. By default, when you create a collection, it’s not rendered. That means when you add an item to the collection, it does not get its own page. These unrendered collections are intended to be used within other parts of the site.
An example might be a collection of social media profiles that get included on every page.
When you define a collection, you can choose to have it rendered by setting the collection’s output property to true. These kinds of collections behave like pages and posts — each new item gets a dedicated HTML file in your generated website. These can be used for things like profile pages, conference talks, recipes, or any other items that are not regular pages or a blog post but do need to be rendered as an HTML page.
To get an idea of how collections work, we’ll create one of each kind.
Worked Example 1 — An Unrendered Collection for Social Media Links
As previously mentioned, collections are defined in the site config file (_config.yml) under a top-level key named collections. You define collections as a dictionary, where the collection name is the key, and the collection’s configuration dictionary is the value.
We want to create a collection named socialmedia_profiles. Rather than relying on the default behaviour, we’ll explicitly configure our collection not to be rendered.
Add the following to ./docs/_config.yml:
# Define site collections
collections:
socialmedia_profiles:
output: false
Note that we are free to add whatever additional fields we like.
Since our collection is named socialmedia_profiles, we need to save our content in a folder named _socialmedia_profiles.
To add items to our collection, simply create Markdown files in the collection’s folder. Depending on your needs, those Markdown files can contain any mix of front matter and content — sometimes you need both, and sometimes only one or the other.
In our case, our entries will consist solely of front matter.
Before we continue, just to note that from Jekyll 4.0 you can define a default sort order for our collection in _confg.yml by adding a key named sort_by who’s value is the name of the front matter field to sort on. This is an extremely useful feature, but since we’re using the default GitHub Pages experience, it’s not available to us 🙁
Even though we can’t get it by default, we still want to control the order in which our social media profiles will be rendered, so we’ll add a front matter variable named sort_order to each of our social media profiles, and then explicitly sort by that field in our layouts.
To see how this works, let’s add some social media profiles.
Create a file named ./docs/_socialmedia_profiles/mastodon.md with the following content:
---
sort_order: 1
title: Bart on Mastodon
handle: '@bart@social.bartificer.ie'
link: https://social.bartificer.ie/@bart
icon: '🐘'
---
Then another file named ./docs/_socialmedia_profiles/mastodon_allison.md with the following content:
---
sort_order: 2
title: Allison on Mastodon
handle: '@podfeet@chaos.social'
link: https://chaos.social/@podfeet
icon: '🐘'
---
And finally, a file named ./docs/_socialmedia_profiles/glass_bart.md with the following content:
---
sort_order: 3
title: Bart on Glass
handle: bbusschots
link: https://glass.photo/bbusschots
icon: '📷'
---
Because we configured our collection not to be rendered, to use it, we need to include it in one of our layouts.
To illustrate how that works, let’s add a Bootstrap Card with the social media profiles to the bottom of the front page.
We can access our collection via the site.collections variable as site.collections.socialmedia_profiles, but we don’t need to use that full long name. Jekyll also adds collections directly to the site variable, so our collection can be more easily accessed at site.socialmedia_profiles. This makes custom taxonomies equal to the standard pages and post taxonomies, which are available as site.pages and site.posts.
Replace the content of ./docs/_layouts/front_page.html with the following:
<!DOCTYPE HTML>
<html>
<head>
{%- comment %}Include the snippet with the standard content for the head tag{% endcomment %}
{% include html_head_common.html %}
{%- comment %}Use the site title as the window title{% endcomment %}
<title>{{ site.title }}</title>
</head>
<body class="bg-light">
<div class="container">
{%- comment %}Start a card with a grid layout embeded within it{% endcomment %}
<div class="card m-5">
<div class="container-fluid">
{%- comment %}Add the site title as a full-width row{% endcomment %}
<header class="row">
<div class="col-12 card-body">
<h1 class="display-1 text-center">{{ site.title }}</h1>
</div>
</header>
{%- comment %}Add the illustration, content, and page buttons as a row with multiple cols{% endcomment %}
<main class="row">
<div class="col-12 col-xl-6">
<img src="{{ 'assets/siteIllustration.png' | relative_url }}" alt="An Illustration showing the PBS logo over the GitHub and Jekyll Logos with a plus between them" class="img-fluid">
</div>
<div class="col-12 col-xl-6 card-body">
<p class="lead card-text">{{ content }}</p>
</div>
<div class="col-12 card-body text-end">
{%- assign list_pages = site.html_pages | where_exp: "item", "item.title != 'Home'" %}
{%- for p in list_pages %}
<a href="{{ p.url | relative_url }}" class="btn btn-outline-primary mx-1">{{ p.title }}</a>
{%- endfor %}
</div>
</main>
</div>
</div>
{%- comment %}Add a social media card{% endcomment%}
<div class="card m-5">
<div class="card-header bg-secondary-subtle text-secondary-emphasis">
Social Media Profiles
</div>
<div class="card-body d-flex align-items-center justify-content-around gap-1 flex-wrap">
{%- assign sorted_profiles = site.socialmedia_profiles | sort: 'sort_order' %}
{%- for profile in sorted_profiles %}
<small class="bg-info-subtle text-info-emphasis border border-info rounded-2 p-1">
{{ profile.icon }} {{ profile.title }}:
<a href="{{ profile.link }}" target="_blank" class="font-monospace link-underline link-underline-opacity-0">{{ profile.handle }}</a>
</small>
{%- endfor %}
</div>
</div>
</div>
{%- comment %}Include the standard body-end snippet{% endcomment %}
{% include html_body_end.html %}
</body>
</html>
The new addition is the following:
{%- comment %}Add a social media card{% endcomment%}
<div class="card m-5">
<div class="card-header bg-secondary-subtle text-secondary-emphasis">
Social Media Profiles
</div>
<div class="card-body d-flex align-items-center justify-content-around gap-1 flex-wrap">
{%- assign sorted_profiles = site.socialmedia_profiles | sort: 'sort_order' %}
{%- for profile in sorted_profiles %}
<small class="bg-info-subtle text-info-emphasis border border-info rounded-2 p-1">
{{ profile.icon }} {{ profile.title }}:
<a href="{{ profile.link }}" target="_blank" class="font-monospace link-underline link-underline-opacity-0">{{ profile.handle }}</a>
</small>
{%- endfor %}
</div>
</div>
Notice that because we’re using Jekyll 3.*, we need to manually sort the collection with the line:
{%- assign sorted_profiles = site.socialmedia_profiles | sort: 'sort_order' %}
Other than that, we have a standard Liquid loop to add the profiles as small boxes with standard Bootstrap colour and font classes applied, contained within a Flexbox created using Bootstrap’s Flex utilities.
Because we’ve edited the site’s config file to define the collection, we can’t rely on Jekyll’s watch mode. Stop and restart Jekyll to rebuild the site from scratch, then open the front page to see the new social media profiles card on the front page.
Worked Example 2 — A Rendered Collection of Recipes
As an example of a rendered collection, let’s add some recipes to our site.
Defining the Collection
Again, the first step is to define the collection. This time, we’ll make it a rendered collection by setting output to true. Update the collections definition in ./docs/_config.yml to become:
# Define site collections
collections:
socialmedia_profiles:
output: false
recipes:
output: true
Given what we learned when creating the social media profiles, we know we’ll be adding our recipes by creating markdown files in a new _recipes folder, but where will those files get rendered to in the generated site?
This is something we can control using the permalink collection property.
This variable expects to receive a template string containing at least one of the following possible placeholders:
:collection— the collection name (recipesin our example):path— the item’s filename without file extension (for a recipe added aswaffles.mdit would bewaffles):name— the item’s filename without file extension converted to a slug (for a recipe added aswaffles.mdit would also bewaffles):title— the item’s slug, either from its front matter or filename.
We’ll use the following common pattern:
/:collection/:title
This would render a recipe with the slug belgian-waffles to the file ./recipes/belgian-waffles.html in the generated site.
Update your definition for the recipes collection to become:
recipes:
output: true
permalink: /:collection/:title
Let’s add a recipe.
Create a file named ./docs/_recipes/banana_pancakes.md with the following content:
---
title: 3-Ingredient Banana Pancakes
slug: 3-ingredient-banana-pancakes
date: 2026-07-17
description: A quick, easy, and tasty base for a weekend breakfast, or a dessert.
serves: 2
ingredients:
- 1 ripe banana (the riper the better)
- 2 eggs
- ¼ tsp sweet cinnamon
- cooking spray, as needed
serving suggestions:
- With maple syrup, to taste, as a dessert
- Rolled with sweet apple sauce inside as a dessert
- Topped with fresh berries and some sugar or syrup of choice as a dessert
- Topped with some Greek yogurt and some fresh berries as a breakfast
---
Heat a small pan to a medium heat.
In a bowl, mash the banana with a fork, and then the back of a spoon until all structure is gone and it becomes as paste.
Add the egg and cinnamon, then whisk into a batter.
Grease the small pan with the cooking spray and add a quarter of the mixture, spread it out as thinly as you can.
Fry for a few minutes on each side until golden brown.
Repeat three times.
Notice that we’ve added a lot of custom data to our front matter. This is what makes collections so powerful!
Again, we can’t rely on watch mode since we’ve edited the config file again, so stop and re-start Jekyll yet again to properly regenerate the site.
When the site rebuilds, inspecting the generated site (remember, Jekyll writes it’s output to the ./docs/_site folder) will show a new folder has been created, /recipes/, which contains one file, 3-ingredient-banana-pancakes.html. Navigating to that page in your browser by change the URL to something like ` http://localhost:4000/recipes/3-ingredient-banana-pancakes (you may need to change the port number if your Jekyll web server is running on a different port) shows it was rendered using the default` layout.
This layout is just not appropriate for a recipe. It doesn’t look great, but worse, it doesn’t render any of that important front matter!
We need to create a dedicated layout that’s appropriate for a recipe and incorporates the information in the front matter.
Create a file named ./docs/_layouts/recipe.html with the following content:
---
layout: default
---
<div class="container">
<div class="row mb-5">
<div class="col-12">
<h1 class="display-1 mb-0">{{ page.title }}</h1>
<div><small class="text-muted"><strong>Serves:</strong> {{ page.serves }}</small></div>
{% if page.description %}
<p class="lead mt-2 mb-0">{{ page.description }}</p>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-12 col-lg-4">
<div class="col">
<div class="card">
<div class="card-header">
Ingredients
</div>
<ul class="list-group list-group-flush">
{%- for ingredient in page.ingredients %}
<li class="list-group-item">{{ ingredient }}</li>
{%- endfor %}
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-8">
<div class="col mb-4">
<div class="card">
<div class="card-header">
Instructions
</div>
<div class="card-body">
<div class="card-text">
{{ content}}
</div>
</div>
</div>
</div>
{%- if page.serving_suggestions %}
<div class="col mb-4">
<div class="card">
<div class="card-header">
Serving Suggestions
</div>
<div class="card-body">
<ul>
{%- for suggestion in page.serving_suggestions %}
<li>{{ suggestion | markdownify }}</li>
{%- endfor %}
</ul>
</div>
</div>
</div>
{%- endif %}
</div>
</div>
</div>
Notice that we’ve nested this layout inside the default layout (with layout: default), just like we did in the previous instalment for the blog and page layouts.
Next, notice that we access the custom information we defined in our front matter using the page variable, e.g., page.serves, page.description, page.ingredients, and page.serving_suggestions.
Finally, notice that we’ve used the Markdown file’s content as the body for the recipe’s instructions with the {{ content }} placeholder.
Assigning a Layout to the Collection
Allowing the site to regenerate still doesn’t fix the rendering of our file — why?
Unlike with pages and posts, creating a layout with the same name as a collection does not result in that layout being automatically used for all items in that collection.
We could manually add layout: recipe to the front matter of every recipe, but that would be tedious and error prone. There must be a better way?
Of course there is — we can assign front matter defaults to collections using the defaults key in the site config (_config.yml).
The defaults key must be a list of dictionaries, one for each default being defined.
Each default must have a scope, which is to say, a file path within which it applies, and a dictionary of values to apply. To scope a default to the entire site, set scope.path to an empty string. Each default can also be limited to a specific taxonomy using the type key.
Putting it all together, we can define a default value for page.layout that sets the layout to recipe on all recipes anywhere on the site with the following entry in the site.defaults array:
scope:
path: ''
type: recipes
values:
layout: recipe
To do this for our demo site, add the following to the bottom of ./docs/_config.yml:
# Configure front matter defaults
defaults:
- scope:
path: ""
type: recipes
values:
layout: recipe
Again, because we’ve edited the config file, you’ll need to stop and restart Jekyll to render the site from scratch.
The recipe is now rendered with the correct layout 🎉
Adding a Landing Page for our Collection
Going directly to our layout’s URL shows us the correct URL, but our site doesn’t expose our recipes yet.
To do that, we need to create a landing page, just like we did for our blog in the previous instalment.
The first step is to create a layout for our recipe listing.
Create a file named ./docs/_layouts/recipes.html with the following content:
---
layout: page
---
{{ content }}
{%- assign sorted_recipes = site.recipes | sort: 'title' %}
<ul>
{%- for recipe in sorted_recipes %}
<li><a href="{{ recipe.url | relative_url }}">{{ recipe.title }}</a></li>
{%- endfor %}
</ul>
Firstly notice we’re embedding into the layout used for pages (layout: page) — this saves us the need to add the title in our layout.
And finally, notice that I sorted the recipes by title before looping over them.
Next, create a new file named ./docs/recipes.md with the following contents:
---
title: Bart's Recipes
layout: recipes
---
Bart likes to cook, so here are some of his favourite recipes.
Allow the site to rebuild, and the recipes page will be available in the menu bar. Navigating to it will show a list of the recipes.
Next Steps
These two simple worked examples are just a taster of what’s possible.
Here are just some of the opportunities for taking things further:
- You can create nested taxonomies using the front matter to define parent relationships
- The taxonomies you create with collections can use categories and tags just like posts can
- You can reference your own taxonomies in the front matter of posts and pages — for example, on multi-author sites it’s common to add a taxonomy for author profiles and then to connect specific authors to each article using a front matter key like
author,authors, orby.
Final Thoughts
Jekyll really comes into its own when you want to add information to your site beyond simple pages and posts. With collections, you can create rich pages for things like recipes or conference presentations with a complete separation between presentation and content. Your site designer creates those layouts with HTML, CSS/Sass, JavaScript, and Liquid templates, and your authors write the content in Markdown. Even when one person runs an entire site, the freedom to write everything in pure Markdown is extremely liberating. The fact that I can write my show notes for my podcast episodes in Markdown, and add all the metadata right there in the front matter, brings me no end of joy 🙂
You might think we’ve finished our exploration of Jekyll at this point, but not quite. There’s one final instalment still to come! We have one more feature to explore in detail, and some advanced features to make you aware of without diving into the full details.