Dynamic Routes Question

This is probably a weird question but I’ll ask it anyway.
I’m coding a site and I have a few categories I want to use to filter the content.

Now I have a folder structure for the single page that looks like this

home/year/product

I assign the category using a select field…

…that I popolate fetching values from a tag field in the stored on the home template.

This way if I want to add a new category I just add a new tag on the home page and I’m done.

Now, what I want to do is to have clean urls across the site.
For example when visiting /tech/ I want to see only the products with that particular category.

I already done all this and my only problem is with the route.
The action part of the route is ok and is working fine.

'action' => function($category) {
    return array('home' , ["category" => $category]);
 }

What I’m trying to find a solution to is the pattern.
I know I can use something like

'pattern' => "(:any)"

But this is obviously going to affect basically every single page of the site.
Also I know I can do something like this:

'pattern' => "(fashion|tech|misc)"

In order to get only the routes that I want but this is clearly not dynamic.

So my question is, is there a way to have some sort of dynamic route that acts like this (foo|bar|baz) but I can set using values coming from a field? Maybe by passing an array or a formatted string.

I tried a bunch of things but none of them seems to work

1 Like

Yes, that’s possible:

<?php
$patterns = page('home')->categories()->split(',');
c::set('routes', array(
  array(
    'pattern' => $patterns,
    'action'  => function() {
      return array('home' , ["category" => $category]);
    }
  )
));
1 Like

Sadly no.
I just tried and the only thing I get is the error page as if the page was not found in kirby.

I double checked and

page('home')->categories()->split(',')

Is returning the correct value when used in a normal page template.
I think the problem is that is not fetching the value from the filed when used in the config file.

I tried to pass the patterns to the template…

return array('home' , ["category" => $category , "patterns" => $patterns]);

And if I print the pattern variable in the template I get nothing, not even an empty array.
But if I do this

return array('home' , ["category" => $category , "patterns" => ['foo' , 'bar']]);

I get the correct array in return.

Also there’s another related problem.
If I manually pass an array to the pattern like this…

'pattern' => ['foo' , 'bar']

I correctly get the home in return when I visit .com/foo and .com/bar but the $category variable is empty

'action' => function($category) {
    return array('home' , ["category" => $category]);
}

So I’m wandering if the variable passed to the function only gets a value when there’s a placeholder like (:any)

Yes, you are right, it didn’t test this with the split() function. What I did, was doing this in a starterkit, which works perfectly.

$patterns = page('projects')->children()->pluck('tags', ',', true);
c::set('routes', array(
  array(
    'pattern' => $patterns,
    'action'  => function() {
      return page('contact');
    }
  )
));

Just wondering why it does not work with split. In your case, you could also pluck the categories from the child pages though, which would avoid using tags that are not used.

which would avoid using tags that are not used

This is not a big deal since all the tags are going to be used.

Anyway, my main problem is still there though.
If I do this:

array(
    'pattern' => ['foo' , 'bar'],
    'action'  => function($category) {
        return array('home' , ["category" => $category]);
    }
)

And I dump the $category variable on the /foo page I get NULL while if I do this

'pattern' => '(foo|bar)',

I correctly get

string(3) "foo"

So even if I manage to pass the content of my tags to the pattern argument I’ll still have a problem using the route since I can’t access the value of the $category variable.

I see, that’s because category does not get defined. Then try this:

$pattern = page('projects')->children()->pluck('tags', ',', true);
c::set('routes', array(
  array(
    'pattern' => '(' . implode("|", $pattern) . ')',
    'action'  => function($category) {
      return array('home' , ["category" => $category]);
    }
  )
));
```

Ok the implode approach works but I still have a problem fetching the values from the tag field.
If I do this in the config file

page('home')->categories()->split(',')

Ant then try to dump it in the page I get the entire field object in return

Field Object ( [page] => Page Object ( [kirby] => Kirby Object...

While the same code put in the template returns the correct field value.
This happens no matter what method I use so it’s not a problem with the split() or with pluck().

Yes, that why I suggested to use pluck(). I currently have no other idea, since it does not seem possbile to use split() in this context.

Edit: new idea:

$pattern = explode(',', page('home')->categories()->value());
1 Like

Ok we have a winner:

$pattern = '(' . implode("|" , page('products')->grandChildren()->pluck('category' , ',' , true)) . ')';

c::set('routes', array(
  array(
    'pattern' => $pattern,
    'action'  => function($category) {
        return array('home' , ["category" => $category]);
    }
)

Have you seen my last edit, maybe it’s more performant.

Read it while I was typing my reply.
Gonna give it a try now and see if it works :wink:

Nope, doesn’t work.
I think the overall problem is that the pattern is expecting to get either a string or an array but if you pass it an array you don’t get the value in return when you then do something inside the action function.

Anyway, the implode solution works even though it’s clearly not the most elegant :wink:

I think I might have found an easier solution.

$pattern = '(' . page('products')->categories()->value() . ')';

and in the products blueprint

categories :
    label     : Categories
    type      : tags
    index     : self
    lower     : true
    separator : |

Thanks for your help btw. Always appreciated :kissing_closed_eyes:

Well, what I meant was this, use explode() in conjunction with implode()

$pattern = explode(',', page('home')->categories()->value());
c::set('routes', array(
  array(
    'pattern' => '(' . implode("|", $pattern) . ')',
    'action'  => function($category) {
      return array('home' , ["category" => $category]);
    }
  )
));

Or even better:

$pattern = str_replace(',', '|', page('home')->categories()->value());
c::set('routes', array(
  array(
    'pattern' => '(' . $pattern . ')',
    'action'  => function($category) {
      return array('home' , ["category" => $category]);
    }
  )
));
```
But using `|` as tag separator is a good idea as well.

Well, what I meant was this, use explode() in conjunction with implode()

Got it. That would probably work as well but the idea of doing both an implode and an explode drives me crazy :grin:
The string replace might also be a good idea because I now see there’s a bug using | as a separator in the panel and looks like it’s a known bug.

Anyway, the important thing is that now the route works and I can move on.
Thanks for the help :wink:

Hi Manuelmoreale,

As I tried for a while to implement the same feature (categories, filter content and simple route), would you mind sharing the detailed code for the categories, the select field, filter content and route ?
Actually I have a folder named “Dossiers” with subfolders for each single article and I would like to set 5 or 6 categories and filter all article by categories.
Since my php skills are very limited (actually almost null) it’s difficult for me to use adequately the exchanged discussion above.
Thank you

Hey @hardboiled I’m more than happy to share my code.
Most of the discussion above is due to the fact that I try to make things as dynamical as possible but you can get the same result with a lot less code.

Anyway, here’s my current solution.

Folder Structure

Home
Products
  Year
    Product

The year folder is there just to avoid having a ton of content in the products folder since this is a blog and we post almost daily.

Template : home.php

The content is coming from the controller described below.

<?php snippet('header') ?>

<?php
if ($products) :
    foreach ($products as $product) :
        snippet('product' , array('product' => $product));
    endforeach;
endif;
?>

<?php snippet('footer' , array('pagination' , $pagination)) ?>

Controller : home.php

I’m using one simple controller for both home page (where I show all the products) and for the categories.
This is fairly simple I just fetch all the products then check if there’s a category coming from the route and if that’s the case filter the products by category. Then I filter by date (because i want to exclude posts with date set in the future since those are programmed) and then I sort the posts by date and paginate.

return function($site, $pages, $page, $args) {

# Fetch all the products
$products = page('products')->grandChildren()->visible();

# Filter products by category
if (isset($args['category'])) :
    $products = $products->filterBy('category' , '==' , $args['category']);
endif;

# Filter by date to exclude programmed posts
$products = $products->filterBy('date', '<', time());

# Sort and paginate
$products = $products->sortBy('date' , 'desc')->paginate(60);

# create a shortcut for pagination
$pagination = $products->pagination();

# Pass $products, $pagination and $category to the template
return compact('products', 'pagination', 'args');

};

Blueprint : product.yml

The relevant part of the blueprint is this

category :
    label   : Category
    type    : select
    options : field
    help    : Product category
    field   :
        page      : products
        name      : categories
        separator : ,

As you can see the category is set using a simple select field.
The select is populated using the values coming from a tag field defined on the main product page but that’s just because this way I can add new category easily.
You can get the same result using a set of predefined categories. As I said earlier, this is just to keep things more flexible and easy to update.

Route

At the end I decided to go with the str_replace solution for the route.

$pattern = '(' . str_replace(',', '|', page('products')->categories()->value()) . ')';
c::set('routes', array(

array(
    'pattern' => $pattern,
    'action'  => function($category) {
        return array('home' , ["category" => $category]);
    }
)
));
4 Likes

Thanks for sharing your code example, @manuelmoreale :slight_smile:

Thanks a lot for sharing your code.
I’ll try to implement similar solution for my project website.

Let me know if you need help.