Filtering children pages with pretty urls using a blueprint select field

Happy New Year!

I am usually the guy trying to adapt other peoples ideas so I can learn how to do my task. I usually take from this forum, not give. Here’s the first move to change that. I’m not a php programmer, so delving into some of this within Kirby can be daunting.

Please let me know if you have any improvements to this task or errors I’ve made, so that others don’t get it wrong.

After spending a lot of time adapting the suggested filtering options within Kirby, I’ve tried to make an example of what I’m using to filter pages based on a select field in children page blueprints.
You can find the recipe from Kirby that this task was based on here:

A few caveats, first:

  • We want Pretty URLs, not colons or semicolons or ? parameters, etc.
  • If you require multiple filters, you’ll be going down your own path. This is not your solution
  • The code I’m writing here is general so you can reference it for filtering any children items
  • Of course, to adapt this to Blog Posts, you’d need to change your object names to reflect this ($items could be $posts, $products, $features, $albums, $recipes, etc.), filters could be ‘tags’, ‘categories’, ‘sizes’, etc.

Now we’re going to go through all of the parts of how to do this in a general way so you can get this working in any section you want to filter, using Pretty URLs, showing item counts, create a filter link list for filtering, and adding aria attributes used to also highlight the current filter.

Example:

We’re not going to use the built-in tags feature of Kirby, but we will create our own using the select field with defined options. We’ll do this in three steps.

First, the Controller

