Jekyll-Style Blogging On Rails
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?
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:
- Use a directory of markdown files with a
YYYY-MM-DD-blog-post-title.md
naming convention. - Have fast syntax highlighting via fenced code blocks.
- Expose model class finders for a single resource & collections.
- Some model object to ask posts for their title, date, html, etc.
- Make local development follow Rails conventions. Like page refreshes.
- Provide a reasonable amount of caching in production.
Additionally if I had time or the gumption:
- Use YAML front matter for different post titles & other options.
- Preprocess markdown with ERB and allow Rails routes & helpers to be used.
- Ask each post for an excerpt so we can render teasers elsewhere.
- Create a rake task that tees up a new post using the current date.
Getting Started
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 BlogPost Model
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.
Time For 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.
Other BlogPost Details
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
Code Highlighting
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.
- pygments-css - CSS files created from pygment's built-in styles.
- GitHub-Dark - GitHub Dark theme for Stylish. Browse the themes directory.
- pygments-github-style - GitHub style for Pygments.
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.
Taking It Further?
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.
Where To Go From Here?
With this setup, you have a great foundation for Jekyll on Rails. Feel free to customize it as needed. Some ideas include:
- Better caching in production. Currently hits the filesystem for
cache_key
. - Maybe make a jekyll-rails engine that does all of this for you?
- Add an Emoji HTML filter to your posts.
Resources
Thanks for reading! Would love to hear your feedback too.