Skip to content

Kirby 3.5.7.1

Filter collections by tags

Tags are a great way to separate your content into different categories and make it easier for your visitors to find the articles and pages they are looking for.

Defining a tags field in a blueprint

A very basic tags field definition looks like this:

fields:
  #...
  tags:
    label: Tags
    type: tags

This creates a tags field where users can add any tags they want. You can finetune this setup with options, e.g. to limit the number of tags, allow only predefines options etc.

Adding tags to your content

In the content file, the tags are stored as a comma separated list:

Title: Some title

----

Text: Some great text

----

Tags: design, photography, architecture, whatever

Filtering content by tag

In your controllers, templates etc. you can now use the content of this field to filter your pages. Using the filterBy() method we can not only filter pages by a single value single value in a field but also to find a certain value in a list like the tags list above. Therefore, we we can filter a set of pages by tag in a single line:

$filteredPages = $page->children()->filterBy('tags', 'design', ',');

This will search through all children of the current page and return all pages with the tag "design". The third argument of the method tells it to search in a comma separated list. If you prefer to separate your tags with any other character, you are free to do that and specify that separation character as the third argument.

If we fetch the latest articles in our blog.php template like this:

$articles = $page->children()
                 ->listed()
                 ->flip()
                 ->paginate(10);

We can now filter them by a specific tag like this:

$articles = $page->children()
                 ->listed()
                 ->filterBy('tags', 'design', ',')
                 ->flip()
                 ->paginate(10);

Controlling the filter by URL

Of course, we usually don't want to filter by a hardcoded tag, but to have different URLs for each tag filter and use this parameter to filter our article list. So that an URL like http://yourdomain.com/blog/tag:fun would show all articles that are tagged fun and http://yourdomain.com/blog/tag:design all design articles.

With the param() helper function, we can fetch those parameters passed by the URL:

// url: http://yourdomain.com/blog/tag:design
echo param('tag');
// result: design

// url: http://yourdomain.com/blog/tag:fun
echo param('tag');
// result: fun

Note that you have to use a semicolon instead of a colon on Windows systems. In your code, you can use the kirby\http\params::separator() method to make sure that the resulting URL is compatible with both Linux and Windows servers.

We can now replace 'design' in our articles example above with the param function.

$articles = $page->children()
                 ->listed()
                 ->filterBy('tags', param('tag'), ',')
                 ->flip()
                 ->paginate(10);

We are almost there, but we want to make sure that the filter is only applied when a tag is added to the URL. We need an if clause to do that:

// fetch the basic set of articles
$articles = $page->children()->listed()->flip();

// add the tag filter
if($tag = param('tag')) {
  $articles = $articles->filterBy('tags', $tag, ',');
}

// apply pagination
$articles = $articles->paginate(10);

We are now ready to filter all your articles in our blog by specifying the tag in the URL

Tagcloud

To fetch all tags for a tagcloud we can use the pluck method, which will extract the values of a single field into an array.

<?php

// fetch all tags
$tags = $page->children()->listed()->pluck('tags', ',', true);

?>
<ul class="tags">
  <?php foreach($tags as $tag): ?>
  <li>
    <a href="<?= url('blog', ['params' => ['tag' => $tag]]) ?>">
      <?= html($tag) ?>
    </a>
  </li>
  <?php endforeach ?>
</ul>

Blog Controller

Putting it all together in /controllers/blog.php makes it easier to keep the blog template clean.

return function($page) {

  // fetch the basic set of pages
  $articles = $page->children()->listed()->flip();

  // fetch all tags
  $tags = $articles->pluck('tags', ',', true);

  // add the tag filter
  if($tag = param('tag')) {
    $articles = $articles->filterBy('tags', $tag, ',');
  }

  // apply pagination
  $articles   = $articles->paginate(10);
  $pagination = $articles->pagination();

  return compact('articles', 'tags', 'tag', 'pagination');

};

Once we've done that, the blog.php template is free of any logic, which makes it much more readable and easier to maintain. The template will automatically know the passed variables ($articles, $pagination, $tags & $tag)

<?php snippet('header') ?>

<h1>Blog</h1>

<!-- articles -->
<?php foreach($articles as $article): ?>
<article>
  <h1><a href="<?= $article->url() ?>"><?= $article->title()->html() ?></a></h1>
  <?= $article->text()->excerpt(300) ?>
</article>
<?php endforeach ?>

<!-- sidebar with tagcloud -->
<aside>
  <h1>Tags</h1>
  <ul class="tags">
    <?php foreach($tags as $tag): ?>
    <li>
      <a href="<?= url($page->url(), ['params' => ['tag' => $tag]]) ?>">
        <?= html($tag) ?>
      </a>
    </li>
    <?php endforeach ?>
  </ul>
</aside>

<!-- pagination -->
<nav class="pagination">
  <?php if($pagination->hasPrevPage()): ?>
  <a href="<?= $pagination->prevPageUrl() ?>">previous posts</a>
  <?php endif ?>

  <?php if($pagination->hasNextPage()): ?>
  <a href="<?= $pagination->nextPageUrl() ?>">next posts</a>
  <?php endif ?>
</nav>

<?php snippet('footer') ?>