Blog archive - filtering articles by months

‘Blog archives’ tutorial in Kirby Docs is not ready yet and tutorial on filtering by dates is beyond my skills :disappointed_relieved: Forum search proves that most people here deal with filtering as smth obvious, so may be forum ninjas can help me with my lamer question…

I want to have smth like this in my navbar in order to get lists of articles sorted by months:

CSS:

nav ul ul {display: none;}
.years:focus ~ .sub-menu,
.years:active ~ .sub-menu,
.years:hover { display: inline; }

HTML:

<ul class="column"><h3>Archives</h3>
    <li><a href="javascript:void(0);" tabindex="1" class="years">2016</a>
    <ul class="sub-menu">    	
    	<li><a href="#">April</a></li>
    	<li><a href="#">May</a></li>
    	<li><a href="#">June</a></li>
    	<li><a href="#">July</a></li>
    	<li><a href="#">September</a></li>
    </ul></li>
    <li><a href="javascript:void(0);" tabindex="1" class="years">2017</a>
    <ul class="sub-menu">    	
    	<li><a href="#">January</a></li>
    	<li><a href="#">March</a></li>
    	<li><a href="#">September</a></li>
    	<li><a href="#">November</a></li>
    	<li><a href="#">...so on</a></li>
    </ul></li>
</ul>

Possibly if I try hard enough I manage to edit each month link manually, using docs instructions on filtering by date, but the question is: how to make this work automatically, to make appear only those months, that contain articles (yeah, my blogging style is very reluctant). Another question is if the result of these filtering will appear on ‘defalt’ template or should I make special arrangements for that? At the moment my search results are using special template and tag filtering results apper in the main blog template.

The code for the archive list depends on your content structure.
The easiest way would be to use the following structure:

content/blog/
  2016/
    year.txt
    01/
      month.txt
      my-amazing-article/
        article.txt
    02/
    03/
    ...

Basically you create subpages for each year and subpages for each month in the year pages.

Your archive list code would then be:

<ul class="column"><h3>Archives</h3>
    <?php foreach(page('blog')->children() as $year): ?>
    <li><a href="javascript:void(0);" tabindex="1" class="years"><?php echo $year->title() ?></a>
    <ul class="sub-menu">    	
    	<?php foreach($year->children() as $month): ?>
    	<li><a href="<?php echo url('blog') ?>/year:<?php echo $year->uid() ?>/month:<?php echo $month->uid() ?>"><?php echo $month->title() ?></a></li>
    	<?php endforeach ?>
    </ul></li>
    <?php endforeach ?>
</ul>

If you click on one of those links, the URL will contain the year and month.
In your controller or template, you can then get those values using param('year') and param('month'):

$articles = $page->index()->filterBy('template', 'article.txt');
if($year = param('year')) $articles = $articles->filter(function($page) use($year) { return $page->date('Y') == $year });
if($month = param('month')) $articles = $articles->filter(function($page) use($month) { return $page->date('m') == $month });

$articles will now contain all matching articles.
Let me know if something doesn’t work or if you have further questions.

1 Like

Thank you, Lukas, I appreciate your answer very much. Everything looks quite clear to me. But I won’t use this method, as I’m not using the panel and publish directly through Dropbox, so simple plain folder structure outweighs archives feature in my mind. @bastianallgeier I think this answer can be used for Blog Solution Docs section :slight_smile:

I think with Kirby 2.3 you can use the $pages->group() method with a callback (a function that will be executed for each page and which should return a value you want to group with).

The doc is here but there’s no example, sadly:
https://getkirby.com/docs/cheatsheet/pages/group
But reading the source code (which is here if you’re curious), I think your callback must return a string or number value.

So you could have something like this:

<?php
function pageYear($p) {
  return $p->date('Y'); // year, e.g. "2016"
}
function pageMonth($p) {
  return $p->date('F'); // month name for the current locale, e.g. "January"
}
$posts = page('blog')->children();
?>

<?php foreach ($posts->group('pageYear') as $year => $yearList): ?>
  <h2><?php echo $year ?></h2>
  <?php foreach ($yearList->group('pageMonth') as $month => $monthList): ?>
    <h3><?php echo $month ?><h3>
    <ul>
    <?php foreach ($monthList as $post): ?>
      <li><a href="<?php echo $post->url() ?>"><?php echo $post->title() ?></a></li>
    <?php endforeach; ?>
    </ul>
  <?php endforeach; ?>
<?php endforeach; ?>

Haven’t actually run this code but it should be working :slight_smile:

It’s not really simple to write or even to read if you don’t have much programming experience, but unless Kirby adds the logic above as a feature it can’t be made much simpler.

3 Likes

@fvsch: That looks good, if it works, I’ll add it to the docs as an example.

Great thanx, Fvsch!
That is not exactly what I wanted but seems it’s the best what can be done without site structure tweaking.
I styled it my way and it goes to work :slight_smile:

