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.
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. :)
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.
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.
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.
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.
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)
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
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.
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:
- I set Alternate Domain Names (CNAMEs) to my base domain of haughtcodeworks.com.
- 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.
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.
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.
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.
---
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!