First, create a controller for your page, the page that will show filtering for the children of this page, let’s use a Blog as an example:
(More on Controllers: Controllers | Kirby CMS, or this video: https://youtu.be/Im2bx2WytPc)

site/controller/blog.php

<?php

return function ($page, $filter = null) {
    // All items, unfiltered
    $items = $page->children()->listed();
    $tags = [];

    // Assuming all items use the same blueprint, get the 'tags' field from the first item
    if ($firstItem = $items->first()) {
        $itemBlueprint = $firstItem->blueprint();
        $options = $itemBlueprint->field('tags')['options'] ?? [];

        // Build an array with the correct keys and counts
        foreach ($options as $key => $value) {
            $tags[$key] = [
                'label' => $value,
                'count' => $items->filterBy('tags', $key, ',')->count()
            ];
        }
    }

    // Filtered items based on the 'filter' parameter
    $filteredItems = $filter ? $items->filterBy('tags', $filter, ',') : $items;

    return [
        'items' => $items, // Keep all items here
        'filteredItems' => $filteredItems, // Apply the filter if needed
        'tags' => $tags,
        'activeTag' => $filter
    ];
};

Again, change the object names, et al, to reflect what sort of items you’re filtering if you like. Just match these up with the other places these names are used in the code that comes next.

Second: the template

Let’s make a filter link list of items, and then we’ll add in a loop to show the items that have been filtered. The link list should look like this:

All (11), Wetsuits (5), Surfboards (1), Fin Systems (2), Snowboards (1), Hardware (1), Boots (1)

Let’s make it:

/site/templates/blog.php

<p class=“filters”>
   <a href="<?= $page->url() ?>"
      aria-current="<?= is_null($activeTag) ? 'page' : 'false' ?>">
       All (<?= $items->count() ?>)
   </a>

<?php foreach ($tags as $key => $tag): ?>
    <?php $isActive = $activeTag === $key; ?>
    <a href="<?= url($page->url() . '/tag/' . urlencode($key)) ?>"
       aria-current="<?= $isActive ? 'page' : 'false' ?>">
        <?= $tag['label'] ?>
        (<?= $tag['count'] ?>)
    </a>,
<?php endforeach; ?>

</p>

<div class="items">
    <?php foreach ($filteredItems as $item): ?>
        <div class="item">
            <h2><?= $item->title()->html() ?></h2>
            <!-- Item details -->
        </div>
    <?php endforeach; ?>
</div>

Note: Make sure that your foreach loop for the items (Blog Posts) is using the ‘filteredItems’ object, and not the object that has all of your items (Blog Posts).

Third, the Routes

Next we’ll add our Routes in the site config files. If you have multiple of these for different environments, make sure to add it to the ones for production where you intend to use this.
Knowing that the syntax of the config.php file is important, if you did this part and it doesn’t work for you, make sure to check all of your opening and closing brackets and general PHP syntax.

Okay, let’s instruct Kirby to look at the URL parameter that has the filter we need to sort the items.

/site/config/config.php

<?php 

// for more, see: https://getkirby.com/docs/reference/system/options

return [
    'routes' => [
        [
            'pattern' => 'section/tag/(:any)',
            'action'  => function ($filter) {
                return page('section')->render([
                    'filter' => $filter
                ]);
            }
        ],
        // ... other routes
    ],
    // ... other configurations
];

Now just alter this to use blog/tag/(:any) instead of section/tag/(:any) or whichever section of your site this is used.
Then also change page('section') to also be the parent page of the filtered items, so for Blog it would be page('blog'), for example.

Lastly, make sure your blueprint for the items have a select field with defined options like this:

fields:
  tags:
    label: Tags
    type: select
    options:
      tshirt: T-Shirt
      hoodie: Hoodie
      // ... replace these with other options for your items, like tags, sizes, kinds, museum names, type of clothes, etc. 
      // NOTE: the first string should have no spaces: 'tshirt', not 't shirt' or 't-shirt'.

So, that’s it! Now your children items filtering will have URLs like these:

store.shop/products/kind/surfboards
museum.museum/artwork/medium/oil
blog.blog/blog/topic/php
restaurant.eat/menu/time/lunch

instead of:

store.shop/products/kind:surfboards
museum.museum/artwork/?medium=oil
blog.blog/blog/topic;windows
…etc.

Of course, the Kirby cookbook recipe lists many more ways to filter children, and for that you should read up and understand the ideas shared there.

Lastly, if you have fixes, edits, or ideas, please reply. Have a great new year!

Suggestions:

  • Don’t use two variables for $items and $filteredItems, when only using the $filteredItems anyway in the template
  • In your controller, you filter by tags, while the field in the blueprint is called kind.

Using the url() helper in combination with the $page->url() is superfluous.

1 Like

Great points! All fixed except the most difficult one: removing the additional $filteredItems object to simplify.
Would you wrap it in an if statement to do this?

Yes, either an if statement or simply

$items = $filter ? $items->filterBy('tags', $filter, ',') : $items;

This won’t work, the url() helper doesn’t accept a $page object as parameter, should be just $page->url() without the helper.

1 Like

I see, okay, that might alter the controller quite a bit. Hmmm.

Okay, only one object to filter now, not two.
Also, I think in order to keep the code lean and not be too complex, I’ve removed the item counts.

/site/templates/blog.php

<style>
	a[aria-current="page"] {
		font-weight: bold;
	}
</style>
<p class="filters" >
	<a href="<?= $page->url() ?>"<?= e(!$kind, ' aria-current="page"') ?>>
		All
	</a>
	<!-- Links for each tag/filter/etc -->
	<?php foreach ($tags as $tagOption): ?>
		<a href="<?= url('blog/kind/' . urlencode($tagOption)) ?>"
			<?= e($tag === $tagOption, ' aria-current="page"') ?>>
			<?= html($tagOption) ?>
		</a>
		<?php endforeach; ?>
	</p>

Then the controller is constrained to only these parts:

/site/controllers/blogpost.php

<?php

return function ($page, $routes, $filter = null) {

// all items, unfiltered
    $items = $page->children()->listed();
    // create the kinds array
    $filter = param('filter');

    // Use a ternary operator to filter products by filter if $filter is set
    $items = $page->children()->listed();
    $items = $filter ? $items->filterBy('filter', $filter, ',') : $items;

    // Assuming all items use the same blueprint, get the 'tags' field from the first item
    $filters = $page->index()->filterBy('intendedTemplate', 'blogpost')->pluck('tags', ',', true);

    return [
        'items' => $items,
        // the filters array
        'filters' => $filters,
        // the current filter parameter is passed in from the route
        'filter' => $filter
    ];
};

NOTE: You can see above where the word blogpost appears, this is the name of your template/blueprint. So change this to fit.

And the config.php routes stay the same.

In some ways this is identical to the way Kirby describes this process, but here it all is for you.

Options:

  1. Switch items for whatever type of objects you’re filtering, i.e.: blogposts, products, events, etc.
  2. Switch filters and filter names so that it makes sense for you, i.ei.: tags, categories, kind, etc.