I what way is it different from what you wanted to achieve?

The result of @fvsch solution is multilevel list of links, located in one place (navbar in my case).
I wanted ‘months’ links lead to a new page containing that month articles, styled as main blog page - the same way as my search results, or articles filtered by tags are shown.

Oh, that can be fixed quite easily. I’ll get back to this later if nobody has come up with a solution in the meantime.

1 Like

Alright, let’s see.

  1. we need a link with the year and the month as parameter
  2. we remove the list of articles

This leaves us with the following code:

<?php
function pageYear($p) {
  return $p->date('Y'); // year, e.g. "2016"
}
function pageMonth($p) {
  return $p->date('F'); // month name for the current locale, e.g. "January"
}
$posts = page('blog')->children()->visible();
?>

<?php foreach ($posts->group('pageYear') as $year => $yearList): ?>
  <h2><?php echo $year ?></h2>
  <?php foreach ($yearList->group('pageMonth') as $month => $monthList): ?>
    <h3><a href="<?= page('blog')->url() . '/year:' . $year . '/month:' . $month ?>"><?php echo $month ?></a><h3>
    
  <?php endforeach; ?>
<?php endforeach; ?>

Now all you get is a list of years with their months.

The link links to the blog page, with two parameters year and month. In your blog controller you can now filter by those parameters.

<?php
if($year = param('year') && $month = param('month')) {
  $posts = $page->children()->visible()->filter(function($p) use($year, $month) {
    return $p->date('Y') === $year && $p->date('F') === $month;
  });
} else {
  $posts = $page->children()->visible();
}
?>

Hope I haven’t forgotten any braces or semicolons …:blush:

Trying to combine your code with mine already existing I’ve got smth like:

<?php
if($year = param('year') && $month = param('month')) {
  $posts = $page->children()->visible()->filter(function($p) use($year, $month) {
    return $p->date('%Y') === $year && $p->date('%B') === $month;
  });
} else
    
$articles = $page->children()->visible()->flip(); if($tag = param('tag')) { $articles = $articles->filterBy('tags', $tag, ','); } $articles = $articles->paginate(6); ?>

AND

Notice: Undefined variable: articles in /opt/lampp/htdocs/test/site/templates/blog.php on line 14 (last line in code above)

Fatal error: Call to a member function paginate() on a non-object in /opt/lampp/htdocs/test/site/templates/blog.php on line 14

You mix $posts and $articles, depending what you pass to your template, you should make a decision, which variable you want to use for your articles :wink:

BTW: When posting blocks of code you can improve readability by wrapping code blocks within three backticks at the beginning and the end of a block on a separate line. I have corrected your code above. To see how it works, click on the pencil icon of your post. Thank you.

This bit of code now only uses $articles, note that I have changed the order of your if-statements:

<?php
if($year = param('year') and $month = param('month')) {

  $articles = $page->children()->visible()->filter(function($p) use($year, $month) {
    return $p->date('Y') === $year && $p->date('F') === $month;
  });
} else if($tag = param('tag')) {
  $articles = $page->children()->visible()->filterBy('tags', $tag, ',');
} else {
  $articles = $page->children()->visible()->flip();
}

$articles = $articles->paginate(6);
?>

Thank you, texnixe. For some reason this code produces regular unsorted list of articles when opening links like siteurl.com/year:2016/month:April
At the same time tag filtering works fine.

I modified the code in my last post a bit, it should work now (at least I hope it does :pray:).

At 3 A.M.?! Sonja, it works! You make me finally add that small snippet ‘made with kirby & love’ in my footer. Tnank you. Blog archive issue is solved.

2 Likes

Back to polishing… My taglist looks like:

<?php $tags = $pages->children()->visible()->pluck('tags', ',', true); asort($tags); ?>
<?php $p = kirby()->request()->params()->tag(); ?>
<ul>
<?php foreach($tags as $tag): ?>
<li>
 <a class="tag<?php ecco($tag == $p, ' tag-active') ?>" href="<?php echo url(url::paramsToString(['tag' => $tag])) ?>"><?php echo html($tag) ?>
    </a>
    </li>
    <?php endforeach ?>
</ul>

It behaves similarily to the code discussed above except for the feature with the “active” class, added to the opened link. How to add this functionality to the month filtering code?

What functionality do you mean to add? The active class? Try this:

<?php 
$m = param('month');
$y = param('year');
foreach ($posts->group('pageYear') as $year => $yearList): ?>
  <h2><?php echo $year ?></h2>
  <?php foreach ($yearList->group('pageMonth') as $month => $monthList): ?>
    <h3><a class="<?php ecco($year == $y && $month == $m, ' active') ?>"href="<?= page('blog')->url() . '/year:' . $year . '/month:' . $month ?>"><?php echo $month ?></a><h3>
    
  <?php endforeach; ?>
<?php endforeach; ?>
1 Like

:relieved::heart_eyes: