Skip to content

Kirby 3.5.7.1

Filtering via routes

For this recipe to work as described, you need to update your Kirby installation to release 3.1.3.

The target

The standard way to filter (page) collections by tags or categories etc. is by using parameters in your URL. A typical URL usually looks like this: http://example.com/blog/tag:travel.

When a website visitor opens this link in the browser, the controller checks if the URL contains a parameter or not and returns the corresponding collection. You can find an example in the "Filtering with tags" recipe.

While this works perfectly fine, some people prefer URLs like http://example.com/blog/travel without the parameter separator or even without the parameter name, i.e. http://example.com/blog/tag/travel.

How can we achieve this? Luckily, Kirby comes with a powerful router, which we can leverage for this purpose.

Let's go through this step by step.

The route

First, we need a route that listens to our URL pattern.

/site/config/config.php
return [
    'routes' => [
        [
            'pattern' => 'blog/tag/(:any)',
            'action' => function ($tag) {
                return page('blog')->render([
                    'tag' => $tag
                ]);
            }
        ]
    ]
]

The pattern blog/tag/(:any) with the (:any) placeholder for the actual value will listen to URLs like blog/tag/travel.

Within the action closure, we render the blog page and send the value for our tag in the methods's parameter array.

The actual filtering logic takes place in the Controller.

The controller

/site/controllers/blog.php
<?php

return function ($page, $tag) {

    $articles = $page->children()->listed();

    if ($tag) {
        $articles = $articles->filterBy('tags', $tag, ',');
    }

    return [
        'articles' => $articles
    ];

};

By loading the $tag variable to our anonymous function, we can fetch the variable we passed to the render() function in the route.

With…

if ($tag) {
  $articles = $articles->filterBy('tags', $tag, ',');
}

…we check if the $tag variable is set and if so, we filter the articles by the tag's value, and finally return the $articles variable to the template.

In your template, you can now loop through the $articles collection and render the HTML for each article.

In case you use multi-word tags with spaces or special characters, you have to urlencode() your tags in your tag links and urldecode() them again in the controller.

A modified route

If you want to leave out the tag bit in the URL, you have to modify the route a bit:

/site/config/config.php
return [
    'routes' => [
        [
            'pattern' => 'blog/(:any)',
            'action' => function ($tag) {
                if ($page = page('blog/' . $tag)) {
                    return $page;
                } else {
                    return page('blog')->render([
                        'tag' => $tag
                    ]);
                }

            }
        ]
    ]
]

In this case, tag values and subpages might conflict. First we therefore check if there is a subpage and return it if it exists. If not, we handle the value as tag and render the blog page as in the route above.

Using a different template

In the examples above, we always return the blog page/template, only with a filtered set of subpages. If we want to use a different template for our tags or categories, we have to change our route and we need a new controller and template:

Route

/site/config/config.php
<?php
return [
    'routes' => [
        [
            'pattern' => 'blog/tag/(:any)',
            'action'  => function ($tag) {

                return Page::factory([
                    'slug'     => $tag,
                    'template' => 'tag',
                    'model'    => 'tag',
                    'content'  => [
                        'title' => 'Results for ' . ucfirst($tag),
                    ]
                ]);

            }
        ]
     ]
];

Within our route, this time we return a virtual page using the Page::factory() method, because the page doesn't exist in the file system.

Controller

/site/controllers/tag.php
<?php

return function ($page) {

    $tag = urldecode($page->slug());
    $articles = page('blog')->children()->filterBy('tags', $tag, ',');

    return [
        'articles' => $articles,
        'tag'      => $tag
    ];

};

Here we use the urldecode()d page slug as our filter value.

Template

/site/templates/tag.php
<?php snippet('header') ?>
<?php if ($articles->isNotEmpty()) : ?>
    <?php foreach ($articles as $article) : ?>
        <article>
            <h2><?= $article->title()->html() ?></h2>
            <?= $article->text()->kt() ?>
            <ul>
            <?php foreach ($page->tags()->split(',') as $tag) : ?>
                <li><a href="<?= page('blog')->url() . '/tag/' . esc($tag, 'url') ?>"><?= $tag ?></a></li>
            <?php endforeach ?>
            </ul>
        </article>
    <?php endforeach ?>
<?php else : ?>    
    <div>No results for tag <?= $tag ?></div>
<?php endif ?>
<?php snippet('footer') ?>