How to Auto-Generate Tag and Category Pages in Jekyll
Automatically create archive pages for every tag and category in your Jekyll blog using jekyll-archives — with custom layouts, tag clouds, and SEO-friendly URLs.
Tags and categories are how readers discover related content on your blog. They click “jekyll” expecting to find every post you have written about Jekyll — but without proper archive pages, they land on a 404 instead. This is one of the most common gaps in Jekyll blogs, and it is entirely fixable.
By default, Jekyll processes tag and category metadata from your post front matter and makes it available to your templates. What it does not do is generate a dedicated page for each tag or category. The jekyll-archives plugin handles that automatically. This guide covers everything from installation to SEO optimisation for taxonomy pages.
The problem: links that go nowhere
When you add tags to a post:
tags:
- jekyll
- tutorial
- static-sites
Jekyll exposes those values through page.tags, so your theme can render them as links. But clicking /tag/jekyll/ produces a 404 because no such page exists in _site/. The same applies to categories — page.category is available, but /category/tutorial/ is a dead end unless you create or generate those pages.
This matters for three reasons. First, it breaks the user experience — a reader interested in a topic clicks a tag and hits a wall. Second, it wastes internal linking opportunity — tag pages are naturally high-value destination pages that should consolidate link equity from every tagged post. Third, it signals to search engines that your site has broken structure.
The fix is jekyll-archives, which reads your existing tags and categories and generates all the archive pages automatically at build time.
Installing jekyll-archives
# Gemfile
gem "jekyll-archives"
# _config.yml
plugins:
- jekyll-archives
Run bundle install.
GitHub Pages note: jekyll-archives is not on the GitHub Pages approved plugin list. If you deploy to GitHub Pages, you need to use GitHub Actions to run the Jekyll build yourself — this unlocks all plugins. For Netlify, Vercel, Cloudflare Pages, and most other hosts, it works with a standard build command.
Configuration
# _config.yml
jekyll-archives:
enabled:
- categories
- tags
layouts:
category: archive-taxonomy
tag: archive-taxonomy
permalinks:
category: /category/:name/
tag: /tag/:name/
With this configuration, Jekyll generates a page for every unique tag and category used across your posts. If you have posts tagged “jekyll”, “tutorial”, and “seo”, you get /tag/jekyll/, /tag/tutorial/, and /tag/seo/ automatically — without creating any files manually.
You can also enable date-based archives:
jekyll-archives:
enabled:
- categories
- tags
- year
- month
layouts:
category: archive-taxonomy
tag: archive-taxonomy
year: archive-year
month: archive-month
permalinks:
category: /category/:name/
tag: /tag/:name/
year: /:year/
month: /:year/:month/
Date archives are useful for news-style blogs but often unnecessary for evergreen content sites. Add them only if your readers would actually use them.
Creating the archive layout
Create _layouts/archive-taxonomy.html. This layout receives the tag or category name as page.title and the associated posts as page.posts:
---
layout: default
---
<div class="archive-header">
<div class="container">
<p class="archive-type">
{% if page.type == "tag" %}Tag{% elsif page.type == "category" %}Category{% endif %}
</p>
<h1 class="archive-title">{{ page.title }}</h1>
<p class="archive-count">{{ page.posts | size }} post{% if page.posts.size != 1 %}s{% endif %}</p>
</div>
</div>
<div class="container">
<div class="post-list">
{% for post in page.posts %}
<article class="post-card">
<time class="post-card__date" datetime="{{ post.date | date_to_xmlschema }}">
{{ post.date | date: "%B %d, %Y" }}
</time>
<h2 class="post-card__title">
<a href="{{ post.url }}">{{ post.title }}</a>
</h2>
{% if post.description %}
<p class="post-card__excerpt">{{ post.description }}</p>
{% else %}
<p class="post-card__excerpt">{{ post.excerpt | strip_html | truncatewords: 30 }}</p>
{% endif %}
<div class="post-card__tags">
{% for tag in post.tags %}
<a href="/tag/{{ tag | downcase | replace: ' ', '-' }}/" class="tag">{{ tag }}</a>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
</div>
Each tag page loops over page.posts — the subset of all your posts that carry that particular tag. The layout reuses your existing post-card styles, so no extra CSS is needed.
Displaying tags and categories in post templates
In your _layouts/post.html, render tag and category links that point to the generated archive pages:
{% if page.tags.size > 0 %}
<div class="post-tags" aria-label="Post tags">
<span class="post-tags__label">Tags:</span>
{% for tag in page.tags %}
<a href="/tag/{{ tag | downcase | replace: ' ', '-' }}/" class="tag">
{{ tag }}
</a>
{% endfor %}
</div>
{% endif %}
{% if page.category %}
<div class="post-category" aria-label="Post category">
<span class="post-category__label">Category:</span>
<a href="/category/{{ page.category | downcase | replace: ' ', '-' }}/"
class="category-badge">
{{ page.category }}
</a>
</div>
{% endif %}
These links now resolve to real pages. Readers can click any tag and land on a properly formatted archive of related posts.
Tags vs categories: choosing the right taxonomy
Before tagging everything, it helps to understand the purpose of each:
Categories are broad sections that every post belongs to. Think of them like chapters in a book — they represent the main topics your site covers. A Jekyll blog might have categories like “Tutorial”, “Opinion”, and “Roundup”. Every post fits into exactly one category (or a small number). Categories tend to map to the navigation structure of your site.
Tags are descriptive keywords that apply across categories. A “Tutorial” post might be tagged with “jekyll”, “liquid”, “front-matter” — specific topics that help readers find posts on that exact subject. Tags are more granular and numerous than categories.
A good rule of thumb: use 2–5 tags per post. More than that dilutes the signal. Create a tag only when you expect to write enough posts on that topic to make a dedicated archive page useful — a tag with one post is barely worth having.
A tag cloud for your sidebar
Display all your tags with visual weight proportional to post count:
{% assign max_count = 0 %}
{% for tag in site.tags %}
{% if tag[1].size > max_count %}
{% assign max_count = tag[1].size %}
{% endif %}
{% endfor %}
<div class="tag-cloud" aria-label="Topics">
{% assign sorted_tags = site.tags | sort %}
{% for tag in sorted_tags %}
{% assign weight = tag[1].size | times: 100 | divided_by: max_count %}
<a href="/tag/{{ tag[0] | downcase | replace: ' ', '-' }}/"
class="tag"
style="font-size: {{ weight | divided_by: 50 | plus: 0.9 }}em"
title="{{ tag[1].size }} posts">
{{ tag[0] }}
</a>
{% endfor %}
</div>
The tag with the most posts renders at 100% weight; others scale proportionally. sort alphabetises the tags so the cloud is consistent across builds.
Listing all categories with post counts
For a categories overview page:
<ul class="category-list">
{% assign sorted_categories = site.categories | sort %}
{% for category in sorted_categories %}
<li class="category-list__item">
<a href="/category/{{ category[0] | downcase | replace: ' ', '-' }}/"
class="category-list__link">
{{ category[0] }}
</a>
<span class="category-list__count">{{ category[1].size }} posts</span>
</li>
{% endfor %}
</ul>
site.categories is a hash where the key is the category name and the value is an array of posts in that category.
SEO for tag and category pages
Taxonomy archive pages can be powerful SEO assets — or they can hurt you. Here is how to make them work:
Add descriptions to your important taxonomy pages. Dynamically generated pages cannot have individual front matter, but you can store descriptions in a data file and apply them in the layout.
Create _data/taxonomy_descriptions.yml:
categories:
Tutorial: "Step-by-step guides for building and customising Jekyll sites."
SEO: "SEO tips and techniques for Jekyll and static sites."
tags:
jekyll: "All posts about Jekyll — the open-source static site generator."
liquid: "Guides covering Liquid, Jekyll's templating language."
"github pages": "Tutorials for deploying Jekyll sites to GitHub Pages."
In your archive-taxonomy.html layout, pull the description from the data file:
{% if page.type == "category" %}
{% assign desc = site.data.taxonomy_descriptions.categories[page.title] %}
{% elsif page.type == "tag" %}
{% assign desc = site.data.taxonomy_descriptions.tags[page.title] %}
{% endif %}
{% if desc %}
<meta name="description" content="{{ desc }}">
<p class="archive-description">{{ desc }}</p>
{% endif %}
Noindex thin tag pages. A tag with only one or two posts creates a near-empty page that is unlikely to rank and may dilute your site’s crawl budget. Add a conditional noindex meta tag for small archives:
{% if page.posts.size < 3 %}
<meta name="robots" content="noindex, follow">
{% endif %}
The follow attribute lets search engines still follow links on the page even though it is not indexed.
Do not duplicate tag content. If a tag page shows the full text of each post rather than an excerpt, it creates duplicate content. Always use excerpts or descriptions on archive pages.
The manual approach (without the plugin)
If you cannot use jekyll-archives — for instance, you are using GitHub Pages without GitHub Actions — you can create tag and category pages manually. It requires a separate file per taxonomy term, which is tedious for large sites but works for small ones.
Create tag/jekyll.md:
---
layout: tag-page
tag: jekyll
title: "Posts tagged: jekyll"
permalink: /tag/jekyll/
---
Create _layouts/tag-page.html:
---
layout: default
---
<h1>{{ page.title }}</h1>
<p>{{ site.tags[page.tag] | size }} posts</p>
{% assign tagged_posts = site.posts | where_exp: "post", "post.tags contains page.tag" %}
{% for post in tagged_posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.description | default: post.excerpt | strip_html | truncatewords: 25 }}</p>
</article>
{% endfor %}
This requires you to create a new file for every tag you use. For a blog with 20+ tags, switching to GitHub Actions and using the plugin is well worth the setup time.
Keeping taxonomy consistent
Over time, it is easy to accumulate messy tags — variations like “GitHub Pages”, “github-pages”, and “Github Pages” that should all be the same tag. Inconsistent tags fracture your archives into multiple thin pages.
Establish a tag naming convention early: lowercase with hyphens (“github-pages”), or title case with spaces (“GitHub Pages”). Stick to it across every post. If you need to rename a tag across many posts, a quick find/replace in your editor or a short shell script saves time.
Well-structured tag and category pages improve navigation, strengthen internal linking, and give search engines clear signals about the topics your site covers. Many Jekyll themes on JekyllHub include archive page layouts ready to use with jekyll-archives.
Designing effective taxonomy archive pages
An archive page that just lists post titles is a wasted opportunity. Readers who land on a tag page have expressed specific interest — they want to read about that topic, and the archive page should immediately surface the best content for them rather than a flat reverse-chronological list.
Consider ordering tag archives by quality signals rather than pure recency. Your most-read, most-shared, or highest-rated posts on a given topic are often more valuable to a new visitor than your newest one. Jekyll does not have built-in quality signals, but you can add custom front matter fields — featured: true, weight: 1, or views: 5000 — and sort by those in your Liquid template. A tag archive that shows your two featured posts at the top before the chronological list gives readers immediate access to your best work on that topic.
Group posts by year or month when a tag archive grows long. A flat list of forty posts is harder to scan than a list organised by year. The group_by_exp Liquid filter handles this: site.posts | group_by_exp: "post", "post.date | date: '%Y'" returns an array of year groups, each containing the posts from that year. Rendering each group with a year heading creates a scannable timeline.
Add excerpts or meta descriptions to archive list items. The post title alone gives readers limited information for deciding whether to click. A two-sentence excerpt dramatically improves click-through on archive pages. Set excerpt_separator in _config.yml and add a brief excerpt below each heading in your archive list template.
Tag and category SEO strategy
Tags and categories are not just navigation features — they are SEO assets. A well-structured taxonomy creates additional pages that can rank for long-tail keywords, and strong internal linking between tag archives and posts signals topical authority to search engines.
Each tag or category archive page should have a unique title, meta description, and (optionally) introductory paragraph. Jekyll-archives generates these pages with generic metadata by default; override them with a _data/tags.yml or _data/categories.yml file that maps each tag slug to a title, description, and optional intro text. Your Liquid template then uses site.data.tags[page.tag].description to populate the page’s meta description and intro paragraph, turning each tag archive into a first-class page with unique content.
Be conservative with the number of tags you use. A blog with one hundred tags where each tag has only two or three posts creates a hundred thin archive pages, most with too little content to rank for anything. Search engines may treat thin tag archives as duplicate or low-quality content. The alternative — consolidating tags into fewer, richer archives (ten to twenty tags, each with fifteen or more posts) — produces archive pages substantial enough to provide genuine value to readers and search engines alike.
Use canonical tags on tag and category archive pages when multiple URL formats can reach the same content. If your tag archives are accessible at both /tags/jekyll/ and /tags/jekyll/?page=1, add <link rel="canonical" href="/tags/jekyll/"> to the paginated versions to consolidate ranking signals on the primary URL.
Pagination on archive pages
Long tag archives benefit from pagination to avoid loading hundreds of posts in a single page. Jekyll’s built-in jekyll-paginate plugin only paginates the main posts list (/blog/index.html), not tag or category archives. For archive pagination, you need jekyll-paginate-v2, which supports pagination on any collection with configurable paths.
Configure jekyll-paginate-v2 in _config.yml with a pagination block that specifies enabled: true, per_page: 12, and a permalink pattern. Then create a tag archive template with pagination.enabled: true in its front matter and iterate over paginator.posts instead of site.tags[page.tag]. The plugin handles generating paginated pages automatically when you run a build.
Infinite scroll is an alternative to traditional pagination — load the first twelve posts on page load, then append additional posts as the reader scrolls. This requires a JavaScript implementation that fetches post data (from a JSON data file you generate at build time) and appends new DOM nodes dynamically. Infinite scroll improves mobile reading experience but complicates browser back-button behaviour, since the reader’s scroll position is lost when they navigate away and return.
Cross-linking tags with related posts
Tag pages strengthen internal linking, and internal linking distributes page authority throughout your site. Take this further by adding tag-based related posts to every individual post. At the bottom of each post, list three to five posts that share at least one tag with the current post.
Implementing this in Liquid requires a nested loop: for each tag on the current post, loop through all posts with that tag, exclude the current post, and collect unique matches up to the desired count. This is computationally expensive for large sites (Liquid loops do not lazy-evaluate), so large Jekyll sites often generate related posts data at build time using a plugin like jekyll-related-posts (which uses TF-IDF similarity) rather than recalculating the relationship in every post’s template.
The simplest related posts approach that performs well: add a related: field to each post’s front matter with a list of two or three related post slugs. This requires manual curation but produces the highest-quality recommendations, since you choose the most genuinely related posts rather than relying on tag overlap. For a site with under one hundred posts, manual curation is feasible and worth the effort.