Skip to content

Instantly share code, notes, and snippets.

@mghaught
Created December 10, 2016 01:15
Show Gist options
  • Save mghaught/47562c605e4fe0045c623aa96782f7d7 to your computer and use it in GitHub Desktop.
Save mghaught/47562c605e4fe0045c623aa96782f7d7 to your computer and use it in GitHub Desktop.
Building Static Sites with Middleman on S3

Building Static Sites with Middleman on S3

In 2015, I converted haughtcodeworks.com to a static site built with Middleman and hosted on S3 and CloudFront. I was asked how I went about that so this gist documents what I did, as best as I can recall. It's been nearly two years since then so some of the gems and tools may have changed for the better.

You may wonder why I shifted to a static site given how dynamic web content is these days. I've known for years that static sites are ideal for certain kinds of content and after hearing about Middleman for the upteenth time, I decided that when it was time to refresh our website, I'd take a look.

As a Ruby programmer, I found that Middleman allowed me to work in the same fashion as I did in Rails yet generate static content. I could still use data or Ruby code to customize how I generated the site.

Cost was another benefit. My monthly hosting bill is $0.13, which includes CDN features via CloudFront. How can you argue with that price? There were a few trade-offs but overall I've been completely happy with this approach.

Pros:

As a static site, it's lightning fast, never goes down and is insanely cheap. Being slashdotted, for those that remember that term, is not possible. :)

Cons:

As a static site, you might need to rethink your strategy on publishing content or displaying dynamic data. This wasn't really a big deal since our content doesn't change that often that generating new static content is fine.

Publishing website changes does require someone to run the middleman commands at this point but I've seen ways around this: http://dojo4.com/blog/static-is-the-new-black. For our team, I'm fine with working directly with markup and keeping our site in a git repo. So running middleman commands explicitly when it's time to publish is not an issue.

It does mean that non-technical folks have to either be setup to work locally with middleman or they hand over markdown files of the content to be previewed. So that's not as convenient as working in a CMS where they can edit, preview, and publish on their own.

I did have to work out a few kinks initially with the blog gem and making some elements dynamic across the site. As a programmer, it wasn't a big deal and maybe took 4-6 hours in total to get it to where I wanted it.

The Stack

Here are the gems we're currently using. I'll go over much of this down below.

  • middleman 3.3, "~>3.3.12"
  • middleman-s3_sync
  • middleman-cloudfront
  • middleman-dotenv
  • middleman-blog
  • middleman-blog-drafts
  • middleman-syntax
  • redcarpet
  • font-awesome-middleman
  • haml
  • bootstrap-sass
  • jquery-middleman
  • bitters
  • bourbon
  • neat

We started with 3.3 originally and haven't yet upgraded to 4.1. We ran into an issue with the font-awesome gem when we tried this summer and decided to put it off since we weren't feeling any pain. Now that I'm writing this up, I naturally want to try it again so perhaps I'll modernize this stack in the coming weeks.

Resources

I referenced several websites and blog posts in this process. I'll list a few of them here before I get into the specifics of the stack.

https://middlemanapp.com/basics/install/

The official docs for middleman provided most of what I needed as I built out the site. I recalling having to dig into source at least once when I found the online docs weren't sufficient.

https://github.com/fredjean/middleman-s3_sync

Fred's README included most of what I needed for the S3 aspect.

https://rossfairbanks.com/2013/05/08/serving-middleman-blog-via-s3-and-cloudfront.html

I can't recall if I used Ross' post or not but it touches on how introducing CloudFront changes the setup. A quick review of his post seems to line up with what I ended up doing for CloudFront.

https://blog.codeship.com/middleman-s3-deploy/

I discovered this post today so I can't vouch for it, but I found it intriguing as I have no experience with the awscli tool. I could see that if the stack I'm using doesn't handle your S3-CloudFront needs this may be worth investigating. To be completely honest, I haven't had any issues with my current stack that would push me to switch.

Setup

One of the reasons I went with Middleman is it mirrors the ease of web development that I'm already comfortable with from other Ruby webapps and looks reasonably extendable. My experience so far is this is true though I haven't done anything crazy with my site.

For my setup, I'll give an overview of how I went about it generally so I'm not spelling out every command I ran. Please make sure to follow along with the current documentation for any gems/services I mention. I will also paste chunks from my config.rb as I touch on those aspects of the setup below.

I generated the site as directed by Middleman doc. I enabled both dot files via middleman-dotenv and directory_indexes so content generated would have index.html to make S3 happy.

activate :dotenv
activate :directory_indexes

I also turned on minification and gzip to be a good internet citizen.

configure :build do
  activate :minify_css
  activate :minify_javascript
  activate :gzip
end

I knew I wanted the blog aspect for the site, so I added the middleman-blog and middleman-blog-drafts gems. I'll go over our workflow on blogging later in the gist.

S3 Integration

The middleman-s3_sync gem's README covers this piece well. I found the defaults matched how I need to work, though I'll point out what I recall being different.

I already mentioned the dotenv gem, so I put my AWS creds into my .env file to keep them out of the git repo.

I disabled the after_build so it wouldn't sync automatically when running a build. I found I wanted to verify how things were generated without doing the sync enough times that I prefered that.

activate :s3_sync do |config|
  config.bucket                 = 'haughtcodeworks.com'
  config.region                 = 'us-east-1'
  config.aws_access_key_id      = ENV['AWS_ACCESS_KEY']
  config.aws_secret_access_key  = ENV['AWS_ACCESS_SECRET']
  config.after_build            = false
end

I also set a default caching policy for max age but I'm not sure how vital this is with Cloudfront.

default_caching_policy max_age:(60 * 60 * 24 * 365)

Cloudfront Integration

Ross' post above covered what I recall doing. I did use the after_s3_sync callback as I was using Middleman 3.3. Looks like 4x of middleman-s3_sync changes this so you'll want to confirm what, if anything, is required to invalidate cache for updated files.

after_s3_sync do |files_by_status|
  invalidate files_by_status[:updated]
end

AWS Console Setup

The official doc is best to refer to as I know I had to play some things to get S3 to do the right thing.

http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html

The first gotcha was my bucket name had to match my domain name. So I named my bucket haughtcodeworks.com.

In properties, you'll need to enable website hosting under Static Website Hosting. Once done, you'll get an endpoint such as:

<website domain>.s3-website-<region>.amazonaws.com

You'll want to set Index document to index.html. I recall activating :directory_indexes in middleman then allowed someone to go to /blog and see my blog's top page, which gets generated as /blog/index.html.

The default settings of middleman-s3_sync will upload your site with public-read permissions, so you will want to make sure you don't override that.

AWS CloudFront Setup

This was very simple, as I recall. You're making a distribution that points at your S3 bucket. The official doc is likely your best bet:

http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-create-cfdist.html

A few things of note:

  1. I set Alternate Domain Names (CNAMEs) to my base domain of haughtcodeworks.com.
  2. I made sure Default Root Object was set to index.html

You'll want to take note of the domain name that CloudFront assigns you. It can be found in the distributions list or under the General tab for your distribution.

DNS

I recall that serving your site under the base domain seemed somewhat tricky. As I use DNSimple, they have an ALIAS record that simplifies this some. Not sure about other DNS providers.

Create an ALIAS record and point it at the domain name from CloudFront. It should look like something like this:

ALIAS haughtcodeworks.com d12jwgedotmg8o.cloudfront.net

As I have a strange dislike of the www domain, I didn't want to serve any traffic under it for the HCW website. If you're like me then you can add a URL record pointing www traffic to the base domain.

URL www.haughtcodeworks.com http://haughtcodeworks.com

One item that is on my TODO list is to put my site fully under SSL. I'll do a follow-up post once I've done that.

Workflows

The middleman server command with livereload allows you to work with your site locally without needing to build after each change.

middleman server

Another benefit of using middleman is we can drop in our favorite gems to work efficiently with markup, such as haml, sass, and bourbon.

Blogging

For making blog posts we use the middlman-blog article command:

middleman article '<title>'

There's a lot to tweak for a blog so I'll paste our config below. I visited https://middlemanapp.com/basics/blogging/ a ton while dialing this in.

activate :blog do |blog|
  blog.prefix = "blog"
  blog.layout = "blog_post"
  blog.sources = "{title}.html"
  blog.permalink = "{category}/{title}.html"
  blog.summary_length = 350

  blog.paginate = true
  blog.page_link = "p{num}"
  blog.per_page = 5


  blog.custom_collections = {
    category: {
      link: '/categories/{category}.html',
      template: '/category.html'
    }
  }
  blog.default_extension = ".md"
end

We prefer to write and publish in markdown files which is supported out of the box. We used the middleman-syntax gem for code highlighting.

activate :syntax
set :markdown_engine, :redcarpet
set :markdown, :fenced_code_blocks => true, :smartypants => true

We use FrontMatter to allow us to generate our posts with things like a category, linked author, tags, and meta-description. Here's an example from a post I made recently:

---
title: The Prototyping Mindset
date: 2016-10-12 09:51 MDT
tags:
  Process
category: Software Development
author: marty
description: The prototyping mindset allows developers to build efficient, high-value software prototypes of a product's essential features in a short timeframe.
---

Deployment

This is simple and fast. We use the build and s3_sync commands to deploy.

middleman build
middleman s3_sync

As I mentioned above, I could have made the s3_sync automatic on build but I found I didn't want them to be tied together and it didn't bother me to chain the commands.

One negative here is that previewing your builds in a separate env like we typically do with a staging server would require extra setup/coding to enable. If I wanted to solve this right now, assuming someone hasn't already built it, I would write a preview command that would use an alternate S3 bucket as the staging env.

I'm pretty sure there are easier/better ways to tackle this given that it's been nearly two years since we converted our site to this stack. You're welcome to share what you've learned or if you have feedback or questions.

Good luck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment