Creating a virtual page with pagination?

Hi! New to Kirby and very grateful for these forums, which have been a huge help on my first project. I’m stuck on a feature for this project, and would love some advice.

I have a directory structure that looks something like this:

media/
  media-landing.txt
  series-name/
    series.txt
    episode-01-name/
      episode.txt
    episode-02-name/
      episode.txt

So far, so good. I can browse to media/series-name/episode-01-name/, or to any level up the tree from there, and the appropriate template renders.

What I’d like to do is create a path at podcasts/series-name/archive/, which dynamically shows a paginated archive of all episodes contained within a given series. But I’m not sure how to create pages that don’t exist in the filesystem.

I did find the documentation on virtual content, and on routing; is that the best approach here? How would that work with pagination?

Thank you!

Hi,
you don’t need a virtual content. Its enough to route with returning a virtual page object.
And pagination will work like every else.


return [
  'routes' => [
    [
      'pattern' => 'media/(:any)/archive',
      'action'  => function (string $slug) {
        if (!$series = page('media/' . $slug)) {
          return false; // falls through to the error page
        }

        return Page::factory([
          'slug'     => 'archive',
          'parent'   => $series,
          'template' => 'series-archive',
          'content'  => [
            'title' => $series->title()->value() . ' – Archive',
          ],
        ]);
      }
    ]
  ]
];

than you need a template in `site/templates/series-archive.php`

<?php
$series   = $page->parent();
$episodes = $series->children()
                   ->filterBy('intendedTemplate', 'episode')
                   ->sortBy('date', 'desc')
                   ->paginate(20);
$pagination = $episodes->pagination();
?>

<?php foreach ($episodes as $episode): ?>
  <a href="<?= $episode->url() ?>"><?= $episode->title() ?></a>
<?php endforeach ?>

and the pagination markup from the cookbook.

best

Thank you so much, @JanStieler. This is a huge help.

Unfortunately, it’s not quite working for me: whenever I link to media/slug/episodes/, I’m always routed to the error template. Here’s my routes definition:

  'routes' => [
    [
      'pattern' => 'media/(:any)/episodes/?',
      'action'  => function (string $slug) {
        // falls through to the error page
        if (!$series = page('media/' . $slug)) {
          return false;
        }
        return Page::factory([
          'slug' => 'episodes',
          'parent' => $series,
          'template' => 'series-archive',
          'listed' => false,
          'content' => [
            'title' => $series->title()->value() . ' archive',
          ]
        ]);
      }
    ]

I can confirm I have a site/templates/series-archive.php file. Do you have any suggestions on how I should start debugging this?

Thanks again for the help!

My immediate thought is the forward slashes after the episodes in the pattern, but that might not be it. I don’t actually do a lot of pagination on virtual pages myself.

Oh, actually, I can’t tell if you’ve defined $series pages object outside of this. If you haven’t it might help to define it here for clarity while debugging? You have the DOESN’T equals already, but just define it first, and then the != line will be simpler and the $ will exist for the page factory below?

$series = page(‘media/‘.$slug);

if ($series === null) {
   return false;
}

then return the page factory?

I love making my logic really lean, and not verbose and multi lined, but it can lock me up.

Somebody else might have an actual proper solution to this, but this is the only thing I can detect without creating it locally and not knowing your other files.
Also, your files setup block doesn’t show a typical Listed or Unlisted state of those Series or Episodes (in the episode.yml blueprint you can define the num: at the top of the file and use dates, for example…). But I assumed you were simplifying. If they’re Listed, then ignore this, since you prevent unlisted v pages from returning in your logic.

Wait, you currently prevent listed pages from returning. Just make sure that’s what you want. I usually gate evertyhing to only show listed pages in my logic.

I’m no pro, and I’d like to see where this goes, and I also hope it isn’t misleading.

Thanks so much, @luxuryluke! I really appreciate the reply, and the help. (Answers inline below.)

My immediate thought is the forward slashes after the episodes in the pattern, but that might not be it. I don’t actually do a lot of pagination on virtual pages myself.

Ha, same team: that was one of the first things I tried to debug. I get the error page whether or not the trailing /? follows /episodes.

But I assumed you were simplifying. If they’re Listed, then ignore this, since you prevent unlisted v pages from returning in your logic.

Good catch, and yep—I was simplifying! Sorry for the confusion.

Wait, you currently prevent listed pages from returning. Just make sure that’s what you want. I usually gate evertyhing to only show listed pages in my logic.

Oh that’s probably my misunderstanding: I thought 'listed' => false would apply to the factory-generated page, not the collection I’m building in the template. Just removed it. (And sadly, still getting the error page.)

Aw, man. Try dumping a $series id right after defining $series to see if it finds any:

$series = ...
dump($series?->id() ?? ’nothing found');
exit;

Oh my gosh, I’m a fool—my path had a glaring typo in it, @JanStieler’s solution works perfectly now.

Thank you both so much for the help! I’m moving into the trees immediately, never to be heard from again

Can relate, beep. This is nothing. I’ve posted some embarrassingly dolt-ish things.

I’m glad to see you here posting. Cheers!