A blog for coders


Here’s how to include syntax-highlighted code examples from actual source files in your blog posts.

def example
  puts "Like this!"
end

At OK GROW! we’re all developers, and when we’re writing about code we want to do it the same way we write code.

When I have a ruby code example I want it to actually be a ruby file rather than text pasted into an HTML editor. Then I can run and test the source code examples and edit them in my favorite editor as usual, with no cutting and pasting.

So we built that. We wrote a little helper for the Middleman blogging extension that can include code examples from actual source files (entire files or a specific range of lines).

A blog article is a markdown file (or any other template format supported by Middleman) plus an optional directory containing source code files (actual .rb or .js or anything else) and images.

Here’s the directory structure for this article:

posts
├── 2013-03-17-how-our-blog-works.markdown.erb
└── 2013-03-17-how-our-blog-works
    ├── blog_helpers.rb
    └── config.rb

HOW TO (the quick version)

You need a Middleman site with the blogging extension and our <%= download_link(‘blog_helpers.rb’) %> file.

Then you can write articles as markdown + ERB, and use the render_code helper. For example, this article is the file 2013-03-17-how-our-blog-works.markdown.erb and it includes the file blog_helpers.rb (which it renders with syntax highlighting) using the following:

<%= '<' + '%= render_code("blog_helpers.rb", "ruby") %' + '>' %>

It also includes lines 1 to 24 only of config.rb using the following:

<%= '<' + '%= render_code("config.rb", "ruby", :range => [1, 24])  %' + '>' %>

Both of those source files are in the same subdirectory where the Middleman blogging extension would expect to find images and other files related to this post, 2013-03-17-how-our-blog-works.

HOW TO (the full version)

Step 1: configure a Middleman blog

First, configure Middleman with the Blogging extension.

Run the following command:

middleman init MY_BLOG_PROJECT --template=blog

Then update Gemfile and config.rb to look like the following:

source :rubygems

gem "middleman"
gem "middleman-blog"
gem "middleman-syntax"
gem "redcarpet"
gem "builder"
# Generate something/index.html instead of something.html
activate :directory_indexes

# Blog settings tweaked to work with directory_indexes This puts all blog
# pages under /posts/, you can probably disable that just by commenting out
# blog.prefix
activate :blog do |blog|
  blog.prefix = "posts"
  blog.permalink = ":year/:month/:day/:title/index.html"
  blog.taglink = "tags/:tag/index.html"
  blog.layout = "post"
  blog.year_link = ":year/index.html"
  blog.month_link = ":year/:month/index.html"
  blog.day_link = ":year/:month/:day/index.html"
  blog.tag_template = "tag.html"
  blog.calendar_template = "calendar.html"
end

# Use a custom layout for everything under /posts/
page "/posts", :layout => "posts"

# Don't use a layout for the Atom feed
page "/feed.xml", :layout => false

Note that this configuration puts all articles under /posts, you could comment out blog.prefix to change that.

Step 2: Add our helper methods

Here’s where you get our fancy render_code method.

Put blog_helpers.rb into /lib:

module BlogHelpers
  def render_code(filename, language, options = {})
    code = File.readlines(guess_full_path(filename))
    if options[:range]
      if !options[:range].is_a?(Array) ||
        options[:range].length != 2 ||
        ! options[:range].first.is_a?(Integer) ||
        ! options[:range].last.is_a?(Integer)
        raise "range must be an array of two integers"
      end
      first = options[:range].first
      last = options[:range].last
      code = code[(first - 1)..(last - 1)]
    end
    options = {
      :linenos => "table",
      :linenostart => first || 1,
      :encoding => 'utf-8'
    }

    formatted_code = Pygments.highlight(code.join, :lexer => language, :options => options)

    # Workaround for https://github.com/tmm1/pygments.rb/pull/51
    formatted_code += '>' unless formatted_code =~ />\z/

    <<-HTML
<div class='code-block'>
  #{download_link(filename)}
  <div class='rendered-code'>
    #{formatted_code}
  </div>
</div>
HTML
  end

  def download_link(filename, display_text = filename)
    %{<a href="#{filename}" download="#{filename}" class="download-link">#{display_text}</a>}
  end

  private

  # This is a bit of a hack to get the full path of a file relative to the
  # current template. There's probably a better way, but this works, and
  # efficiency isn't an issue since it happens only at site build time.
  def guess_full_path(filename)
    # Read the file from the directory that the calling template is in
    # Assume there's one method (from this module) in the callstack before the
    # template
    caller[1] =~ /([^:]*):\d*/
    template = File.basename($1)
    template =~ /([^\.]*)\./
    dir = $1
    File.join(settings.source, "posts", dir, filename)
  end
end

Add this to config.rb:

# Use middleman-syntax and redcarpet for our markdown engine (redcarpet is
# required)
set :markdown_engine, :redcarpet
set :markdown, :fenced_code_blocks => true,
               :autolink => true,
               :smartypants => true
activate :syntax

# Include and configure lib/blog_helpers
require 'lib/blog_helpers'
helpers BlogHelpers

And here’s some CSS for the syntax highlighting, put this in /source/stylesheets:

<%= Pygments.css %>

That’s it, you’re done!

Now you have a dynamically-generated site that can be hosted on any static site hosting service, I recommend Amazon S3. We deploy to S3 using S3Sync, but I’ll save that for a future post.

No more cutting and pasting source code into blog articles. That’s one example of why we went DIY instead of using a hosted blogging service. As developers this is our playground. Publishing online isn’t just writing text. We can tweak this to perfection and add cool interactive features like Examplifier. There’s nothing stopping us except our own imagination.

Let us know what you think in the comments.

Let's stay connected. Join our monthly newsletter to receive updates on events, training, and helpful articles from our team.