People are often surprised to hear that we don’t use Meteor for this very site. After all, if anybody should want to use Meteor everywhere, it should be us.

But we’re also big believer in using the right tool for the right situation. And I personally think that when it comes to building static sites, Meteor just isn’t the right fit.

Instead, we’re using Middleman. Just like its slightly more famous cousin Jekyll, Middleman is a Ruby static site generator, only with few extra tricks under its sleeves.

In this post, I’ll highlight three hacks that we’re using on this very site:

  • Generating complex pages from YAML data.
  • Faking a “Recommended Reading” section.
  • Generating discount codes landing pages.

Let’s get started!

Please note that this post is about “Middleman” – and not “man-in-the-middle” - hacks. We apologize for any confusion.

The Power of Static

First, a quick refresher: static site generation means that instead of generating HTML pages on each server request like a classic PHP or Ruby app, Middleman will generate your templates once and for all whenever you compile your site, and then serve these static files to the client.

Having that compile phase lets you do all kinds of cool stuff, such as writing your content in Markdown and having it automatically translated into HTML.

Paul Stamatiou’s popular blog uses Jekyll.

And using static files also means minimal resource use and great scalability. For example, we’re hosting this site on a free Heroku instance; but GitHub pages is another great option, and so is S3.

Of course, static sites can’t react to user input like a server-side app would. But despite that downside, “static” doesn’t have to mean “dumb”. Here’s what I mean.

Local Data: YAML + HAML = HTML

Let’s say you were coding up a pricing page as part of a standard database-backed site (let’s say a WordPress site). Where would you store the pricing table’s actual data?

You could put it in the database, but that would also require writing some custom code to extract the values, and maybe even building some kind of user interface to edit that data. And that seems like overkill, since this data would rarely change, if ever,

So you’d probably end up including it right in the markup, which is fine except now each time you want to add a line to the table you also need to worry about all those <tr>’s and <td>’s.

Ugh. HTML code.

Middleman has found what I think is the perfect “middle”-ground (pun intended) approach with the local data feature: store your data in YAML, then iterate over it in HAML to produce the desired markup.

Let’s look at a concrete example. Here’s the YAML code for our homepage’s “features” section:

features:
- title: "14 Chapters"
  text: "The book's main chapters take you through building a social news site."
  image: "files.svg"
- title: "11 Sidebars"
  text: "In between each chapter, sidebars explain a specific aspect of Meteor."
  image: "files.svg"
- title: "17 Notes"
  text: "We answer all your questions, and make sure you don't miss the important stuff."
  image: "note.svg"  
- title: "18 Diagrams"
  text: "Detailed diagrams explain concepts like reactivity and latency compensation."
  image: "diagram.svg"
- title: "45 Code Commits"
  text: "We provide git commits for every code change to help you follow along."
  image: "code.svg"
- title: "45 Live Instances"
  text: "Every commit is also linked to a live demo instance of the app at this stage."
  image: "browser.svg"
/data/features.yml

And here’s the HAML code for the corresponding partial:

- data.features.features.each_with_index do |feature, index|
  .feature
    %img{:src => "/images/#{feature.image}"}
    %h3=feature.title
    %p=feature.text

And finally, here’s the result:

Our homepage’s features list

Throughout the site, we’ve used that technique for every single section that features repeating markup. This includes the case studies section, reviews page, packages page, and more.

Recommended Reading

A classic feature of many blogs is “Recommended Reading” sections, where you try and suggest what the user should read next.

There are all kinds of algorithms to achieve this, from keyword-based analysis to machine learning, but these more complex strategies are sadly out of the reach of our poor HTML files. So instead we’ll settle on something much simpler: picking a random post out of a list.

CNN’s advanced recommendation engine: “You seem interested in Syria, so how about some Britney Spears news?”

Now even this is tricky, as there’s no way to dynamically change the contents of a page with HTML alone. So we’ll have to fake randomness: we’ll pick a random post to recommend at the bottom of each page at compilation time.

This does mean that once the random post has been set, it will remain the same until the next time we compile our static files. But that doesn’t really matter: after all, what really matters is that recommended posts change between each page.

We have a second requirement: we want to make sure we only feature our best posts. So we’ll add a featured: true flag to the posts we’d like to feature YAML metadata to set them apart:

---
title: Understanding Meteor Publications & Subscriptions
author: sacha
published: true
date: 2014/01/02
thumb: pubsub.png
tags: Article
featured: true
---

The way Meteor handles an app's data is one of the framework's greatest assets, but also one of the hardest things to wrap your head around when you're just getting started. 

...

We’re now ready to write our random-selection code. First, we’ll get a list of all posts whose featured flag is set to true. Since some posts have no featured flag at all, we’ll add in a rescue to take care of the exception:

- featured = blog.articles.select{|item| item.metadata[:page]["featured"] rescue false}

