Building Static Sites with Middleman

The Weekly Iteration

This video is only a short sample, but you can access the full version and all our other great content by subscribing.

Video

Notes

What are Static Sites

Static sites are sites comprised of and served from static files. Every HTML, CSS, and JavaScript file already exists and is sitting on a file system, somewhere, waiting to be served to the end user.

This is in contrast to dynamic sites which generate the pages on demand, returning different content based on the nature of the request, records from a database, etc. This may sound limiting, and in some ways it is, but those limitations can do wonders for simplifying the setup and deployment of your site.

Why a Static Site Generator

Static sites are a great choice in any case where you have content that does not update too often. For instance, blogging, marketing sites, and even most content sites can be built and thrive as static sites generated by a tool like [Middleman][].

Since there is no application code running on the server, serving a static site to be vastly simpler than serving a dynamic site. This leads to better performance, but also has less obvious benefits like improved security. When all your server does is send existing files, security becomes almost automatic.

Similar to the security idea, using static files makes it very easy to cache the content and gain a nice performance boost (in addition to the speedup of not running application code), and all without having to think too deeply about cache invalidation. How nice!

Why Middleman

So now that we're convinced of the virtues of static sites, the question becomes "why Middleman?". The primary reason I choose Middleman is that it is very similar to Rails. As a Rails dev, it's very easy for me to map those ideas into Middleman without having to rethink or relearn. Concepts like partials, erb templating, and the asset pipeline all work largely as I'm used to, and I really appreciate that.

Initial Commit

Throughout the video we'll be working to build up a simplified version of the thoughtbot.com website. The code for this video can be found in the accompanying repository on GitHub.

Getting started with a Middleman site is very straightforward and Rails-like:

$ gem install middleman
$ middleman init my-great-project-name

From that init command, we get an initial directory structure and dependencies in our Gemfile. The first file of interest is the config.rb file in the root of the generated project. This file contains all of the configuration for our Middleman site, but currently is largely empty. We'll come back to it in the coming steps as we add content and functionality to our site.

Also interesting is the source/ directory. This directory contains all of the files that will be mapped into our final build output. By default, every file in the source/ directory will be mapped to a corresponding output file. We can configure slightly more complex mappings from the source to the output files, but as a start, this simplicity is really great.

Lastly, just like Rails, Middleman has a server command that we can run to preview our site locally, and see changes as we make them.

Cleaning up Generated Content

As a general rule with either Rails or Middleman generated sites, I like to first commit the as generated files exactly, then go through and clean them up and commit that as well. I find that it's useful to be able to see that delta and know what I removed, what I cleaned up, etc. You can see these changes in the Clean up generated files commit.

Initial Content

Now that we have a skeleton in which to build our site, we can begin populating the content. Once again, we'll be rebuilding the thoughtbot.com site, or at least a simplified form of it. You can review all of these first content changes in the Add initial content and locations page commit.

The majority of the changes in this commit served to rough out the initial content, but the changes to the config.rb file are worth a closer look:

configure :development do
  activate :livereload
end

activate :directory_indexes

The first block activates the livereload plugin which will push content and styling (and even some JS) changes to the browser without needing to reload the page. This is nothing new, but it is nice that this functionality is so easy to opt into in a Middleman site.

The second configuration activates :directory_indexes which updates the mapping of source files to provide cleaner URLs. Instead of having a file like about.html.erb map to a URL of about.html, it can be severed as /about. Much nicer!

Data Dir & Partials

Now that we have our initial content populated, we can see one of the potential drawbacks to using a static site on the /locations page. This page consist of a list of all of the thoughtbot locations, with each location rendered roughly as a card with an image, the address, etc. The simplest approach here is just to manually copy the markup for each new location, but this is exactly the sort of thing we've learned to avoid with Rails and dynamic apps.

Thankfully, with Middleman we have functionality to remove this duplication and make generating pages with this sort of repeated content easy. The core feature in use here is the Middleman data directory. Each file in the data directory is a YAML (or JSON) file describing some structured data. In our case, we have a file listing out the details for each location.

# locations.yml
- city: Austin, TX
  image: "austin.jpg"
  address:
    - 701 Brazos St.
    - 5th Floor
    - Austin, TX 78701

- city: Boston, MA
  image: "boston.jpg"
  address:
    - 41 Winter St.
    - 7th Floor
    - Boston, MA 02108

# ... and so on

In addition, we can extract the markup for our location card into a partial, and then rendering the full list of locations becomes:

