It’s alive. Again ;)
This blog was dead for a while. It was running on an old Mephisto system and
it’s admin interface just broke one day, throwing some cryptic exception.
It was probably not that hard to fix, but every time I thought to write a blog
post it was like “oh, I have to dig into this antient codebase to figure out
how to fix it before I actually get to write anything. I’ll pass…”
So days become weeks, and weeks become months, pretty fast its a year that
passed by with no blog posts whatsoever.
Meanwhile I was scribing my thoughts on future blogposts in markdown
files
on my harddrive.
Not long ago we finally decided to do something about it.
Getting the data out.
First we had to get the data out of the mephisto databse. We had to write our own script as
whatever we could find on the net was broken in one way or another.
We dumped all articles into simple text files.
Most of the articles were in markdown
format but some had to be converted from textile
.
Deciding where to have the new blog.
We couldn’t decide for a long time. There were a couple of options:
- standalone static site
- something under /blog/ directory in the current astrails.com site which was
implemented in Rails.
- 3rd party service.
I was for option 2 since I think its the best one for SEO and also gives us
complete control over the style, syntax coloring, urls, smart sidebar and
footer and whatnot.
But we went with options 3 at first, as it seemed the fastest one, and created
a blog on Tumblr.
Now, Tumblr is a nice service, even though we didn’t intend to use its community
features, we just needed a blog. Quite a few high profile ruby people and
companies use it, for example Thoughtbot and
many others.
But we immediately hit a couple of problems with it.
First, it doens’t let you completely control your urls, so we would have to
redirect old blog post urls to not break linkbacks and not to be punished by
Google and friends. Fortunately it does allow to define redirects, but the
process is rather cumbersome, and lets face it nice urls are, well, nice ;)
We did manage to import our old blogpost into it using their API but we had
to manually fix some styling later and once we discovered that some blogposts
had content problems and we had to re-run the import script again (which means
repeating the manual formatting again too), we decided to look again at the
option 2.
integrating blog into existing Rails app.
The hardest part was to convert the old app to Rails 3.2 as it was still
running on Rails 2. After that it was not that hard at all.
Note: the code below is somewhat simplified, we did end up adding things like
filtering by tags, authors and atom.xml support.
It all is built around model Post:
class Post
def self.model_name
ActiveModel::Name.new self
end
...
end
model_name
is needed so that we can just pass a post into link_to
like so:
What else is there?
We decided to just commit the markdown files under directory blog
preserving
the old url s tructure. So that, for example, a blogpost that was at
http://blog.astrails.com/2008/6/4/being-lazy-with-ruby
would go into blog/2008/6/4/being-lazy-with-ruby.html.markdown
.
Enumerating the files.
Once the files were in place we needed a way to enumerate them all to build the blog’s index pages:
# This is just an quick and UGLY hack ;)
def self.filenames
@@filenames ||=
Dir['blog/20*/*/*/*.html.markdown'] .
map {|f| f.gsub("blog/", "")} .
map {|f| y, m, d, p = f.split("/", 4); [y.to_i, m.to_i, d.to_i, p.split(".", 2).first]} .
sort .
reverse .
map {|f| f.join("/")}
end
Its a quick and dirty way to sort all the blogposts chronologically according to their path.
Post.find and friends
Next, lets try to make it behave somewhat like an ActiveModel (Note I didn’t
use the actual ActiveModel since I really wanted to keep this to a bare
minimum), I did ‘emulate’ the interfaces I was going to use so that it will be
trivial to convert to ActiveModel later.
attr_reader :id
def initialize(id)
@id = id
end
def self.index
@@index ||= filenames.inject({}) do |all, file|
all[file] = Post.new file
all
end
end
def self.all
index.values
end
def self.find(id)
index[id] or raise ActiveRecord::RecordNotFound.new(id)
end
def self.first
find filenames.first
end
Post#content
Once we have the blog posts indexed, we need to get the content
def path
@path ||= File.join(Rails.root, "blog/", "#{id}.html.markdown")
end
def content
@content ||= File.read(path)
end
Metadata
Besides the content we also need things like title
, author
, tags
etc.
We decided to use a YAML prefix to all markdown files, similar to what Jekyll
does. The YAML is separated from the rest by an empty line:
def parse
return if @body # already parsed
meta, @body = content.split("\n\n", 2)
@meta = YAML.load(meta).with_indifferent_access
end
def body
parse
@body
end
def meta
parse
@meta
end
Markdown rendering
Now that we have the content parsed we need to render the markdown and with
syntax highlighting.
After some investigating we decided to use
Redcarpet gem for markdown and
Albino for syntax coloring.
application_helper.rb
def markdown
@markdown ||= Redcarpet::Markdown.new(
AlbinoRenderer,
:space_after_headers => true,
:fenced_code_blocks => true)
end
def format_markdown(text)
markdown.render(text).html_safe
end
Syntax highlighting
AlbinoRenderer
turned up a little more complicated then the standard examples
found on the net as we wanted to:
- support both code blocks and in-line code fragments
- a simple syntax to declare language without using the fenced_code_blocks.
A fenced code block is a markdown extension of Redcarpet. The syntax is as following:
This is markdown text.
~~~~ruby
Here goes the ruby code
~~~~
The problem with it was mostly aesthetics ;). Vim markdown wouldn’t recognize
it as code and would freak out on underscores and such as an invalid markdown
syntax. So I wanted to use the standard 4 space indentation for code blocks,
but still be able to declare the language.
The syntax that was implemented is like this:
This is markdown text.
#!ruby
This is a ruby code
albino_renderer.rb:
class AlbinoRenderer < Redcarpet::Render::HTML
def detect_language(code)
lang, rest = code.split("\n", 2)
return code unless lang =~ /^#!\w+$/
return rest, lang[2..-1]
end
def block_code(code, language)
unless language
code, language = detect_language(code)
end
if language.present?
Albino.colorize(code, language.presence)
else
%Q{<div class="highlight"><pre>#{code}</pre></div>}
end
end
def codespan(code)
code = code.strip
if code.starts_with? '#!'
language, code = code.split ' ', 2
elsif code.starts_with? '\#!'
code = code[1..-1]
end
if language
code = Albino.new(code, language[2..-1]).colorize(:P => "nowrap=true").strip
end
%Q{<span class="highlight">#{code}</span>}
end
end
Controller and routing.
We now have all the pieces, lets glue it all together:
config/routes.rb:
match "/blog" => "posts#index"
match "/blog/:year/:month/:day/:id" => "posts#show"
posts_controller.rb:
class PostsController < ApplicationController
caches_page :only => [:index, :show], :gzip => :best_compression
layout "blog"
def protect_against_forgery?;false;end
def index
@posts ||= Post.all
end
def show
id ||= [params[:year], params[:month], params[:day], params[:id]].join("/")
@post = Post.find id
end
end
Redirecting the old blog urls
Now that the blog has moved to the new home, we need to redirect the old urls.
At first I tried with Rails 3 constraints
(I know, its kind of long, but that
was a debuggin version, where I wanted to print every segment):
# redirect from blog
constraints :subdomain => /blog\.?/ do
match '(*path)' => redirect { |params, req|
subdomain = req.subdomain.gsub(/^blog\.?/, '').presence
domain = req.domain
path = ['blog', params[:path].presence].compact * "/"
query = req.query_string.presence
host = [subdomain, domain].compact * "."
url = ["http://#{host}/#{path}", query].compact * "?"
URI.escape url
}
end
The problem with it was that whenever a page was page-cached, like
public/index.html, it would be returned before it hits Rails routing, so
http://blog.astrails.com/ would not redirect
displaying http://astrails.com content instead.
So instead we ended up with a Rack redirector:
in config/application.rb
:
config.middleware.insert_before 0, 'BlogRedirector'
blog_redirector.rb:
class BlogRedirector
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if handle?(request)
[301, {"Location" => redirect_url(request)}, self]
else
@app.call(env)
end
end
def each(&block)
end
protected
def redirect_url(request)
request.base_url.sub("//blog.", "//") + "/blog" + request.fullpath
end
def handle?(request)
request.host.starts_with?("blog.") && "/robots.txt" != request.fullpath
end
end
Making it look like a blog
Now, all this low level stuff is cool, no arguing about that. But someone will
have to read it and that means it has to be readable and look like a blog.
We used twitter bootstrap for layout
and general structure and styled it to our liking, added a photo of an author
to make it personal, big fat date for each blog post so you can keep track,
sidebar with tags filtering and some useful links etc.
Now, we no designers here, but i think it looks ok. Am i wrong here?
And we blog again
Now we can avoid using 3rd party blog admin interfaces, one stranger than
another. We use Vim to throw down markdown blog posts, along with relevant code
pieces and it’s all get rendered and cached as it should.
Writing posts is a nice and simple experience, using familiar tools is warm and cozy, so, we’ll see you
soon.