Unable to route to a page

I want a page per year, displaying blog posts from that year, but am failing at the first hurdle – when I visit the URL for the year, I get an error from the route:

Call to a member function render() on null

My blog posts are in a structure like:

blog/
  2023/
    01/
      31/
        1_my-post/
          post.txt

Posts should be visible at urls like /blog/2023/01/31/my-post/.

I have this in my config.php:

return [
	'routes' => [
		[
			'pattern' => 'blog/(\d{4})',
			'action' => function ($year) {
				$data = [
					'year' => $year
				];
				# THIS LINE GENERATES THE ERROR:
				return page('year')->render($data);
			}
		]
	]
]

I’ve added a template at site/templates/year.php, a model at site/models/year.php, a blueprint at site/blueprints/pages/year.yml, and a controller at site/controllers/year.php.

Model:

<?php
class YearPage extends Page {
}

Controller:

<?php
return function ($page, $year) {
  return [
	'year' => $year
  ];
};

I’ve obviously misunderstood something simple, or made a typo, because I can’t see how to have page('year') in the route not be NULL.

But this page doesn’t exist.

And why do you need the route at all, given you already have the year folders in your structure?

What’s the name of the content file for the year pages? All you need is a template for that page that fetches all the descendents from the third level, so $page->index()->filterBy('intendedTemplate', 'post')

Maybe I don’t - clearly I’m misunderstanding some fundamentals. I thought that because I want these pages that list months to be generated programmatically, then there was no need for page files at something like blog/2023/year.txt. What would go in them? Would I have to create a new one every time I start a new year of blog posts? And do the similar for every new month, and every new day?

I’m not sure what you mean, sorry.

I have a template at site/templates/year.php.

This folder structure automatically gives you the following URLs

  • your.domain/blog
  • your.domain/blog/2023 ← this corresponds to the route you created, therefore the route is superfluous
  • your.domain/blog/2023/01
  • your.domain/blog/2023/01/31
  • your.domain/blog/2023/01/31/your-post-title

Given that your 2023 folder has a text file year.txt, you would create a template called year.php in /site/templates (if you haven’t yet), and inside that template, you can fetch all the post for that year like so:

<?php

foreach ($page->index()->filterBy('intendedTemplate', 'post') {
  echo $post->title();
}

By filtering the page tree that index() gives us by the post template, we ignore the month and day folders.

Thanks @texnixe

I don’t currently have a year.txt file within each year folder but I can do if necessary.

Once I’ve got this working, I’d also want pages at blog/2023/01/ and blog/2023/01/31/ listing sub-pages, and the same for all the other month/day pages.

Does this mean that if I want to write blog post on 1st Jan 2025, using the Panel, I’d have to make a new page at 2025/year.txt, a new page at 2025/01/month.txt, a new page at 2025/01/31/day.txt, and only then start the new post?

This seems too laborious and I wonder if there’s a better way to do this? The main thing I want to do is keep my current (non-Kirby) site’s post URLs, and have “index” pages at each of the year, month, and day URLs.

Yes, you can do that in the same way. Note that these subfolders for month and day also need text files in them, otherwise they will fall back to using the default.php folder.

Theoretically, if there is no layout difference for year, month, day, they could use the same template and therefore content text file name.

So this is what I’d have to do when I want to write a new post?

Does this mean that if I want to write blog post on 1st Jan 2025, using the Panel, I’d have to make a new page at 2025/year.txt , a new page at 2025/01/month.txt , a new page at 2025/01/31/day.txt , and only then start the new post?

Is there no way to have URLs like blog/2023/01/31/my-post/ without having to manually create any missing ancestor pages above the post?

Well, what I understood from what you posted above, that’s the folder structure you already have in place?

Of course, you could also have a flat structure where all posts are direct descendants of blog, and then create the required url structure via routes.

It all depends on the number of pages altogether, with many posts, a nested structure can be the better option.

But to return to the route example for the year you posted above, it would have to be adapter like so (and then all the logic would have to happen inside the blog template/controller

return [
	'routes' => [
		[
			'pattern' => 'blog/(\d{4})',
			'action' => function ($year) {
				$data = [
					'year' => $year
				];
				# THIS LINE GENERATES THE ERROR:
				return page('blog')->render($data);
			}
		]
	]
]

But yes, for the nested folder structure as outlined above , you would have to create the years, months and day folders first, if they don’t exist yet.

You can also just create years, and skip the month and day folders, but still create the required url structure via routes.

Sorry, maybe I’ve led you in a wild goose chase - I can change the folder structure if that makes it easier! I initially assumed it would be necessary to match the URL structure I wanted. I have 1800 posts spanning 25 years.

So maybe I split them only into year folders, if that will be faster than having them in a single folder, and use routes for the specific URLs for year, month, day and post.

I’ve made progress, and got things working, without yet changing the folder structure (which sounds like it would be a good idea, in terms of simplifying creating new posts).

I now have this as my routes:

	'routes' => [
		[
			'pattern' => 'blog/(\d{4})',
			'action' => function ($year) {
				$data = [
					'year' => $year
				];
				return page('blog')->render($data);
			}
		],
		[
			'pattern' => 'blog/(\d{4})/(\d{2})',
			'action' => function ($year, $month) {
				$data = [
					'year' => $year,
					'month' => $month
				];
				return page('blog')->render($data);
			}
		],
		[
			'pattern' => 'blog/(\d{4})/(\d{2})/(\d{2})',
			'action' => function ($year, $month, $day) {
				$data = [
					'year' => $year,
					'month' => $month,
					'day' => $day
				];
				return page('blog')->render($data);
			}
		]
	]

And I have a controllers/blog.php (which could be tidied up, and some error-checking added):

return function ($page, $year, $month=False, $day=False) {
  $posts = $page->index()->filterBy('intendedTemplate', 'post')->listed();

  if ($year) {
    if ($month) {
      if ($day) {
        $d = new DateTime("$year-$month-$day");
        $from_date = $d->format('Y-m-d 00:00:00');
        $to_date = $d->format('Y-m-d 23:59:59');
      } else {
        $d = new DateTime("$year-$month-01");
        $from_date = $d->format('Y-m-01 00:00:00');
        $to_date = $d->format('Y-m-t 23:59:59');
      }
    } else {
      $d = new DateTime("$year-01-01");
      $from_date = $d->format('Y-m-d 00:00:00');
      $d = new DateTime("$year-12-31");
      $to_date = $d->format('Y-m-d 23:59:59');
    }

    $posts = $posts
      ->filterBy('date', 'date >=', $from_date)
      ->filterBy('date', 'date <=', $to_date);
  }

  return [
    'posts' => $posts,
    'year' => $year,
    'month' => $month,
    'day' => $day,
  ];
};

And then in my templates/blog.php:

  <ul>
    <?php foreach($posts as $post): ?>
      <li><?= $post->title() ?></li>
    <?php endforeach ?>
  </ul>