Auto-generate navigation from page headers in Middleman
I’m working on a Middleman site right now that has one long page of content with a sidebar of navigation that links to the headers and sub-headers found on the page. (Underscore.js is a good example of this.) This is a common style for technical documentation sites these days.
I didn’t want to have to maintain the navigation content by hand. And since the navigation is meant to exactly reflect the header structure, I decided to write a navigation builder.
This is by no means a perfect solution, but it’s workable.
First, create a basic Middleman layout that loads a nav partial:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title><%%= current_page.data.title %></title>
<%%= stylesheet_link_tag "all" %>
<%%= javascript_include_tag "all" %>
</head>
<body class="<%%= page_classes %>">
<div class="container">
<aside>
<nav>
<%%= partial "nav" %>
</nav>
</aside>
<article>
<%%= yield %>
</article>
</div>
</body>
</html>
Your nav partial is going to call the helper that will build your table of contents based on the content from your index.html.erb
file:
<h2>Table of Contents</h2>
<%%= build_toc("index.html.erb") %>
Your index file will hold the content for your site, with headers and subheaders, and ids for each:
<h1>Site or page title</h1>
<h2 id="introduction">Introduction</h2>
<h3 id="first_subhead">First Subhead</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<h3 id="second_subhead">Second Subhead</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
Now, create a helpers folder in you middleman project’s main folder and add the following navigation_helpers.rb
file:
require 'nokogiri'
module NavigationHelpers
def build_toc(file)
TOCBuilder.new(file).toc
end
class TOCBuilder
include Padrino::Helpers::OutputHelpers
include Padrino::Helpers::TagHelpers
include Padrino::Helpers::AssetTagHelpers
attr_reader :file
def initialize(file)
@file = file
end
def toc
link_tree(get_outline())
end
private
def get_nodes
page = Nokogiri::HTML(File.open("source/#{file}"))
page.css('h2, h3')
end
def get_outline
outline = []
last_headline = nil
get_nodes.map { |h|
{ type: h.name, id: h['id'], title: h.text, children: [] } if !h.text.blank?
}.compact.delete_if { |h|
case h[:type]
when "h2"
outline << h
last_headline = h
false
when "h3"
last_headline[:children] << h
true
end
}
end
def link_tree(outline)
return if outline.empty?
content_tag(:ul) do
item_content = ''
outline.each do |link|
item_content << content_tag(:li) do
link_content = ''
link_content << link_to(link[:title], "/##{link[:id]}")
link_content << link_tree(link[:children]) if !link[:children].empty?
link_content.html_safe
end
end
item_content.html_safe
end
end
end
end
Middleman will automatically load and register helper modules found in the helpers folder if the file name matches the module name. (See the Middleman documentation for Custom Defined Helpers.)
TOCBuilder
uses Nokogiri to find all the h2
and h3
tags in your page, then builds the navigation by creating an unordered list for all h2
tags and sub-lists for the h3
tags under each.
Need help building or maintaining a Rails app?
Book a one-hour project inquiry call or reach out via email.