Blogging on Heroku with Middleman

Michael on Mar 23

Following on from our last post on using Middleman as a blog platform, we wanted to take a deeper dive into how to go from start to finish deploying a Middleman blog to Heroku.

Launching this blog was our first time working with Middleman, which so far couldn't have been easier, and combined with Heroku's recent GitHub integration, we now have a particularly nice workflow in place; to publish a new post, the author simply needs to create a new markdown file with the article content, commit this, and push to Github. We have two primary branches - a dev branch which automatically deploys to a staging version of the blog, and the master branch, that deploys the production version. While Middleman provides a great local server for previewing your site live as you're writing, one of the really nice side-benefits of this workflow is that we don't have to go through a desktop environment to publish - in effect, Github has become our web-based CMS to our static blog!

There is a lot of really great flexibility that Middleman provides, and while this article will dip into a few of those areas, the main intent is to highlight the simplest path to get up and running on Heroku, including a few features that are not essential for Heroku, but we found very useful when developing this blog.
This is meant to be complimentary to Middleman's own docs, so if something is only lightly touched on here, check out the more in-depth background provided on the Middleman site.

This post is split into three parts:

Part 1: Setup a new middleman-blog app

Part 2: Production environment config

Part 3: Prepping for Heroku

Finally before we begin, I have created a repo at https://github.com/pixelcabin/middlemanblog_heroku_template showing the end result of the steps outlined below, which is running on Heroku at https://blog-template-staging.herokuapp.com - see the footnote for each section to view the corresponding diff.


Part 1 - Setup a new Middleman-blog app1

There is a lot of info on setting up the middleman-blog extension in the official documentation, however if you're already familiar with that, you can kick off with:

  1. gem install middleman
  2. gem install middleman-blog
  3. middleman init MY_BLOG_PROJECT --template=blog

Middleman uses a config.rb file in your project root to set up various options. We will be coming here a lot, but to start with, let's set up the location and file format of our articles:

1
2
3
4
5
6
activate :blog do |blog|
  #…
  blog.sources = "articles/{year}-{month}-{day}-{title}"
  blog.default_extension = ".md"
  #…
end

Once setting this, you'll want to move the example article into source/articles, and rename to a .md extension (making sure to drop the .html part of the filename too).

Now, start up the local development server with bundle exec middleman server. You should now have a basic middleman-blog app, with the default example article visible.

Create an article layout

By default, Middleman will render the contents of your markdown article into the <%= yield %> tag of source/layout.erb, however if you want your article to look different to the rest of your blog, you will probably want to set up a separate layout for it.

The simplest way to do this is to create a new article_layout.html.erb file, and include the following:

1
2
3
4
<% wrap_layout :layout do %>
  <!-- Article-specific markup here -->
  <%= yield %>
<% end %>

Then, within config.rb:

1
2
3
4
5
activate :blog do |blog|
  #…
   blog.layout = "article_layout"
  #…
end

This uses nested layouts to nest the article's layout within the main layout, and then the contents of your markdown file will be rendered where yield is called within article_layout. You can optionally move these into a layouts subfolder to keep them organized, and Middleman will still find them.

A quick Middleman cheat sheet

More in depth info can be found under Listing Articles, along with the RDoc for the gem (BlogArticle is of particular interest).

Index

  • page_articles - Assuming pagination is enabled, the collection of articles for the current page (otherwise blog.articles will return all of the blog's articles)
  • article.data - the article's frontmatter as a structured object - i.e. if the frontmatter includes featured, this can be accessed as article.data.featured
  • article.url the relative path to the article

Article layout

  • current_article - the current article

Custom Helpers

Middleman supports writing custom helper methods - these are described under Custom Defined Helpers, and can really help with keeping your templates dry.


Part 2 - Production environment config2

Setup RSS Feed

Middleman blog will generate a template source/feed.xml.builder, however this needs updating before you launch your blog.

site_url

This should be updated to whatever domain your blog will be hosted on, however for a deployment to Heroku, we set this up as a config variable APP_DOMAIN, which allows this to be set on an app-by-app basis without needing to modify the codebase. For info on setting these on Heroku, see Heroku's docs on Configuration and Config Vars.

1
site_url = "https://#{ENV['APP_DOMAIN']}/"

Once this is updated, modify any other relevant properties. This will then expose an rss feed available at /feed.xml

Configure sitemap

There is a great Middleman plugin that will do the work for you called search_engine_sitemap. Once configured, this will output the compiled sitemap at /sitemap.xml

  1. Add gem 'middleman-search_engine_sitemap' to your Gemfile
  2. Add the following to config.rb, before the configure :build block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
set :url_root, "https://#{ENV['APP_DOMAIN'] ? ENV['APP_DOMAIN'] : 'localhost:4567'}"

activate :search_engine_sitemap,
         exclude_if: -> (resource) {
           # Exclude all paths from sitemap that are sub-date indexes
           resource.path.match(/[0-9]{4}(\/[0-9]{2})*.html/)
         },
         default_change_frequency: 'weekly'

# Filewatcher ignore list (workaround for search_engine_sitemap on
# Heroku - see https://github.com/Aupajo/middleman-search_engine_sitemap/issues/2)
set :file_watcher_ignore,
    [
        /^bin(\/|$)/,
        /^\.bundle(\/|$)/,
        # /^vendor(\/|$)/,
        /^node_modules(\/|$)/,
        /^\.sass-cache(\/|$)/,
        /^\.cache(\/|$)/,
        /^\.git(\/|$)/,
        /^\.gitignore$/,
        /\.DS_Store/,
        /^\.rbenv-.*$/,
        /^Gemfile$/,
        /^Gemfile\.lock$/,
        /~$/,
        /(^|\/)\.?#/,
        /^tmp\//
    ]

Compress assets

Add gem 'middleman-minify-html' to your gemfile, then modify the configure :build block in config.rb as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Build-specific configuration
configure :build do
  # For example, change the Compass output style for deployment
  activate :minify_css

  # Minify Javascript on build
  activate :minify_javascript

  # Enable cache buster
  activate :asset_hash

  activate :gzip
  # Use relative URLs
  # activate :relative_assets

  activate :minify_html do |html|
    html.remove_http_protocol = false
  end

  # Or use a different image path
  # set :http_prefix, "/Content/images/"
end

JS Compilation

Add the following gems to speed up JS compilation (optional):

1
2
gem 'therubyracer' # faster JS compiles
gem 'oj' # faster JS compiles

Part 3 - Prepping for Heroku3

There are a few things that need to be set up for use on Heroku:

  • Gemfile - add additional gems required for various rack plugins
  • Rakefile - provides the relevant code to allow Heroku to run middleman build when compiling a new build
  • config.ru - the rackup file that will be used by heroku to start up the Rack server
  • 404 Page
  • Procfile - used by Heroku to start up the web process

Gemfile

Add the following gems:

1
2
3
4
gem 'puma'
gem 'rack-contrib'
gem 'rack-ssl'
gem 'rack-cache'

Rakefile

Create a new file Rakefile, and paste in the following:

1
2
3
4
5
6
7
require 'bundler/setup'

namespace :assets do
  task :precompile do
    sh 'middleman build'
  end
end

config.ru

Create a new file config.ru, and paste in the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#\ -s puma
require 'rack'
require 'rack/contrib/try_static'
require 'rack/deflater'
require 'rack/cache'

# Forces SSL on all requests
unless ENV['RACK_ENV'] == 'development'
  require 'rack/ssl'
  use Rack::SSL
end

use Rack::Cache,
    :verbose     => true,
    :metastore   => 'file:/var/cache/rack/meta',
    :entitystore => 'file:/var/cache/rack/body'

# Enables compression of http responses, used in conjunction with `activate :gzip` in config.rb
use Rack::Deflater

ONE_WEEK = 604_800

# Serve files from the build directory
use Rack::TryStatic,
    root: 'build',
    urls: %w[/],
    try: %w(.html index.html /index.html),
    header_rules: [
        [
            %w(css js png jpg woff html),
            { 'Cache-Control' => "public, max-age=#{ONE_WEEK}" }
        ]
    ]

run lambda { |env|
      four_oh_four_page = File.expand_path('../build/404/index.html', __FILE__)
      [
          404,
          {'Content-Type'  => 'text/html', 'Cache-Control' => "public, max-age=#{ONE_WEEK}"},
          [ File.read(four_oh_four_page) ]
      ]
    }

404 Page

Create a new file source/404.html.erb, and add content as appropriate - lines 27-30 of config.ru will handle returning the 404 page if Rack::TryStatic does not find any suitable files to return.

Procfile

Create a new file Procfile, and paste in the following:

web: bundle exec puma -p $PORT

 


Once this is all set up, create your Heroku app, set the APP_DOMAIN config var to the domain you'll be accessing the app on, and push to Heroku.

 

Building this blog has been my first time working with Middleman, so if you've spotted something that could be improved, let me know in the comments below, or on Twitter at @michaelrshannon!


Edits

While prepping our next article, we noticed that links were having their protocol removed on build, meaning that if the site was loaded over https (as ours is), all links would end up becoming https too, which could cause problems with external resources.
To fix this, the section Compress Assets has been updated to instruct the minify_html Middleman extension not to remove the http protocol on compression, which it otherwise does by default.