By KEN COLLINS   Ken Collins

Do you want to use your existing Rails' layouts & business logic for your new Blog? Do you know and love Jekyll, but cant seem to get the two to play nice together?

Jekyll on rails

In the spirit of doing the simplest thing that could possibly work, I set out on a solution to this problem. Besides mimicing Jekyll as closely as possible, my requirements were loosly defined as:

Additionally if I had time or the gumption:

Most times I like to start coding to high-level interfaces. It helps me think about the smaller details I will need to build later. A Rails controller is a good option but be careful and keep it simple! If you are the type of person that likes to jump to the end, check out the final gist of our completed work.

First, we are going to make our blog routes and controller. We want two actions, an index to list all blog posts and a show to render each post.

# In config/routes.rb
resources :blog, only: [:index, :show]

# In app/controllers/blog_controller.rb
class BlogController < ApplicationController

  layout 'site'
  before_filter :find_post, only: [:show]

  def index
    @posts = BlogPost.all
  end

  def show
    render text: @post.html, layout: 'site' if stale? @post, public: true
  end

  private

  def find_post
    @post = BlogPost.find params[:id]
    redirect_to blog_index_path unless @post
  end

end

Pretty simple looking, but this code exposes some neat details that we will have to build while accomplishing a lot too. Our index action shows we will need a class finder for all posts and assumes they will be ordered somehow. The show action which is behind our find_post filter tells us we need some type of key for our posts – likewise we have determined that we need an html instance method. Lastly, we are going to check when a post is stale somehow while setting the public Cache-Control header for proxies.

Our :id key will use the file name as our slug. Our markdown files will be located where our controller would normally find them, in the app/views/blog directory. Feel free to create your first post there, name it 2014-04-19-my-first-post.md using the current date. Now here is our first itteration of our new BlogPost model that allows us to find our posts.

class BlogPost

  attr_reader :slug

  class << self

    def all
      all_slugs.map{ |slug| new(slug) }.sort
    end

    def find(slug)
      all.detect { |post| post.slug == slug }
    end

    def directory
      Rails.root.join 'app', 'views', 'blog'
    end

    private

    def all_slugs
      @all_slugs ||= Dir.glob("#{directory}/*.md").map { |f| File.basename(f).sub(/\.md$/,'') }
    end

  end

  def initialize(slug)
    @slug = slug
  end

end

Notice how we memoize the private all_slugs class method to avoid hiting the filesystem again and again in production? This idea is definitly not complete and if needed, we can update it later. So, with the class methods out of the way, we can flesh out the model more. But first, we need to think about Jekyll.

I've used Jekyll for a handful of static sites and blogs. Most of which have been running the next major version which is not quite out yet. Jekyll v2 is amazing and we totally need it for this project. Below are the gem deps you will need to bundle up. Remember, when Jekyll v2 is released, you can change your Gemfile as needed.

# In Gemfile
gem 'jekyll', '~> 2.0.0.alpha'
gem 'redcarpet'
gem 'rouge'

So, most of us are familiar with the Redcarpet gem. That will be the markdown converter we use, but what about rouge? The Rouge gem is a pure-ruby syntax highlighter. It can highlight over 60 languages, and output HTML or ANSI 256-color text. Its HTML output is compatible with existing stylesheets designed for pygments.

class BlogPost

  # ...

  private

  def to_html
    Jekyll::Converters::Markdown::RedcarpetParser.new({
      'highlighter' => 'rouge',
      'redcarpet' => {
        'extensions' => [
          "no_intra_emphasis", "fenced_code_blocks", "autolink",
          "strikethrough", "lax_spacing",  "superscript", "with_toc_data"
        ]
      }
    }).convert(markdown)
  end

end

It is not just sexy, Rouge is fast too! And by using it, we avoid bundling to pygments which has Python dependencies. We are going to let Jekyll do the heavy lifting and implement our private to_html method like so. This will give us Github flavored markdown using Rouge for our fenced code blocks.

In our BlogPost.all class method, we called sort on the collection with the expectation that the newest post comes first. To fulfill this we are going to use Ruby's <=> spaceship operator by comparing each post's date. This completes our controller's index requirement.

class BlogPost
  # ...
  def <=> other
    other.date <=> date
  end
  # ...
end

We want each post to have a title and date method. We can easily parse these form the slug. I even made a date_formatted method using ActiveSupport's Inflector that will be useful for our views. It returns date strings like April 19th, 2014. We also have a path method that we can use when linking to our posts from elsewhere in our site. It will use the slug as the :id param. This completes our controller's find_post requirement and gives us pretty URLs just like Jekyll.

class BlogPost
  # ...
  def title
    slug.sub(/\d{4}-\d{2}-\d{2}-/, '').titleize
  end

  def date
    Date.parse(slug)
  end

  def date_formatted
    day_format = ActiveSupport::Inflector.ordinalize(date.day)
    date.strftime "%B #{day_format}, %G"
  end

  def path
    "/blog/#{slug}"
  end
  # ...
end

Before implementing our markdown to HTML conversion, we should think about caching. Following Rails' conventions we implment a cache_key method that combines the blog namespace with the posts unique slug and timestamp. The updated_at timestamp is simply the last time the file was modified on disk. Convention is to convert this to an integer which is seconds since Unix Epoch. The cache_key will be used by the controller's show method when determining the ETag for the response.

class BlogPost
  # ...
  def updated_at
    File.mtime(file_path)
  end

  def cache_key
    ActiveSupport::Cache.expand_cache_key ['blog', slug, updated_at.to_i]
  end
  # ...
end

Now the fun part, converting our markdown to HTML. We are going to use the Rails.cache which in local development is a simple memory store and Memcached in production. The fetch method takes a key and a block. The block is only executed if there is a cache miss.

class BlogPost
  # ...
  def html
    Rails.cache.fetch("#{cache_key}/html") { to_html }
  end


  private

  def file_path
    self.class.directory.join "#{slug}.md"
  end

  def markdown
    Rails.cache.fetch("#{cache_key}/markdown") { File.read(file_path) }
  end
  # ...
end

The only thing left to do is style the highlighted output from the Rouge gem. Thankfully, there are many existing pygment theme options spread accross the internet. Here are a few to get you started.

For the HomeMarks blog, I used the github jekyll-github.css from the last resource above. Have fun choosing your own and @import it into your existing CSS bundle in the manner that matches your own project.

So that's it, we can ship it! But what about those other great requirements like YAML front matter, ERB templates, Rails' routes/helpers, and more? The good news is that I found the time to do them all. You can find each implemented in the final Jekyll-Style Blogging On Rails gist.

def scope
  ApplicationController.helpers.clone.tap do |h|
    h.singleton_class.send :include, Rails.application.routes.url_helpers
  end
end

One aspect that I was particularly proud of was ERB pre-processing with a evaluation scope to Rails' routes and helpers. The solution turned out to be simple after digging into the railties IRB console helpers and ActionPack. My solution above used this scope with the Tilt's erubis template.

With this setup, you have a great foundation for Jekyll on Rails. Feel free to customize it as needed. Some ideas include:

Thanks for reading! Would love to hear your feedback too.

HomeMarks is your new todo list, internet springboard, research assistant, inspiration repository, and application launcher. Bookmarks Reimagined!
comments powered by Disqus