In a previous post I showed you how to generate subnavigation links for each H2 in a Jekyll page. In this post, we'll build on that foundation and show you how you can add arbitrary levels of subnavigation based on H3, H4, etc.
Overview
I've broken this project down into a couple of steps:
- First, we will use nokogiri to pull out the sections defined by H3 tags "inside" of H2 tags
- Next, we'll use a cool trick to render arbitrary levels of sub navigation. We're going make a recursive template.
Before we get started, let's make something clear. When I refer to an H3 tag as being inside of an H2, I don't mean that it's literally nested. Instead, I'm referring to the situation that we see below:
<h2>Animals</h2>
<p>Here are some kinds of animals.</p>
<h3>Giraffe</h3>
<p>This section about giraffes logically belongs inside of the section about animals, even though the structure of the Dom doesn't define it as being nested</p>
<h3>Zebra</h3>
<p>Another section that logically belongs under "Animals"</p>
Breaking the Document into Sections
The obvious problem that we face when breaking an HTML document like the one above into sections, is that nothing is nested. Most of the tools for parsing HTML are built to work with nesting.
This isn't a deal breaker, but it does mean that we have to do a little bit more work. In the example below we find each H2 tag and then manually scan siblings for H3 tags.
I did get fancy and use a custom enumerator. If you have any questions about those, check out my blog post on them.
require "nokogiri"
class MySubnavGenerator < Jekyll::Generator
def generate(site)
parser = Jekyll::Converters::Markdown.new(site.config)
site.pages.each do |page|
if page.ext == ".md"
doc = Nokogiri::HTML(parser.convert(page['content']))
page.data["subnav"] = doc.css('h2').map do |h2|
to_nav_item(page, h2).tap do |item|
item["children"] = subheadings(h2).map { |h3| to_nav_item(page, h3) }
end
end
end
end
end
# Converts a heading into a hash of the info for a link
def to_nav_item(page, heading)
{
"title" => heading.text,
"url" => [page.url, heading['id']].join("#")
}
end
# Returns an enumerator of all H3s "belonging" to an H2
def subheadings(el)
Enumerator.new do |y|
next_el = el.next_sibling
while next_el && next_el.name != "h2"
if next_el.name == "h3"
y << next_el
end
next_el = next_el.next_sibling
end
end
end
end
I realize that this is quite a blob of code to throw at you, but it builds off of the work we did in a previous post. If you have any questions about the structure of Jekyll plug-ins, or the way we're using nokogiri please check that article.
When I run this code against our documentation site, I get a hash that looks something like this:
[{"title"=>"Getting Started",
"url"=>"/lib/java.html#getting-started",
"sub_subnav"=>
[{"title"=>"Download / Maven", "url"=>"/lib/java.html#download-maven"},
{"title"=>"Stand Alone Usage", "url"=>"/lib/java.html#stand-alone-usage"},
{"title"=>"Servlet Usage", "url"=>"/lib/java.html#servlet-usage"},
{"title"=>"Play Usage", "url"=>"/lib/java.html#play-usage"},
{"title"=>"API Usage", "url"=>"/lib/java.html#api-usage"}]},
...
Now all that we have to do is figure out how to render this thing using liquid templates.
Rendering the Subnav
It's actually not that difficult to render an arbitrarily deep sub navigation using liquid templates. The trick is to use a partial that renders itself.
In my layout, I render the partial and pass in the collection of navigation items.
{% include navigation_item.html collection=page.subnav level=0 %}
The partial creates the links for this level of navigation, and then renders itself, passing in a list of children. Just like a recursive function, this can theoretically go on forever. Just for kicks, I've added a bit of code to give each level of the subnav a class like level-1
or level-2
. This is really useful for styling.
{% if include.collection.size > 0 %}
<ul class="nav nav-list level-{{ include.level }}">
{% for item in include.collection %}
{% if item.url == page.url %}
<li class="active">
{% else %}
<li>
{% endif %}
{% if item.subnav.size > 0 %}
<a class="has-subnav" href="{{ item.url }}">
<span class="glyphicon glyphicon-plus"></span>
<span class="glyphicon glyphicon-minus"></span>
{% else %}
<a href="{{ item.url }}">
{% endif %}
{{ item.title }}
</a>
{% assign next_level = include.level | plus: 1 %}
{% include navigation_item.html collection=item.children level=next_level %}
</li>
{% endfor %}
</ul>
{% endif %}
That's it!
This concludes our brief foray into the wonderful world of Jekyll. In the next few days of the publishing a series of articles on Ruby internals, so stay tuned!