In a previous post I added navigation links. I liked the solution because it was simple, but since it was working only chronologically sometimes it didn’t make sense where the links would take you. The next post sometimes wouldn’t relate to the current one, and if the current one is a post that belongs to a series of posts that are closely related, the link needs to take you to the exact correct place.

Solution Concepts

I decided the previous/next post should be the previous/next post in the same category. I wanted the URL and link text accessible through the front matter, to keep the liquid code simple. I used a hook to make that happen (plugins/assign_navigation.rb).

For the edges of the category posts (first and last posts of a category) I decided to add a link to the category page. This link, differently from the previous/next links, is not specific to a post but specific to a category (common for all posts in a category). I added that information in a data file (_data/categories-metadata.json).

In the end not only the new hook was needed but I also had to do two updates to the old hook (_plugins/assign_categories.rb) that assigned categories: first, to make it run earlier (as :site, :post_read) for the categories to be used later by the new hook, and second to also bring the edge cases data from the data file to the post front matter (this is used by the liquid script when previous/next is nil).

Solution Code

The liquid code now looks like this (the next/previous data is composed of front matter variables that come from the new hook, and edge cases are front matter variables that come from the old hook, now updated, using the data file):

<!-- page navigation -->
<div class="PageNavigation">
  {% if page.previous_post %}
    <a class="prev" href="{{page.previous_post.url}}">&laquo; {{page.previous_post.title}}</a>
  {% else %}
    <a class="prev" href="{{page.category_page_url}}">&laquo; See all {{page.category_page_title}}</a>
  {% endif %}
  {% if page.next_post %}
    <a class="next" href="{{page.next_post.url}}">{{page.next_post.title}} &raquo;</a>
    {% else %}
    <a class="next" href="{{page.category_page_url}}">See all {{page.category_page_title}} &raquo;</a>    
  {% endif %}
</div>

The old hook for assigning categories, now updated, looks like this:

# _plugins/assign_categories.rb
Jekyll::Hooks.register :site, :post_read do |site|
    puts "Assigning categories based on folder structure after posts are read"

    categories_metadata = site.data['categories-metadata']

    site.posts.docs.each do |post|
      # Extract the category from the post's directory - that will be the category name
      dirname = File.basename(File.dirname(post.path))
      category_name = dirname
      # Assign the category to the post's front matter
      if category_name != "_posts" && (post.data['categories'].nil? || post.data['categories'].empty?)
        post.data['categories'] = [category_name]
        puts "\tCategories assigned to post #{post.data['title']}: #{category_name}"
        # Add category related data from _data dir
        if categories_metadata.key?(category_name)
            metadata = categories_metadata[category_name]
            post.data['category_page_title'] = metadata['page_title']
            post.data['category_page_url'] = metadata['page_url']
            puts "\tAdded category metadata :\n\t\tCategory page title `#{post.data['category_page_title']}`, \n\t\tCategory page url `#{post.data['category_page_url']}`"
          else        
            puts "\tNo metadata found for category #{category_name} in categories-metadata.json"
        end            
      end
    end
  end  

And this is the category navigation links hook:

# _plugins/assign_navigation.rb
Jekyll::Hooks.register :site, :pre_render do |site, payload|
    puts "Starting Category Navigation Generator"
  
    site.posts.docs.each do |post|
      puts "\tProcessing post: #{post.data['title']} in categories: #{post.data['categories']}"

      post_categories = post.data['categories'] || []
  
      category_posts = site.posts.docs.select do |p|
        p_categories = p.data['categories'] || []
        p_categories.sort == post_categories.sort
      end
  
      category_posts.sort_by! { |p| p.date }
  
      current_index = category_posts.index(post)
      if current_index
        if current_index > 0
          post.data['previous_post'] = category_posts[current_index - 1]
          puts "\t\tPrevious post: #{category_posts[current_index - 1].data['title']}"
        else
          post.data['previous_post'] = nil
          puts "\t\tPrevious post: nil"
        end
  
        if current_index < category_posts.length - 1
          post.data['next_post'] = category_posts[current_index + 1]
          puts "\t\tNext post: #{category_posts[current_index + 1].data['title']}"
        else
          post.data['next_post'] = nil
          puts "\t\tNext post: nil"
        end
      end
    end
  
    puts "Category Navigation Generator finished."
  end