Dynamic Routes Question

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.

Hi, sorry to bring this thread to life after months.

I’m not entirely sure how you are using your tagging system, but I think it’s close to what I am trying to achieve as well (see thread here).

Which is, using a route to transform this

site.dev/cerca/technique:ritratto

into

site.dev/cerca/ritratto

and basically filtering out the technique: bit of the url.

I tried to only adapt your route to my code like so

c::set('routes', array(
  array(
    'pattern' => 'cerca/(:any):',
    'action'  => function($technique) {
      return $array('cerca', ["technique" => $technique]);
    }
  )
));

and the route is not working, but would like to know first if you were trying to filter out the tag: part (categories) from your url or not! And eventually if you have any experience on how to do it?

Thank you

There’s an extra colon at the end of this line: 'pattern' => 'cerca/(:any):', which should be removed.

Thank you!

I also delete the $ sign in front of array, and now if I try to manually edit the url from

site.dev/cerca/technique:ritratto

to

site.dev/cerca/ritratto

the page loads correctly.

Still, my code to produce that url is as follows, and am not sure if I should edit it

<a class="c-button__tag" href="<?php echo url($page->url() . '/' . url::paramsToString(['technique' => str::slug($tag)])) ?>">
  <?php echo html($tag) ?>
</a>

Or, if I simply try to click on the tag link, I still get the same url path.

c::set('routes', array(
  array(
    'pattern' => 'cerca/(:any)',
    'action'  => function($technique) {
      return array('cerca', ["technique" => $technique]);
    }
  )
));

Yes, you can use a custom page method to create the desired URL.

OK, the route is actually not working—when I take out the technique: bit in the url it shows me all the page result (I have implemented a search function in the main page site.dev/cerca with a rule to show all results if the query does not meet the input criteria).

I tried to disable the search function and see what happens, and I still get displayed site.dev/cerca, instead of site.dev/cerca/tag.

I am thinking if, as a workaround, I could simply create a subpage of cerca, called technique, where to show all subpages of cerca with specific tags under technique, but I think I would still get the technique: bit, like so site.dev/cerca/technique:technique, right?

Have you adapted your controller as in the code snippet above?