Jekyll installation

As I was saying, I moved my blog from WordPress to Jekyll.

It's mostly finished now. All posts and comments have been moved here, and adding comments is kind of possible too.

Jekyll is a blog-aware static site generator, so from template files it generates all the HTML pages of the website. The result is a static website.

There are several interesting points of having a static site including the fact that it's damn fast and it's much more secure. The main drawback being the static part. The obvious limitation on a static blog is that there are no comments. Lots of Jekyll users are including Disqus for managing comments but since I don't like centralized stuff I went to a fully static solution (comments work by sending me emails, so all comments are moderated before being published).


Here is the workflow I use to write posts:

  • Write post using a Markdown friendly editor (on Mac I use Mou).
  • Run Jekyll on my Mac to see the end result.
  • Commit the post in my blog's git repository.
  • Push to git repository on my server (a script is launched at the end to regenerate the website).

Same for adding comments.

Installation on Debian (server side)

In this example I will assume that the blog is on

$ sudo apt-get install rubygems
$ sudo apt-get install python-pygments
$ sudo gem install jekyll
$ sudo gem install rdiscount
$ mkdir
$ cd
$ git --bare init
$ sudo git clone ~/ /srv/
$ sudo chown -R laurent:laurent /srv/
  • Line 1: Jekyll is written in Ruby and installable as a rubygem.
  • Line 2: Jekyll uses Pygments for syntax highlighting.
  • Line 3: Install Jekyll.
  • Line 4: Install rdiscount which is a faster (and less buggy) markdown parser than the default one (maruku) provided with Jekyll.
  • Line 5: Create the directory that will hold the reference git repository.
  • Line 6: Go into that directory.
  • Line 7: Create a bare git repository in it.
  • Line 8: Clone the repository, this clone will be used by Jekyll.
  • Line 9: Give me ownership of the git clone.

When I commit into the main repository, I want the checkout to be updated and the website to be generated. We can use git hooks for that, just create file ~/


unset GIT_DIR

git pull

Add it execution rights:

$ chmod +x /home/laurent/

Create a new virtual host in Apache with the file /etc/apache2/sites-available/blog:

<VirtualHost *:80>

  ExpiresActive on
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/jpg  "access plus 1 year"
  ExpiresByType image/png  "access plus 1 year"
  ExpiresByType text/css   "access plus 1 week"
  ExpiresDefault "access plus 1 week"
  DocumentRoot /srv/

In this file I'm using the expires module to specify cache policy by content types.

Activate all that:

$ sudo a2enmod expires
$ sudo a2ensite blog
$ sudo service apache2 reload

Everything is ready, just need some content in this blog.

Installation on Mac OS (client side)

First, install ruby, There are several ways to do so, I choosed MacRuby wich is fairly easy to install. Then installing Jekyll is pretty much the same.

For Pygments:

$ sudo easy_install Pygments

I won't explain the basics of Jekyll, there are a lot of websites talking about that already, just some things specific to my case.

First of all, I'm using the HTML5 video tag and Jekyll in server mode was not able to server video files properly because of unknown mime types. I had to add them in /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/webrick/httputils.rb:

    DefaultMimeTypes = {
      # (…)
      "m4v"   => "video/x-m4v",
      "webm"  => "video/webm",
      "oga"   => "audio/ogg",

As I was saying I wanted to keep comments on my blog. I came accross the jekyll-static-comments plugin but it requires to add a PHP script to send an email when the comment form is submitted. And I was to lazy for that. But Matt Palmer, the guy doing this plugin, points to an even-more-static-comments method. It's the same except emails are sent directly by the user. I like that the user can keep track of comments sent (since it's an email that is sent).

I was also interested by learning a little bit of ruby and I wanted comments to be written like posts (a YAML header followed by the content (which can be written using markdown syntax)).

So I rewrote most of the original plugin, still called static_comments.rb:

class Jekyll::Site
  attr_accessor :comments

  alias :site_payload_without_comments :site_payload

  def site_payload
    if self.comments
      payload = {
        "site" => { "comments" => self.comments.values.flatten.sort.reverse },
      payload = site_payload_without_comments
  rescue Exception => e
    puts "Site exception: " + e

class Jekyll::Post
  alias :to_liquid_without_comments :to_liquid

  def to_liquid
    data = to_liquid_without_comments
    data['comments'] = StaticComments::find_for_post(self)
    data['comment_count'] = data['comments'].length

module StaticComments

  class StaticComment
    include Comparable
    include Jekyll::Convertible

    MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)-([0-9]+)(\.[^.]+)$/

    attr_accessor :site
    attr_accessor :id, :url, :post_title, :date, :author, :email, :link
    attr_accessor :content, :data, :ext, :output

    def initialize(post, comment_file)
      @site =
      @post = post
      self.read_yaml('', comment_file)

      if'date') = Time.parse(["date"])
      if'name') =["name"]
      if'email') =["email"]
      if'link') =["link"]
      self.url = "#{post.url}#comment-#{id}"
      self.post_title = post.slug.split('-').select {|w| w.capitalize! || w }.join(' ')

      payload = {
        "page" => self.to_liquid
      do_layout(payload, {})
    rescue Exception => e
      puts "Exception: " + e

    def process(file_name)
      m, cats, date, slug, index, ext = *file_name.match(MATCHER) = Time.parse(date) = index
      self.ext = ext

    def <=>(other)
      cmp = <=>
      if 0 == cmp
        cmp = <=>
      return cmp

    def to_liquid{
        "id" =>,
        "url" => self.url,
        "post_title" => self.post_title,
        "date" =>,
        "author" =>,
        "email" =>,
        "link" =>,
        "content" => self.content

  def self.find_for_post(post) ||=[] ||= read_comments(post)

  def self.read_comments(post)
    comments =

    Dir["#{}/_comments/#{'%Y-%m-%d')}-#{post.slug}-*"].sort.each do |comment_file|
      next unless File.file?(comment_file) and File.readable?(comment_file)
      comment =, comment_file)
      comments << comment


With this plugin, comments must be in _comments directory and named like the post + a comment number (example: The content is something like that:

date: 2011-12-29 17:11
name: A. Nonymous
This is a `static` comment with **markdown** syntax.

The other benefit of doing it that way, is that I can do an atom feed for comments like that:

layout: nil
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="">
  <id>{{ site.url | xml_escape }}/</id>
  <title>{{ | xml_escape }} - Comments</title>
  <link href="{{ site.url | xml_escape }}/atom-comments.xml" rel="self"/>
  <link href="{{ site.url | xml_escape }}/"/>
  <updated>{{ site.time | date_to_xmlschema }}</updated>
  {% for comment in site.comments limit:site.paginate %}
      <id>{{ site.url | xml_escape }}{{ comment.url | xml_escape }}</id>
      <title type="html">Comment on {{ comment.post_title | xml_escape }} by {{ | xml_escape }}</title>
      <link href="{{ site.url | xml_escape }}{{ comment.url | xml_escape }}"/>
      <updated>{{ | date_to_xmlschema }}</updated>
        <name>{{ | xml_escape }}</name>
      <content type="html">{{ comment.content | xml_escape }}</content>
  {% endfor %}

For now the only other plugins I'm using are a sitemap generator plugin and a Pygments cache. But there is a plugin that I would like (I need to search for it or write it myself) that would allow me to write scheduled posts. It could be something like when the site is generated it writes in a file the next date the site should be generated again (if there are scheduled posts) and a cron task would regularly check this date and generate again the site if needed.


Comments Add one by sending me an email.