A dance with Sinatra

A dance with Sinatra

This posts describes building a lightweight Markdown-powered blog engine in Ruby.
This (2013) iteration of my site in the archives here.

I’ve never been great at keeping a blog1. Any space I’ve made for writing in the past has been as Shedbot, the moniker I’ve used for my photo/design work the past few years. I would always tread lightly posting on behalf a business, even my own. When speaking on behalf of a business you’re always conscious of offending that exec who perhaps would’ve given you that big contract if he didn’t see you talking about ManBearPig that one time.

I’ve been wanting to write my own blogging app since seeing this post by Sam Soffes’ about one of his many blog iterations earlier this year. He wrote a Sinatra app that uses a GitHub repo of markdown files as posts and caches them in Redis. I really liked the idea of using static markdown files as a post archive; no messy databases or clunky web interface to get in the way of actually writing. I built something similar.

The build

This was my first trial in server-side programming, so the build took a while. Though I had studied some Ruby previously, there’s no substitute for diving in and getting your hands dirty. I found a handful of repos on Github that performed similar actions, so I cloned a handful locally and referenced them throughout the build. While Sam’s blog was the original inspiration, his Redis integration was an added level of complexity that I wasn’t ready for. Here’s a quick overview of my my app, starting with a sample markdown file:

# A-Dance-With-Sinatra.md

title: A Dance With Sinatra
excerpt: Adventures in programming with Ruby and Sinatra.
date: 2013-09-20
tags: general
--
Lorem ipsum dolor...

In the Rakefile, line 5 iterates through each of the .md files in the entries directory, and 6 splits off each post’s metadata above --. The metadata is then ordered by reverse date and written to _directory.yaml.

# Rakefile
namespace :journal do
  task :reload do
    a = []
    Dir["entries/*.md"].each do |file|
      entry = File.read(file).split("--\n")
      meta = Psych.load(entry[0])
      a.push({
        title: meta['title'],
        excerpt: meta['excerpt'],
        date: meta['date'],
        tags: meta['tags'],
        slug: "/journal/" << File.basename(file, ".md"),
      })
      puts "#{a.count} posts indexed."
    end
    File.open("views/journal/_directory.yaml", 'w') {
      |file| file.write a.sort_by{|a| a[:date]}.reverse.ya2yaml
    }
  end
end

Now, when the user requests a URL from /journal, Sinatra checks to see if the corresponding file exists in /entries. If so, it’ll load up the entry’s structure according to haml template journal/entry.haml, and parse the actual post content along with the metadata.

# Application.rb
get "/journal/:entry/?" do
  file = "entries/#{params[:entry]}.md"
  if File.exist?(file)
    @entries = load_directory('journal')
    parse('/journal/entry', file)
  else
    404
  end
end

helpers do
	def parse(template, file)
  	entry = File.read(file).split("---\n")
	  @meta = Psych.load(entry[0])
  	@text = entry[1]
	  haml template.to_sym
	end
	def load_directory(dir)
	  Psych.load(File.open("views/#{dir}/_directory.yaml"))
	end
end

The markdown rendering is handled by Redcarpet with SmartyPants enabled for smart quotes and all that good stuff. Pygments.rb is used for code highlighting.

# Application.rb
class BetterRender < Redcarpet::Render::HTML
  include Redcarpet::Render::SmartyPants
  def block_code(code, lang)
    if lang
      Pygments.highlight(code, lexer: lang.to_sym, options: {linespans: 'line'})
    else
      "<div class='highlight ki'><pre>#{code}</pre></div>"
    end
  end
end

def redcarpet(text)
	markdown = Redcarpet::Markdown.new(BetterRender, fenced_code_blocks: true)
	markdown.render(text)
end

Design & typography

The sans-serif body type is Whitney and the headline slab Sentinel. Both typefaces are designed by HF&J and served by Cloud.typography. Typesetting follows the modular scale.

I designed a custom pygments stylesheet to ensure code highlighting in the browser would precisely mimic my local environment in Sublime Text 3. The colour scheme is based on the Tomorrow Night theme by Chris Kempson. The monospace font used in the code views is Source Code Pro by Adobe.

The accent colour, #3BA7BB is the colour of Cartman’s hat from South Park.

Hosting & deployment

The app is hosted on Heroku and deployed via Git.

In order to begin a post as quickly as possible, I wrote a helper function for fish shell that creates a markdown file with proper formatting and cd’s into my app directory.

#	Blog Kickstart - Fish Shell
function blog # $title $tags
  set date (date '+%Y-%m-%d')
  set filename (echo $argv[1] | tr -s ' ' | tr ' ' '-')
  set gitdir ~/documents/Journal/
  set var_count (count $argv)
  cd $gitdir
    if test $var_count = 2
  		echo title: $argv[1]\nexcerpt: \ndate: $date\ntags: $argv[2]\n---\n\n >entries/$filename.md
    else
   		echo title: $argv[1]\nexcerpt: \ndate: $date\ntags: general\n---\n\n >entries/$filename.md
   	end
  open entries/$filename.md
end

Closing

This has been a lot of fun to build, and I have a lot to do on it still. It’s endearing releasing something before it’s polished; I feel more inspired to continue work on it instead of feeling utterly and completely sick of it.

Let me know your thoughts on Twitter.

Footnotes

  1. 2020 update: still terrible