Create a simple Jekyll-like blog in your Rails 4+ app

As I wrote in my first blog post, I had a hard time deciding how to add a blog to my app. Should I use Jekyll, another Rails blog engine, or just build a simple blog functionality myself? I already use Jekyll for my private developer blog, and I like it. But in this case I decided to write my own – and I'll show you why and how.

Why not Jekyll?

I couldn't find a way to deeply integrate Jekyll into the Rails application. While the bloggy gem is a good try – it places the blog into the config folder, gives you a route, and has tasks to generate the blog – there are still too many issues. You have to duplicate your layout, views, the stylesheets, and so on. You cannot use the asset pipeline, and all links, for example in your header and footer, need to be hard coded. If you want a more separate blog area, it will be a good fit. But not if it should be a part of the site.

The implementation

The implementation of my own system should be dead simple and include the features I appreciate from Jeykll:

Plain text files to edit in your text editor of choice

Text files in Markdown format

Code highlighting

YAML front matter for metadata

An Atom feed

Let's start with the most important part: the Article model. The whole blog "system" is placed inside the blog namespace.

# app/models/blog/article.rb class Blog :: Article include ActiveModel :: Model attr_accessor :title , :content , :created_at , :permalink , :author # Used for ATOM-feed id def id @permalink . to_i end def content remove_yaml_frontmatter_from @content end def excerpt content . split ( '<!--more-->' ). first end def more_text? content != excerpt end def created_at @created_at . to_date end def to_param @permalink . parameterize end # Query methods def self . all article_files . reverse . map do | file | self . new extract_data_from ( file ) end end def self . find_by_name ( name ) file = find_file_by ( name ) self . new extract_data_from ( file ) end private def self . article_files sort_by_id Dir . glob ( articles_path + '/' + '*.md' ) end def self . sort_by_id ( files ) files . sort_by { | x | File . basename ( x , '.*' ). to_i } end def self . find_file_by ( name ) id = article_files . index { | x | x =~ / #{ name } .md/ } article_files [ id ] end def self . articles_path Rails . root . join ( 'app' , 'views' , 'blog' , 'published' ). to_s end # Content retrieval def self . extract_data_from ( file ) { content: File . read ( file ), permalink: File . basename ( file , '.*' ) }. merge ( yaml_frontmatter_metadata_from ( file )) end def self . yaml_frontmatter_metadata_from ( file ) YAML . load_file ( file ) end def remove_yaml_frontmatter_from ( text ) text . sub ( /^\s*---(.*?)---\s/m , "" ) end end

As you can see, there are Active Record like all and find_by_name instance methods to retrieve all articles or a one. Articles are placed into the /app/views/blog/published directory and need the .md extension. The filename is used as identifier and permalink, so find_by_name finds the article by the filename. The metadata is stored in a YAML front matter block ...

--- title : " Company blog finally online" created_at : " 2014-02-01" author : " Torsten Bühl" --- The article's content goes here ...

... and can be used in our Article objects. Additionally to the content method, which just retrieves the content of the file without the front matter block, I introduced an excerpt method. excerpt either returns the content, or an excerpt if the following HTML comment is used within the article.

This will be the excerpt <!--more--> This will be the rest of the content

That's it! Dead simple as I said before. Oh wait, we still need the markdown parsing and syntax highlighting. I learned most of it from this Railscast episode. First, we need to add these two gems to our Gemfile

# Gemfile.rb gem 'redcarpet' # For the Markdown parsing gem 'pygments.rb' # Syntax highlighting

For the the pygments.rb gem to work, you need to have Python installed on your machine. If that isn't an option for you, Ryan Bates shows other gems here. Now we create the markdown and preserve_markdown (only needed with Haml) methods in our helper file.

# app/helpers/blog_helper.rb module BlogHelper class HTMLwithPygments < Redcarpet :: Render :: HTML def block_code ( code , language ) Pygments . highlight ( code , lexer: language ) end end def markdown ( text ) renderer = HTMLwithPygments . new ( hard_wrap: true , filter_html: true ) options = { autolink: true , no_intra_emphasis: true , fenced_code_blocks: true , lax_html_blocks: true , strikethrough: true , superscript: true } Redcarpet :: Markdown . new ( renderer , options ). render ( text ). html_safe end def preserve_markdown ( text ) # Used to get the indentation right in the <pre> code blocks with Haml preserve markdown ( text ) end end

To make the implementation complete I show you a sample controller, views and the routes file.

# app/controllers/blog/articles_controller.rb class Blog :: ArticlesController < ApplicationController def index @articles = Blog :: Article . all end def show @article = Blog :: Article . find_by_name ( params [ :id ]) end end

# app/views/blog/articles/index.html.haml - @articles . each do | article | % article % h1 = link_to article . title , blog_article_path ( article ) = render partial: "meta" , locals: { article: article } = preserve_markdown article . excerpt - if article . more_text? % p = link_to "Continue reading →" , blog_article_path ( article )

# app/views/blog/articles/show.html.haml % article % h1 = @article . title = render partial: "meta" , locals: { article: @article } = preserve_markdown @article . content % hr % p . action = link_to "← Back to Overview" , blog_articles_path

# app/views/blog/articles/_meta.html.haml . meta % time { pubdate: "" , datetime: article . created_at } = l article . created_at , format: :long by = article . author

# routes.rb # This gives you: # /blog # /blog/:name-of-the-article namespace :blog do resources :articles , path: '' , only: [ :index , :show ] end

I said I wanted an Atom feed, too. Let's just add a simple builder view for that – our ArticlesController takes care of the rest.

# app/views/blog/articles/index.atom.builder atom_feed do | feed | feed . title ( "Exceptiontrap Blog" ) feed . updated ( @articles [ 0 ]. created_at ) if @articles . length > 0 @articles . each do | article | feed . entry ( article , url: blog_article_url ( article )) do | entry | entry . title ( article . title ) entry . content ( markdown ( article . content ), type: 'html' ) entry . author do | author | author . name ( article . author ) end end end end

Gotchas

Well, there were a few, but I noticed a big one while writing this article: Don't use greedy regular expressions. (Yeah, you hear that all the time.)

# Before (greedy) def remove_yaml_frontmatter_from ( text ) text . sub ( /^\s*---(.*)---\s/m , "" ) end # After (non-greedy) def remove_yaml_frontmatter_from ( text ) text . sub ( /^\s*---(.*?)---\s/m , "" ) end

The greedy version removed the whole text between the first --- and the last --- , which was the code block where I showed the YAML front matter in this article.

Conclusion

As you can see, it's no big deal to write a simple blog system yourself. I intentionally decided against all the existing Rails blog engines, because they're doing much more than I needed here. The goal was to have something similar to Jekyll, but with a deeper integration into the existing application, and avoiding duplication.

I'd like to hear your opinion – just ping me at @tbuehl

This is a Nuts & Bolts Series post – join the mailing list below to get more tips & tricks.

← Back to Overview