Skip to content

Grav Taxonomy & Blog Setup — Tags, Categories, and Listing Pages

DodaTech Updated 2026-06-27 5 min read

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

  1. Where do you define which taxonomies Grav uses? Answer: In user/config/system.yaml under the taxonomies: key.

  2. What function returns a page's children as a collection? Answer: page.collection() — returns child pages sorted by date (newest first).

  3. How do you filter pages by a specific taxonomy value? Answer: Use page.collection({'@taxonomy': {'tag': 'value'}}).

  4. What template does a blog listing page use? Answer: blog.html.twig (because the Markdown file is named blog.md).

  5. 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