<div class="locations">
  <% data.locations.each do |location| %>
    <%= partial "location", locals: { location: location } %>
  <% end %>
</div>

The files in this data directory are essentially a static database. Now, adding a new location is as easy as adding a few lines to a yaml file.

The full changes for the location data extraction can be seen in the Pull locations from data file, extract partial commit.

Assets

Now that we have our content in a solid place, we can turn our eye to the assets. We'd like to clean up the styles, and add a bit of JavaScript for some client side behavior. This is an area where Middleman really shines as it brings essentially the entirety of the asset pipeline along for us to work with.

Asset Gems

Both Bourbon and Neat, our libraries for working with Sass and semantic grids, can be added via their main gems, exactly as we would in any Rails project.

Unfortunately, a number of the JavaScript asset gems are more tightly coupled to Rails, but via the rails-assets.org project, we can bring in just about any JavaScript asset we need via the Gemfile. The specific updates to the Gemfile are as follows:

+source "https://rails-assets.org" do
+  gem "rails-assets-jquery"
+  gem "rails-assets-moment"
+end
+
+gem "bourbon"
+gem "neat"

Styling Changes

With Bourbon and Neat added to the project, the first thing we'll want to do is rename the all.css stylesheet to all.scss to take advantage of Sass features like nesting, @import and @include, variables, and all of Bourbon and Neat's niceties.

JavaScript Changes

Here again we have a very asset pipeline-like experience. We can pull in jQuery and moment.js, provided by the asset gems, and then write a simple DOM manipulation function:

//= require jquery
//= require moment

$(function() {
  var today = moment().format("dddd MMM Do, YYYY");
  $('footer').html('<p>Hello from jQuery land. Today is ' + today + '</p>');
});

Again, you can see all of these changes together in the Make assets fancy w/ Sass, Bourbon, Neat, jquery commit.

Deploying

At this point we have built up a great foundation with our site and it's time to get it on the web. There are many approaches we can take to this since the output of our site is just HTML, CSS, and JavaScript. Many of these approaches have Middleman specific plugins to make it easy as pie.

In our case we'll be looking at using Amazon's S3 service combined with it's static website hosting functionality to host our site cheaply and easily.

Middleman S3 Sync

In order to make this as simple as possible, we'll be using the middleman-s3_sync plugin to automate and optimize our deployment. This particular gem (there are multiple Middleman + S3 gems, yay community!) stands out to me as it strikes a great balance between convection and ease of use, while providing just about any configuration point you might need open.

Dotenv for Secrets

The one final piece needed to tie all of this together is a way to make our S3 keys accessible to the s3_sync gem. We'll do this using the middleman-dotenv gem. Again, very similar to a Rails app, we can put our keys in a .env file and have them loaded into the environment as needed.

Adding a 404 Page

S3 allows us to define an "error page" for unmatched URLs. In order to provide this, we can opt-out of the directory_indexes functionality for that one file to ensure that 404.html will indeed be in our output:

page "/404.html", directory_index: false

Deploy Script

With all of this in place, deployment is now extremely simple, but we can go one step further to wrap this up in a deploy script:

#!/bin/sh

set -e

bundle exec middleman build --clean
bundle exec middleman s3_sync

This script forces a full clean build, then pushes the files up to S3 using the s3_sync command.

You can check out the full changes for deployment in the Add a deployment config for S3 commit.

Performance Tweaks

Now we have our content fleshed out, assets enabled, and deployment streamlined, the one final tweak we might want to make before calling it a day is to apply a handful of performance tweaks.

The specific configurations we'll be adding are to minify our CSS and JS, gzip our files, and to apply an asset hash fingerprint. Combined, these reduce the size of every piece of content we serve, and in the case of the fingerprinting, they allow for optimized long lived caching.

In order to activate the performance tweaks, we only need to modify the config.rb file (a very good sign), adding the following:

+configure :build do
+  activate :gzip
+  activate :minify_css
+  activate :minify_javascript
+  activate :asset_hash
+end
+
 activate :dotenv
-activate :s3_sync
+activate :s3_sync do |s3|
+  s3.prefer_gzip = true
+end

Once again, this is an example of Middleman making it very easy to enable the same sort of workflows and optimizations we're used to from Rails. You can check out the full performance changes in the Gzip and fingerprint assets commit.

Additional Resources

And with that, we have our site built out. If you're interested in diving deeper, the following resources will keep you on the Middleman train.

×

15 Full Courses, 100+ Screencasts & New Content Weekly