Grav Taxonomy & Blog Setup — Tags, Categories, and Listing Pages
In this tutorial, you'll learn how Grav handles taxonomy (tags, categories) and how to build a blog with listing pages.
What You'll Learn
- How taxonomy works in Grav
- Defining tags and categories in page frontmatter
- Creating a blog listing page with child posts
- Filtering and displaying pages by taxonomy
- Pagination for large listings
Why It Matters
Taxonomy organizes content. Without it, visitors scroll through pages trying to find what they need. With tags and categories, they filter to exactly the content they want. Grav's taxonomy system is simple but powerful — define it in frontmatter, query it in templates.
Real-World Use
A developer documentation site might have tags like php, security, performance and categories like tutorial, reference, guide. Blog posts show related posts by shared tags. Documentation pages filter by category. Grav's taxonomy makes this possible without plugins.
Defining Taxonomy
Step 1: Configure Taxonomies
In user/config/system.yaml, list the taxonomies you want to use:
taxonomies:
- tag
- category
- author
These three are the most common. You can add any: tag, category, author, language, difficulty, topic.
Step 2: Add Taxonomy to Pages
In any page's frontmatter:
---
title: Getting Started with Grav
taxonomy:
tag:
- php
- cms
- tutorial
category: tutorial
author: admin
---
Multiple tags go in a list. Single-value taxonomies like category can be a plain string or a list.
Step 3: Create a Blog Listing
A blog listing page uses the blog.md filename and the blog.html.twig template. It automatically lists its child pages.
Create user/pages/03.blog/blog.md:
---
title: Blog
menu: Blog
published: true
blog_url: /blog
---
# Latest Posts
Stay up to date with the latest tutorials and articles.
Step 4: Create Blog Posts
Each post is a subfolder with item.md:
Create user/pages/03.blog/01.first-post/item.md:
---
title: Getting Started with Grav
date: 2026-06-01
taxonomy:
tag:
- grav
- cms
category: tutorial
---
This is my first blog post about **Grav CMS**.
Grav is a flat-file CMS that doesn't need a database. Content is stored as Markdown files, making it perfect for Git-based workflows.
```php
// Example: Grav plugin skeleton
namespace Grav\Plugin;
class MyPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onPageContent' => ['onPageContent', 0]
];
}
}
Create a second post: `user/pages/03.blog/02.second-post/item.md`:
```yaml
---
title: Grav Caching Explained
date: 2026-06-15
taxonomy:
tag:
- grav
- performance
category: tutorial
---
Learn how Grav's caching system works and how to optimize it for production.
Step 5: Create a Blog Template
user/themes/mytheme/templates/blog.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<h1>{{ page.title }}</h1>
{{ page.content|raw }}
<div class="blog-posts">
{% for post in page.collection() %}
<article class="blog-post">
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p class="meta">
{{ post.date|date('M d, Y') }}
{% for tag in post.taxonomy.tag %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</p>
<p>{{ post.summary(300)|striptags }}</p>
<a href="{{ post.url }}" class="btn">Read More →</a>
</article>
{% endfor %}
</div>
{% endblock %}
The key function is page.collection() — it returns the page's child pages as a collection, sorted by default by date (newest first).
Filtering by Taxonomy
Show Posts by Tag
To display only posts with a specific tag:
{% set collection = page.collection({
'items': {
'@taxonomy': {
'tag': 'grav'
}
},
'order': {
'by': 'date',
'dir': 'desc'
}
}) %}
{% for post in collection %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.summary|striptags }}</p>
</article>
{% endfor %}
Create a Tag Archive Page
Create user/pages/03.blog/04.tags/default.md:
---
title: Tags
menu: Tags
published: true
---
# Posts by Tag
And user/themes/mytheme/templates/tags.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<h1>{{ page.title }}</h1>
{# Get all unique tags #}
{% set tags = grav.taxonomy.find('tag') %}
{% for tag, pages in tags %}
<h2 id="{{ tag }}">{{ tag|capitalize }}</h2>
<ul>
{% for post in pages %}
<li><a href="{{ post.url }}">{{ post.title }}</a></li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}
This lists every tag and links to all pages with that tag.
Pagination
For blogs with many posts, add pagination.
Install the Pagination Plugin
bin/gpm install pagination
Update the Blog Template
{% set collection = page.collection({'items': {'@page': '/blog'}, 'order': {'by': 'date', 'dir': 'desc'}}) %}
{% set collection = collection.slice(0, 5) %} {# 5 posts per page #}
{% for post in collection %}
{# ... post content ... #}
{% endfor %}
{% if config.plugins.pagination.enabled %}
{% include 'partials/pagination.html.twig' with {'base_url': page.url, 'pagination': collection.params.pagination} %}
{% endif %}
The Pagination plugin automatically splits the collection and provides navigation.
Custom Taxonomies
You can define any taxonomy you want. For example, a difficulty taxonomy:
In system.yaml:
taxonomies:
- tag
- category
- difficulty
In page frontmatter:
taxonomy:
difficulty: beginner
In template:
{% set beginner = grav.taxonomy.find('difficulty', 'beginner') %}
Common Taxonomy Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Taxonomy not defined in system.yaml | Tag/category not recognized | Add taxonomies: [tag, category] |
| Wrong taxonomy query syntax | Collection returns no results | Use {'@taxonomy': {'tag': 'grav'}} format |
| Tags with spaces | URL encoded or broken | Use hyphens: my-tag not my tag |
| Missing pagination template | Pagination links not showing | Include partials/pagination.html.twig |
| Blog post not appearing in collection | Wrong folder structure | Each post needs its own folder with item.md |
Learning Path
flowchart LR A["What is Grav?"] --> B["Installation"] B --> C["Pages & Content"] C --> D["Navigation"] D --> E["Twig Templating"] E --> F["Themes"] F --> G["Taxonomy & Blog
← You are here"]:::current G --> H["Plugins & Admin"] H --> I["Configuration & Caching"] I --> J["Deployment & Maintenance"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Practice Questions
Where do you define which taxonomies Grav uses? Answer: In
user/config/system.yamlunder thetaxonomies:key.What function returns a page's children as a collection? Answer:
page.collection()— returns child pages sorted by date (newest first).How do you filter pages by a specific taxonomy value? Answer: Use
page.collection({'@taxonomy': {'tag': 'value'}}).What template does a blog listing page use? Answer:
blog.html.twig(because the Markdown file is namedblog.md).Challenge: Create a blog with at least 3 posts in different categories. Add a sidebar that lists all categories with links to filtered views. Add pagination with 2 posts per page.
What's Next
Your site has a blog. Now let's extend it with plugins:
Continue to Lesson 8: Plugins & Admin Panel — Extend Grav with plugins and manage content visually.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro