Middleman

by Pete Nicholls on at Christchurch Ruby

Middleman is a static site generator, similar to Jekyll (which powers GitHub Pages) or Stasis.

Static site generators let you break up your website into templates and pages that are compiled together to create a static HTML website.

In this example, we’re going to build a site to host notes on talks. You can see the finished project on GitHub.

Starting out

Install Middleman:

gem install middleman

Generate a new site:

middleman init talks

Start a local server by cd-ing into the newly created talks directory and running:

bundle exec middleman server

Your site will be served up from http://localhost:4567.

Adding a talk

Let’s add a talk. Create a source/talks directory, and add a new file called middleman.md.erb:

---
title: Middleman
layout: talk
author: Pete Nicholls
---
Welcome to my talk!

The two sets of three dashes (---) delineate the page’s metadata. The metadata, also known as the frontmatter, consists of YAML. Everything following the frontmatter is the content of the page (in this case, written in Markdown).

Understanding frontmatter

For this page, we have three pieces of frontmatter: the layout of the page, the title and the author of the talk.

The layout attribute has a special meaning in Middleman. When given, it tells Middleman to use a particular layout inside source/layouts (in this case, we gave talk as the value, which corresponds to source/layouts/talk.erb). If no layout is given, it will fall back to the default layout (typically source/layouts/layout.erb).

The title and author are attributes we simply made up. Any valid YAML is fair game for the frontmatter. We can use these attributes elsewhere in the site.

Generating a list of talks

Now that we have our talks, we want to create a list of them to display on the home page. The simplest way we can do this is through Middleman’s sitemap.resources, which contains a list of all resources in the site.

We can filter these resources by URL, for instance, to select only those that are inside the source/talks directory.

Inside source/index.html.erb:

<h1>All talks</h1>

<ul>
  <% sitemap.resources.select { |resource| resource.start_with?('/talks') }.each do |talk| %>
    <li><%= link_to talk.data.title, talk.url %></li>
  <% end %>
</ul>

We access the the frontmatter through the data attribute of the resource.

Extracting a helper method

link_to is a helper method provided to us by Middleman for generating links. We can write our own helper methods, too.

Let’s extract the awkward sitemap.resources.select code in the example above to something nicer.

Inside config.rb, add the following method definition to the helpers block:

helpers do
  def talks
    sitemap.resources.select do |resource|
      resource.start_with?('/talks')
    end
  end
end

Any method defined inside the helpers block becomes available to every page.

We can now change our source/index.html.erb to use new the method:

<ul>
  <% talks.each do |talk| %>
    <li><%= link_to talk.data.title, talk.url %></li>
  <% end %>
</ul>

Much nicer.

Extracting an object

But can we do better? The talk.data.title still feels awkward. It would be much nicer if we could just use talk.title.

Let’s extract talks into their own class. For convenience, we’ll continue to keep everying inside the helpers block inside config.rb for now.

helpers do
  class Talk
  end

  def talks
    sitemap.resources.select do |resource|
      resource.start_with?('/talks')
    end
  end
end

First, we can pull the implementation of talks into the talk class:

helpers do
  class Talk
    def self.all(resources)
      resources.select do |resource|
        resource.start_with?('/talks')
      end
    end
  end

  def talks
    Talk.all(sitemap.resources)
  end
end

We pass in sitemap.resources because it allows us to decouple the Talk class from having to know about the sitemap, and make it easier to test later if we need to.

We’re still returning a simple filtered array of resources. Let’s change that to returning an array of Talk instances:

class Talk
  def initialize(resource)
    @resource = resource
  end

  def url
    @resource.url
  end

  def data
    @resource.data
  end

  def title
    data.title
  end 

  def self.all(resources)
    filtered_resources = resources.select do |resource|
      resource.start_with?('/talks')
    end

    filtered_resources.map do |resource|
      Talk.new(resource)
    end
  end
end

We’ve created a Decorator for the resource object. We’ve added the methods we’d like to have available – in this case, url and title, that simply delegate to the resource object underneath.

Now we can drop the data method in index.html.erb:

<li><%= link_to talk.title, talk.url %></li>

Great! But there’s still a little room for refactoring.

Refactoring the Talk class

In initialize, we’re passing in an object that we’re going to delegate other methods to. It turns out there’s a class in the standard library which does this for us, so we can simplify our code:

class Talk < SimpleDelegator
  def title
    data.title
  end 

  def self.all(resources)
    filtered_resources = resources.select do |resource|
      resource.start_with?('/talks')
    end

    filtered_resources.map do |resource|
      Talk.new(resource)
    end
  end
end

Any method on the resource object becomes available for use. We don’t need to explicitly define the url or data methods any more. We can also throw away initialize, because SimpleDelegator does that for us.

Finally, extract the class from the helpers block and move it into its own file. Move the class into a file called lib/talk.rb, and use require to load it into config.rb:

require './lib/talk'

helpers do
  def talks
    Talk.all(sitemap.resources)
  end
end

Much nicer.

Be aware that if talk.rb changes, Middleman won’t pick up on it by default unless you restart the server.

Building the site

To build the site, run:

bundle exec middleman build

The static site will be built into the build directory, ready to deploy anywhere.

Links