So you have a static site in Jekyll that you want to deploy to Heroku. Lucky for you, this is a relatively easy task and does not require anything as complex as a deployment server as mentioned in the Jekyll docs. You can support this with a simple Rack script.

For those not familiar, Rack is a bare-bones web-server adapter in Ruby. Most Ruby web-frameworks sit on “top” of Rack while the Ruby web-servers sit “underneath” of it. However, we don’t need all the complexities of a web-framework to serve our static content, so we only need Rack and any Rack-compatible web-server to accomplish our goals.

Requirements

The bare minimum that you’ll need are

  • Jekyll site to publish
  • Your favoriate text editor
  • Heroku account to push to
  • Ruby (recent version)
  • Bundler (gem)

There are a lot of ways to install Ruby so I’ll leave this to you. I will say that I prefer rbenv for its ease of use. Once you have Ruby set up, you can install Bundler via

$ gem install bundle

Gemfile

The first thing we need to declare is all of our runtime requirements for our Rack script that we’ll write later. Put this in a file called Gemfile

source 'https://rubygems.org'

ruby '2.2.0'

gem 'rack-contrib'
gem 'puma'

This file is rather simple, we’re stating the source of our gems (the official repo), the Ruby version (2.2.0) and a couple of gems. The rack-contrib gem will pull in some stuff for static-asset serving (as well as rack itself) and puma is our Rack-compatible web-server.

Once we have our Gemfile we need to create a lock file. This will be used to “lock in” specific versions of gems so that what we try out locally should be exactly the same when we deploy it to Heroku. We can create this with

$ bundle

You should now have a Gemfile.lock in your directory. Do not edit this file.

Rack

Now that we have the required gems installed, we can get down to writing our Rack file that will serve our static assets. Create a file config.ru with the content

require 'rack'
require 'rack/contrib/try_static'

# enable compression
use Rack::Deflater

# static configuration (file path matches reuest path)
use Rack::TryStatic,
      :root => "_site",  # static files root dir
      :urls => %w[/],    # match all requests
      :try => ['.html', 'index.html', '/index.html'], # try these postfixes sequentially
      :gzip => true,     # enable compressed files
      :header_rules => [
        [:all, {'Cache-Control' => 'public, max-age=86400'}],
        [['css', 'js'], {'Cache-Control' => 'public, max-age=604800'}]
      ]

# otherwise 404 NotFound
notFoundPage = File.open('_site/index.html').read
run lambda { |_| [200, {'Content-Type' => 'text/html'}, [notFoundPage]]}

Let’s break this file into the important parts. The first thing we do is import the required gems. This is self explanatory. Next we see

use Rack::Deflater

This bit of code will enable compression on any content that is served. This will speed up your page load time, especially for those with slower internet connections. The next section is the real meat of our Rack file

use Rack::TryStatic,
      :root => "_site",  # static files root dir
      :urls => %w[/],    # match all requests
      :try => ['.html', 'index.html', '/index.html'], # try these postfixes sequentially
      :gzip => true,     # enable compressed files
      :header_rules => [
        [:all, {'Cache-Control' => 'public, max-age=86400'}],
        [['css', 'js'], {'Cache-Control' => 'public, max-age=604800'}]
      ]

This portion defines our static asset handler. The first thing it does is set the “root” of the content directory to _site. This means that if a request comes to /index.html that our static asset handler will look in _site/index.html. Since _site is the default folder for Jekyll auto-generated content, we should be good to go here.

The url defines how we want to match requests. If we wanted to have a Jekyll site as only a sub-section of our site, this would be useful. Since, for the purposes of this example, the Jekyll site comprises the entire web-site we can define this as / to mean the “root” of the web-site.

The try defines various extensions to try when receiving a request. So given what we’ve seen so far, if the application receives a request of /awesome-article it will check for the following files in order

  • _site/awesome-article
  • _site/awesome-article.html
  • _site/awesome-articleindex.html
  • _site/awesome-article/index.html

With this we can make a variety of requests and get very clean URLs without having to include the .html extensions or without having to specify index when visiting the root of the site (e.g. www.mysite.com).

The gzip and header_rules options simply allow caching to work which we defined earlier. This should help to significantly speed up your site, especially as visitors click from page to page as they will avoid re-loading common resources such as CSS or JS files.

This last part is especially nifty.

# otherwise 404 NotFound
notFoundPage = File.open('_site/index.html').read
run lambda { |_| [200, {'Content-Type' => 'text/html'}, [notFoundPage]]}

This will redirect any request that we can’t understand (broken link or just bad URL) back to the homepage. You could, if you wanted to be really fancy here, provide a custom “Not Found” page. Let your imagination run wild.

Procfile

If we are deploying to Heroku, we need to create a Procfile that tells Heroku how to run our site. We can create a simple file as

web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development}

This instructs Heroku to use our puma gem to run our web-service and some additional parameters that Heroku requires (you can safely ignore these).

Releasing!

We are now ready to push to Heroku! Assuming you’ve already setup your Heroku account and have authenticated in your local repo. We can run the following bit to deploy.

# Add all of the files we've recently created
git add Gemfile Gemfile.lock Procfile config.ru
git commit -m "Adding required files for deploying to Heroku"

# Regnerate your site
rm -rf ./_site
jekyll build

# Add your site to your git repo (important for Heroku to work)
git add ./_site
git commit -m "rebuilt site"

# deploy to heroku via git push (assuming remote for Heroku already setup)
git push heroku master

Personally, I like to put all of this into a Makefile sot that I can automate this process.

default: deploy

deploy:
	rm -rf ./_site
	jekyll build
	git add ./_site
	git commit -m "rebuild of ./_site dir for release"
	git push heroku master