This is Anti-pattern—thoughts on programming and whatnot by Brandon Weiss.

Complex Static Sites on Heroku

May 14th, 2015

A few years ago I wrote about a simple way to host static sites on Heroku. That was great when my personal site was just one page and some assets, but it eventually grew past that. Now it’s a handful of static pages built by Middleman, as is this blog.

Hosting a static site generated by Middleman (or Jekyll) on Heroku is easier than it might seem. First you generate the static files, then you serve them. You don’t need a custom buildpack or a gem, all you need is Rake and a few middlewares.

Generating

Do not build your site locally and commit the build folder to your repository before deploying. That is gross and unnecessary.

Heroku’s Ruby buildpack will invoke a Rake task called precompile:assets during deployment if it exists. This feature is primarily for deploying Rails apps, but you can hook into it by creating your own precompile task that will build your static site during deployment.1

# Rakefile
require "rake"

namespace :assets do

  desc "Precompile assets"
  task :precompile do
    Rake::Task["assets:clean"].invoke
    sh "bundle exec middleman build"
  end

  desc "Remove compiled assets"
  task :clean do
    sh "rm -rf #{File.dirname(__FILE__)}/build/*"
  end

end

Serving

Serving a static site is usually dead simple. Rack::Static, the same middleware that serves assets like images and stylesheets can be used to serve any static file. Unless, like me, you’re very particular about your URLs and don’t want them to have file extensions.

If you want pretty URLs you’ll have to generate static files that are Directory Indexes-compliant.2 Middleman has a Directory Indexes extension and Jekyll has a pretty permalink style. Then you can use the Rack::TryStatic middleware in rack-contrib to map your URLs to the right files on disk.

# config.ru
require "bundler"
Bundler.setup("production")

require "rack/contrib/try_static"
require "rack/contrib/not_found"

use Rack::TryStatic, {
  root: "build",
  urls: %w[/],
  try:  %w[
    .html index.html /index.html
    .xml  index.xml  /index.xml
  ]
}

run Rack::NotFound.new("build/404/index.html")

Rack::TryStatic is just like Rack::Static except it will sequentially try appending each postfix in try to the URL to check if there’s a matching file on disk. If it finds a match it will serve it via Rack::Static, if not it will pass on through and serve the 404 page.

That’s it!


  1. These are the shell commands for Middleman, but if you’re using Jekyll or something else you’ll need to adjust them. 

  2. Back in the day, most HTTP servers would respond to a request for a directory (a URL with a trailing slash) by listing the directory’s contents. They also commonly had a feature called Directory Indexes that would serve an index.html file found inside the directory instead. So if you went to /foobar/ it would serve a file at /foobar/index.html. Everyone realized they could make their URLs prettier this way if they just re-jiggered their file structure a bit. This is why for a long time trailing slashes were so common in URLs. Eventually most static sites were replaced by something dynamic like a content management system or some other application. These systems look at a URL and decide what content to serve. At that point it became pretty trivial to drop the trailing slash and map /foobar onto some page with a title of “Foobar” in the database. That’s why you almost never see file extensions or trailing slashes in URLs any more; the file extension is usually implied and unnecessary, and the trailing slash was only ever a hack we used to get rid of them in the first place.