Middleman
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.