Jeremy Smith on May 20, 2015

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?

Jeremy is currently booked until mid-2023, but always happy to chat.

Email Jeremy