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).

Workflow

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 blog.example.com.

1 $ sudo apt-get install rubygems
2 $ sudo apt-get install python-pygments
3 $ sudo gem install jekyll
4 $ sudo gem install rdiscount
5 $ mkdir blog.example.com.git
6 $ cd blog.example.com.git
7 $ git --bare init
8 $ sudo git clone ~/blog.example.com.git/ /srv/blog.example.com
9 $ sudo chown -R laurent:laurent /srv/blog.example.com/
  • 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 ~/blog.example.com.git/hooks/post-receive:

#!/bin/sh

unset GIT_DIR
GIT_REPO=/srv/blog.example.com

cd $GIT_REPO
git pull
jekyll

Add it execution rights:

$ chmod +x /home/laurent/blog.desgrange.net.git/hooks/post-receive

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

<VirtualHost *:80>
  ServerName blog.example.com

  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/blog.example.com/_site
</VirtualHost>

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 },
      }.deep_merge(site_payload_without_comments)
    else
      payload = site_payload_without_comments
    end
    payload
  rescue Exception => e
    puts "Site exception: " + e
  end
end

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
    data
  end
end

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.site
      @post = post
      self.process(comment_file)
      self.read_yaml('', comment_file)

      if self.data.has_key?('date')
        self.date = Time.parse(self.data["date"])
      end
      if self.data.has_key?('name')
        self.author = self.data["name"]
      end
      if self.data.has_key?('email')
        self.email = self.data["email"]
      end
      if self.data.has_key?('link')
        self.link = self.data["link"]
      end
      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
    end

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

    def <=>(other)
      cmp = self.date <=> other.date
      if 0 == cmp
        cmp = self.post.slug <=> other.post.slug
      end
      return cmp
    end

    def to_liquid
      self.data.deep_merge({
        "id" => self.id,
        "url" => self.url,
        "post_title" => self.post_title,
        "date" => self.date,
        "author" => self.author,
        "email" => self.email,
        "link" => self.link,
        "content" => self.content
      })
    end
  end

  def self.find_for_post(post)
    post.site.comments ||= Hash.new()
    post.site.comments[post.id] ||= read_comments(post)
  end

  def self.read_comments(post)
    comments = Array.new

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

    comments
  end
end

With this plugin, comments must be in _comments directory and named like the post + a comment number (example: 2012-06-11-jekyll-installation-1.md). The content is something like that:

{% raw %}
---
date: 2011-12-29 17:11
name: A. Nonymous
email: anonymous@example.net
link: http://www.example.net
---
This is a `static` comment with **markdown** syntax.
{% endraw %}

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

{% raw %}
---
layout: nil
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>{{ site.url | xml_escape }}/</id>
  <title>{{ site.name | 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 %}
    <entry>
      <id>{{ site.url | xml_escape }}{{ comment.url | xml_escape }}</id>
      <title type="html">Comment on {{ comment.post_title | xml_escape }} by {{ comment.author | xml_escape }}</title>
      <link href="{{ site.url | xml_escape }}{{ comment.url | xml_escape }}"/>
      <updated>{{ comment.date | date_to_xmlschema }}</updated>
      <author>
        <name>{{ comment.author | xml_escape }}</name>
      </author>
      <content type="html">{{ comment.content | xml_escape }}</content>
    </entry>
  {% endfor %}
</feed>
{% endraw %}

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.

Links

Comments Add one by sending me an email.