I have had more blogs than I have had blog posts. The Way Back Machine contains a humiliatingly high number of websites bearing my name that contain a single blog post and some mention of being the latest and greatest blog. The last one (that previously existed on this domain) at least shows some awareness of the problem:

I have a life-long passion for creating blogs and immediately forgetting about them, this is probably the latest.

So this is the latest one of those. Will this be different? Perhaps. I’ve previously been a little too eager to try out the latest static-site generators but soon discover their limitations. I’m also less eager to continue learning how to use the static-site generator when I just want to make a change a year later.

This is just a simple Rails site, which I hope means I can come back to it, at any time, and add entire new features and toys. The design is simple and I’m hoping that I can plop anything below the header and keep it somewhat incorporated with the rest of the site.

I thought it would be fun (and eventually even save me bother) if the menu system on this site could be generated from Rails’ routes.rb config file.

After a lot of digging through .inspect outputs, I found that I can include extra variables in routes.rb and access them:

get "/about", to: "pages#about", include_in_menu: true

I can then find that in Rails.application.routes.routes.map under defaults[:include_in_menu]. Here’s an example where I loop through the routes, checking for the variable, and building the path (/about in this case).

Rails.application.routes.routes.map do |route|
  if route.defaults[:include_in_menu]
    built_path = route.path.build_formatter.evaluate({})
    path_sections = built_path[1..-1].split("/")
    data = add_to_menu(data, path_sections, built_path)
  end
end

You can see the add_to_menu method below. It is called recursively to build a hash that represents the URL structure. Each route has a :path (used as a href later) and a :children hash which contains any sections deeper within that route. For example, “/about/contact” would be a child of “/about”. We can access it, and other relevant menu options along the way, with data[“about”][:children][“contact”].

def add_to_menu(data, sections, path)
  if sections.length==1
    data[sections[0]] = {:path => path, :children => {}, :selected => false}
  else
    if data[sections[0]]
      this_section = sections[0]
      sections.shift
      data[this_section][:children] = add_to_menu(data[this_section][:children], sections, path)
    end
  end
  return data
end

sections contain is an array which is created by splitting the path (/about/contact) by / (['about', 'contact']). The method works through the array, recursively calling itself on the children of each section until there’s only one section left. At that point, it sets the path and creates a new children hash.

The way this works means that the order of your routes.rb file is now important. If /about/contact is declared before /about, it doesn’t have anywhere to go. There’s no error but it won’t get added to the hash.

I also compare the current route (request.path) against the hash, setting :selected on each section that matches. This defines which items are selected in the menu and which children to render.

To render the menu, I use a partial that which is passed the menu data and recursively includes itself. This is the main menu partial:

<div class="container">
  <a href="/">guntrip</a> / 
  <%= render 'layouts/menu_section', this_menu_section: menu_data, up: "/" %>
</div>

And the menu partial itself:

 <% selected_key = "null" %>
 <select>
  <option id="<%= up %>">...</option>
  <% this_menu_section.each do |key, menu_item| %>
    <option id="<%= menu_item[:path] %>"<% if menu_item[:selected] %> selected <% selected_key = key %><% end %>><%= key %></option>
  <% end %>
</select>
<% if selected_key != "null" && this_menu_section[selected_key][:children].length > 0 %>
  / 
  <%= render 'layouts/menu_section', this_menu_section: this_menu_section[selected_key][:children], up: this_menu_section[selected_key][:path] %>
<% end %>

I chose to use a series of <select> comboboxes (they’re easy and work well on mobile) and the partial renders one <select> at a time. It adds a “…” option which points at the level above and then iterates through the provided options. If :selected is set to true, selected_key is updated; I couldn’t think of a better way to do that without duplicating more code.

If selected_key is set, we include the same partial and pass it the selected item’s children and the selected item’s path as up. That adds the next section of the menu.

To make this menu functional, I am just binding the onclick (and onchange for mobile) events to navigate to the id of the selected option.

I added a few other options too, include an id variable in routes.rb which allows me to give blog posts a static link (and include them in the menu if I want). This is helpful for text pages as the blog uses markdown.

get “/special_page", to: "posts#static", include_in_menu: true, id: 3

I hope this is interesting, and perhaps useful, for someone. I’ve not spent much time in the Rails codebase so this is possibly far more hacky a solution than it needed to be. I’d love to hear from anyone if they have thoughts about this.