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 ActiveRecord inspired finder methods like all
and find_by_name
. 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 wanted it. 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
gem 'redcarpet' # For the Markdown parsing
gem 'pygments.rb' # Syntax highlighting
TODO: Use Rouge (http://rouge.jneen.net/)
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.
Any feedback? Just ping me at @tbuehl.