This is a Nuts & Bolts Article – Tips & Tricks for Developers

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

by Torsten Bühl

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 on my own? I use Jekyll for my private developer blog and I like it. But 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 site. 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 has_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 AR-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.has_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 an easy blog system on your own. 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

Try our simple and powerful
application error tracking