Complex Static Sites on Heroku
Hosting a static site generated by Middleman or Jekyll on Heroku is easier than it might seem. You don’t need a custom buildpack or a gem, all you need is Rake and a few middlewares.
Generating
Some people like to build their site locally and commit it to the repo, but you really don’t need to do that.
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 can be very simple. Rack::Static, the same middleware that serves assets like images and stylesheets can be used to serve any static file.
If you’re particular about your URLs (like me) and don’t want them to have file extensions, you’ll have to get a little more complex. To get 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!
-
These are the shell commands for Middleman, but if you’re using Jekyll or something else you’ll need to adjust them. ↩
-
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. ↩