Categories based dynamic menu

So I’ve got a project idea that requires me to organize pages/posts into categories, and sub categories.
as an example here some possible scenarios

Category1->Technical Stuff->Page 1
Category1->Technical Stuff->Page 2
Category1->non Technical Stuff->Page 3
Category 2->Technical Stuff->Page 4
Category2->some other descriptor->Page 5

What I would like to build is a different menu dependent on what your viewing.
If you are currently browsing Category 1, the menu contains all the sub pages in category 1
If you are currently browsing Category 2, the menu contains all the sub pages in category 2
If you are browsing a page that is not categorized the menu contains all the parent categories.

There could also be an arbitrary number of categories i.e.
Category 2->Technical Stuff->Software Stuff->Software Config->Page Name

I’ve currently got the “parent” categories menu working with

{% for category in site.categories %}
  {% capture category_name %}{{ category | first }}{% endcapture %}
  <li><a href="{{ site.url }}/categories/#{{ category_name | slugize }}">{{ category_name | slugsize }}</a></li>
{% endfor %}

Maybe pages or posts not both. You can have pages with dates that you treat like posts.

I would not use categories as it is a thing that only applies to posts.

Collections would be neat way to organize the top level. You can have a layout using for all collections and dynamically lookup the collection for the current page (or the first part of the path) and use that to render an appropriate navbar that shows only pages in a given collection.

When it comes to nesting content multiple levels, for my project I didn’t use categories or collections. I used an index.md page with layout: listing set. Then the listing layout looks for pages and folders at the current level only (and not 1 level down or 1 level up) and builds up a menu.

Here is my POC with notes in README

I have a Cookbook project which implements my approach in a more real world project.

I hope this gives you some ideas.

Code Cookbook

My menu is in the center part of the page as Pages and Topics headings. But you can rework into a navbar.

Here I have a page where to can follow the breadcrumbs up or you can navigate down using the Topic buttons.

My repo

https://github.com/MichaelCurrin/code-cook

I actually moved my layouts and includes to a separate repo forked from minima so I can keep my files focused on content. Which means you can change your main content and point to my theme and you’ll get nested content and menus and breadcrumbs.

Here is my theme, forked from Minima

1 Like

Oh, thats some interesting reading! Thanks.

I’m essentially trying to replace a wiki, which is only really used as a documentation repo.
All things could be done in markdown, its the templating and "taxonomy " im struggling with.

I’ll read through your code over the weekend :grin: thanks

ah, I was reading your nested menus repo earlier this week.
My only (completely selfish) thought was I don’t really want to have a complicated src directory structure.

in my ideal world I would have $src/_pages (or $src/_posts would fine too :slight_smile: )
then in front matter I could do something like:
categories: category1
subcategories: tech->software->config

It sounds like your code cookbook is exactly what I’m building, but perhaps thought out in a different way . .which is awesome :+1: but as above uses a complicated src dir structure (which I may have to do, I’m just trying to be lazy in the future :stuck_out_tongue: )

Oh right. So my approach is to categorize something based on the file structure.

e.g. Category 2/Technical Stuff/Software Stuff/Software Config/Page Name.html

While your approach is to categorized based on frontmatter.

categories: 'Category 2' 'Technical Stuff' 'Software Stuff' 'Software Config'

For posts with categories, using categories actually makes the output URL path use that.

For non posts you would have to figure out a permalink or plugin solution.

I have a ton of files so I don’t want to mix them in a single folder. I like the nesting. The output path is predictable. And the folder name is used once. So I have rename a category I change the folder. I don’t have to find and replace the use of the category in metadata. Plus there is no risk of typos for inconsisties like if I used JavaScript and Javascript as categories they would be different.

I think instead of categories and subcategories as metadata, I think you could use collections for categories. Then at least you have split the files into buckets by that level 1. Then you can use the subcategories a few levels down in the metadata.

Another reason to use file based structure is that the order is fixed based on the path.
So JavaScript/NPM is always the way that nests. So I won’t accidentally do NPM/JavaScript for some category fields and JavaScript/NPM for others. It gets more complicated if you have to keep the order more levels down.

You may also want to look at tags if you don’t want a hierarchy within your top level categories. For tags on Jekyll posts, the order doesn’t matter and the output path doesn’t matter. But you can list the tags on the post. Or have a page of just tags. And maybe items in them.

well, I haven’t used collections before, but as I understand it I could have
$src/_category1
$src/_category2
as a src directory structure

then I could use with {% for parentTaxonomy in site.parentTaxonomies %}
Is that correct?

And then you’re suggesting combining that with tags, to categorize lower level pages?
I also haven’t used tags :open_mouth:

I like this idea I think :smiley:

I am not sure what you mean so I am going to cover a collection example

At the repo root:

_first_category/
  foo.md
  bar.md
_second_category/
  bazz.md
index.md

You would also have to setup config as

collections:
  first_category:
    output: true
  second_category:
    output: true

Not lack of leading underscores

You would also need default layouts setup, in your config.

If you want a listing for each collection, you could do first_category.md at the root and use a permalink like /:collection/ so it becomes /first_category/
And then subpages as /first_category/foo.html etc.

To get items in a collection

{% for f in site.first_category %}
- [{{ f.title }}]({{ f.url | relative_url }})
{% endfor %}

Oh and in general dashes are better for SEO. So like _first-category/ and contact-me.md

That was collections. Without tags and categories.

If you convert all your pages to posts, then you can use categories and tags out the box.

If you do pages in a collection rather, you effectively get a category by folder name without a categories field.

And you can easily add tags field to pages even though it is not built in. You’ll be able to use tags to display on a page
You just have to write the code yourself to get all unique tags across all pages, and perhaps only tags for first category on that collection homepage and only tags for second category on its homepage. To avoid lumping all tags together.

And again, tags are useful for no hierarchy. Like you have a 100 pages in first collection and some have 1 tag and some have 5 tags.
But I prefer having a nested folders for mine. And renaming folders instead of renaming a tag across many pages which use it.

I might actually add tags to my project as an additional level for my nested structure so I can relate Python pages in the CI category and Make category together, for example.

If you must use categories with collections other than posts, try using the jekyll-tagories plugin.

Thanks for sharing. Note for OP that the plugin suggested is not supported on GH Pages.

1 Like

Here’s an update (and where I’m stuck … probably with bad logic :laughing: )

I have a collection

collections:
  docs:
    title: Documentation
    permalink: /:path/
    output: true

and then a file:

cat _docs/Personal/a-cat-named-tivo.md

---
title: A Cat Named Tivo
space: Personal
category: Animals
---

text

and here is another file

cat _docs/Personal/Lexi-the-Spaniel.md

---
title: Lexi the Spaniel
space: Personal
category: Animals
---

text

and then in my layout:

<ul>
    {% assign grouped = site.docs | group_by: 'space' %}
    {% for group in grouped %}
    <li class="nav-item top-level {% if group.name == page.space %}current{% endif %}">
        {% assign items = group.items  %}
        <a href="{{ site.baseurl }}{{ items.first.url }}">{{ group.name }}</a>
        <ul>
        {% for item in items %}
            <li class="nav-item {% if item.url == page.url %}current{% endif %}"><a href="{{ site.baseurl }}{{ item.url }}">{{ item.title }}</a></li>
        {% endfor %}
        </ul>
    </li>
    {% endfor %}
</ul>

which results in a menu that looks like:

Personal
  - A cat named Tivo
  - Lexi the Spaniel

What I can’t figure out is how to group the items by category, then echo the category
so that I can have a menu that looks like

Personal
  - Animals
      - A cat named Tivo
      - Lexi the Spaniel
1 Like

You need to group twice. One you have the space group, then use that narrow list of items in one of those groups (like Personal) and group those by categories (like Animals) that are within that space.

Then on the next space group, you’ll have items with relevant categories (maybe different to the categories used in the previous space group) and you group by those.

e.g.

{% assign space_groups = site.docs | group_by: 'space' %}
{% for s_group in space_groups %}
  {{ s_group.name }}

  {% for s in s_group.items %}
     {% assign category_groups = s | group_by: 'category' %}

     {{ category_groups.name }}

     {% for c_item in category_groups.items %}
        {{ c_item.title }}
     {% endfor %}
{% endfor %}

Result will be like