We’ll then make sure to exclude the current article. After all, we don’t want to end up recommending the very article you just read!

- featured.delete_if{|item| item == current_article}

Finally, we’ll pick an article at random our of the list and feed it to our summary partial:

=partial("partials/summary", :locals=>{post:featured.sample()})

And here’s what the finished product looks like:

Recommended posts.

Now of course, as far as recommendation features go, let’s just say this one won’t win the Netflix prize anytime soon. But from a more pragmatic point of view, this is still a great way to drive up engagement without the complexity of a database-backed system.

Smart Coupon Codes

The final hack is my favorite, because it really showcases the power of one of Middleman’s more unique feature, Dynamic Pages.

Whereas the Local Data feature we previously discussed lets you fill in a page’s content from YAML data,Dynamic Pages let you generate a whole range of pages from a YAML array.

For example, if you were selling fruit, you could simply feed in something like:

- name: 'Apple'
  price: '$2.99'
- name: 'Orange'
  price: '$1.99'
- name: 'Banana'
  price: '$0.99'

And from that generate http://www.mysite.com/apple, http://www.mysite.com/orange, http://www.mysite.com/banana, etc. landing pages.

Pretty powerful! But how does that relate to discount codes? Glad you asked!

It all started when I learned that adding a promo code field to your check-out form can actually decrease conversion rates, because potential customers end up abandoning their shopping cart to go look for a coupon and sometimes forget to ever come back.

One of the proposed solutions was to use a special URL (i.e. http://www.mysite.com/10percentoff) to trigger the discount instead of relying on a manually inputted code.

The good news is that our payment provider Gumroad already supported this feature, meaning that accessing https://gumroad.com/l/name_of_product/discount_code will trigger the appropriate discount provided it has already been entered in Gumroad’s UI. But how could we implement it on our side?

When accessing a special URL, Gumroad automatically applies the right discount.

In other words, how can we set up discovermeteor.com/10percentoff, discovermeteor.com/20percentoff, etc. for as many codes as we need? Ideally we’d want these landing pages to be identical to the original, except that the “buy now” link to Gumroad would include the right discount code.

Once again, Middleman comes through. Remember that I said we could use Dynamic Pages to generate a list of pages. So the first step will be storing our discount codes in one of our trusty YAML files:

- code: '20percentoff'
- code: '30percentoff'
- code: 'goodguydiscount'
- code: 'freecarwitheachpurchase'
...
/data/promocodes.yml

We’ll then tell Middleman to generate specific landing pages for each of these codes by editing config.rb:

data.promocodes.each do |item|
  proxy "/#{item.code}.html", "/promocode.html", :locals => { :code => item.code }, :ignore => true
end
/config.rb

Let’s examine what we’re doing exactly. The proxy method takes four arguments. First we pass the path we want to create, in this case /20percentoff, /30percentoff, etc. (Middleman will take care of removing the .html for us later on).

Then, you tell Middleman which template you want to use to build those pages. In our case we’ll use the promocode.html template.

We then pass our item.code variable to the template and make it available as code, and pass ignore => true to tell Middleman not to build the promocode.html template just by itself.

Our promocode URLs now work, but they point to an empty template. We could set a cookie and then use a JavaScript redirection, but there’s a simpler way: we’ll store the discount code using the content_for method, then include our whole index template like so:

- content_for(:discount_code, code)

=partial("index")
/source/promocode.html.haml

We then modify our index.html.haml to make use of the discount code if it exists:

- if content_for?(:discount)
  %a{:href => "https://gumroad.com/l/name_of_product/#{yield_content(:discount_code)}"} Get the book
- else
  %a{:href => "https://gumroad.com/l/name_of_product"} Get the book
/source/index.html.haml

If we stopped here, we’d have a working implementation, but we’d also end up with a lot of duplicate content on Google. To avoid any SEO penalty (as well as make our discount codes harder to find) let’s add a canonical <link> tag to our landing pages, as recommended by Google.

We’ll include that tag in our <head>, presumably inside the global site layout:

%link{rel => "canonical", href => "https://www.discovermeteor.com"}
/source/layouts/layout.haml

Now a small disclaimer: this specific implementation will work great as long as your discount code URL points to the page that has your “buy now” link.

For multi-page sites, you’ll have to add in a bit of JavaScript to set a cookie so that the discount code can persist across pages (which is what we’re doing on this site). But the overall principle remains the same.

The finished implementation on the Discover Meteor homepage.

The Future is… Static?

Maybe it’s because I spend most of my time hacking on real-time Meteor projects and I subconsciously yearn for a break, but I find that there’s a certain appeal to working on static sites: there’s no server-side code, no database, and very few moving parts.

And it turns out that we often don’t need all of this to accomplish what needs to be done. In 2014, it’s a good reminder that sometimes the simplest solutions are the best!