Personal # space
  Animals # category
     Dog
     Cat
  Plants
     Catcus
     Daisy
Business # space
   Plants  # category. same name as plant category uses earlier but now only with Business + Plants items.
     Oak

Liquid Exception: no implicit conversion of String into Integer in /home/paul/git-repos/docs/_layouts/default.html

             <ul>
                 {% assign space_groups = site.docs | group_by: 'space' %}
                 {% for s_group in space_groups %}
                     <li class="nav-item top-level {% if s_group.name == page.space %}current{% endif %}">
                         <a href="{{ site.baseurl }}{{ s_group.items.first.url }}">{{ s_group.name }}</a>
                         {% for s in s_group.items %}
                         {% assign category_groups = s | group_by: 'category' %}
                         <ul>
                             <li class="nav-item second-level {% if category_groups.name == page.space %}current{% endif %}">
                             <a href="{{ site.baseurl }}{{ category_groups.items.first.url }}">{{ category_groups.name }}</a>
                             <ul>
                             {% for item in category_groups.items %}
                                 <li class="nav-item {% if item.url == page.url %}current{% endif %}"><a href="{{ site.baseurl }}{{ item.url }}">{{ item.title }}</a></li>
                             {% endfor %}
                             </ul>
                         </ul>
                         {% endfor %}
                     </li>
                 {% endfor %}
             </ul>

Here is the complete error

  Generating...
   Jekyll Feed: Generating feed for posts
  Liquid Exception: no implicit conversion of String into Integer in /home/paul/git-repos/docs/_layouts/default.html
Traceback (most recent call last):
    72: from /home/paul/gems/bin/jekyll:23:in `<main>'
    71: from /home/paul/gems/bin/jekyll:23:in `load'
    70: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/exe/jekyll:15:in `<top (required)>'
    69: from /home/paul/gems/gems/mercenary-0.4.0/lib/mercenary.rb:21:in `program'
    68: from /home/paul/gems/gems/mercenary-0.4.0/lib/mercenary/program.rb:44:in `go'
    67: from /home/paul/gems/gems/mercenary-0.4.0/lib/mercenary/command.rb:221:in `execute'
    66: from /home/paul/gems/gems/mercenary-0.4.0/lib/mercenary/command.rb:221:in `each'
    65: from /home/paul/gems/gems/mercenary-0.4.0/lib/mercenary/command.rb:221:in `block in execute'
    64: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/commands/build.rb:18:in `block (2 levels) in init_with_program'
    63: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/command.rb:91:in `process_with_graceful_fail'
    62: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/command.rb:91:in `each'
    61: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/command.rb:91:in `block in process_with_graceful_fail'
    60: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/commands/build.rb:36:in `process'
    59: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/commands/build.rb:65:in `build'
    58: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/command.rb:28:in `process_site'
    57: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:80:in `process'
    56: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:210:in `render'
    55: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:530:in `render_docs'
    54: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:530:in `each_value'
    53: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:531:in `block in render_docs'
    52: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:531:in `each'
    51: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:532:in `block (2 levels) in render_docs'
    50: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/site.rb:547:in `render_regenerated'
    49: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/renderer.rb:63:in `run'
    48: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/renderer.rb:93:in `render_document'
    47: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/renderer.rb:163:in `place_in_layouts'
    46: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/renderer.rb:194:in `render_layout'
    45: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/renderer.rb:131:in `render_liquid'
    44: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:36:in `render!'
    43: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:70:in `measure_time'
    42: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:37:in `block in render!'
    41: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:63:in `measure_bytes'
    40: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:38:in `block (2 levels) in render!'
    39: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:59:in `measure_counts'
    38: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/liquid_renderer/file.rb:39:in `block (3 levels) in render!'
    37: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/template.rb:220:in `render!'
    36: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/template.rb:207:in `render'
    35: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/template.rb:242:in `with_profiling'
    34: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/template.rb:208:in `block in render'
    33: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:82:in `render'
    32: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:103:in `render_node_to_output'
    31: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:79:in `render'
    30: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:150:in `render_segment'
    29: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/context.rb:123:in `stack'
    28: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:158:in `block in render_segment'
    27: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:158:in `each'
    26: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:160:in `block (2 levels) in render_segment'
    25: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:82:in `render'
    24: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:103:in `render_node_to_output'
    23: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:79:in `render'
    22: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:150:in `render_segment'
    21: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/context.rb:123:in `stack'
    20: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:158:in `block in render_segment'
    19: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:158:in `each'
    18: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/for.rb:160:in `block (2 levels) in render_segment'
    17: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:91:in `render'
    16: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/block_body.rb:103:in `render_node_to_output'
    15: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/tags/assign.rb:26:in `render'
    14: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/variable.rb:82:in `render'
    13: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/variable.rb:82:in `inject'
    12: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/variable.rb:82:in `each'
    11: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/variable.rb:84:in `block in render'
    10: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/context.rb:86:in `invoke'
     9: from /home/paul/gems/gems/liquid-4.0.3/lib/liquid/strainer.rb:56:in `invoke'
     8: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/filters/grouping_filters.rb:16:in `group_by'
     7: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/filters/grouping_filters.rb:16:in `group_by'
     6: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/drops/drop.rb:245:in `each'
     5: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/drops/drop.rb:245:in `each'
     4: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/drops/drop.rb:245:in `each'
     3: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/drops/drop.rb:246:in `block in each'
     2: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/filters/grouping_filters.rb:16:in `block in group_by'
     1: from /home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/filters.rb:430:in `item_property'
/home/paul/gems/bundler/gems/jekyll-31e152b0d0e3/lib/jekyll/filters.rb:442:in `read_liquid_attribute': no implicit conversion of String into Integer (TypeError)

Which line is that happening in? The error is too vague.

You can also start from a simple outline

<ul>
     {% assign space_groups = site.docs | group_by: 'space' %}
                 
</ul>

And keep adding stuff inside until it breaks

<ul>
  {% assign space_groups = site.docs | group_by: 'space' %}
  {% for s_group in space_groups %}
       {{ s_group | inspect }} <br>
  {% endfor %}
</ul>

UPDATE
Would be wonderful if you checked my logic :laughing: … but it’s working :+1:

 {% assign space_groups = site.docs | group_by: 'space' %}
 {% for s_group in space_groups %}
 {{ s_group.name  }}
     {% assign CATEGORIES = "" | split: ',' %}
     {% for s in s_group.items %}
         {% assign CATEGORIES = CATEGORIES | push: s.category | uniq %}
     {% endfor %}
     {% for cat in CATEGORIES %}
     {{ cat }}
     {% assign local_list = s_group.items | where: "category", cat %}
         {% for i in local_list %}
         {{ i.title }} - {{ site.baseurl }}{{ i.url }}
         {% endfor %}
     {% endfor %}
 {% endfor %}

Result:

Personal
    Animals
        Lexi the Spaniel - http://docs/Personal/Lexi-the-Spaniel/
        A Cat Named Tivo - http://docs/Personal/a-cat-named-tivo/
1 Like

Glad to hear.

Using an inner group_by would handle the category logic.

Or at least use map to iterate over an array and get an attribute on each item. The uniq filter is the run against the array of strings. You could use sort after that too.

 {% assign space_groups = site.docs | group_by: 'space' %}

 {% for s_group in space_groups %}
     {{ s_group.name  }}

     {% assign CATEGORIES = map: s, "category" | uniq %}

     {% for cat in CATEGORIES %}
         {{ cat }}

         {% assign local_list = s_group.items | where: "category", cat %}

         {% for i in local_list %}
             {{ i.title }} - {{ site.baseurl }}{{ i.url }}
         {% endfor %}
     {% endfor %}
 {% endfor %}

But your code is usable enough and you can fall back to it if you can’t find something better that works

1 Like

A final follow up (just because) and thanks @MichaelCurrin for your help :+1:

I have a Makefile that resizes images (using imagemagick), bundles and compiles javascript (using closure), then builds static docs. (I really should learn gruntjs)

Plus comments enabled using nodejs staticman, and search using lunr.

Now I have to move a bunch of data from a Confluence install, and a Mediawiki install into markdown :open_mouth:

Here’s a screenshot (I can’t send a link or repo, as there’s sensitive data :stuck_out_tongue: )

1 Like