Jekyll comments

Sven Illert -

I’m a big fan of resource efficiency and for this reason I have chosen Jekyll as software for my blog. I don’t really like the idea to use big CMS like software like the big WP which use a database and tons of code for simple static content. Anyways, that’s just my spleen and geekiness. One backdraw of this choice is, that naturally there’s no dynamic content and therefore by default no possibility to leave a comment for a blog post.

But as you may have noticed, my blog does have one. So what did I do to achieve this? Of course there are possibilities to externalize such a feature to various services. If you have a github account you may use something like Staticman. But I don’t want to depend on such services and so I went my own way which was inspired by a post from Robert Riemann about the same topic.

Jekyll has a nice feature to handle JSON, YAML or other data when generating the target web pages from source. That’s the easy part. Have a look at my template code I am using for the comments section:

<section id="comments">
{% if site.data.comments[page.slug] != null %}
    {% assign comments = site.data.comments[page.slug] | where:"published","true" %}
    {% if comments.size > 0 %}
    <h1>Comments</h1>
    {% endif %}
    {% for comment in comments %}
    {% if comment.published == "true" %}
    <h2 data-content="{{ comment.date | date: '%d.%m.%Y' }}">{{ comment.name }}</h2>
    {% if comment.url %}
        <p class="commenturl"><a href="{{ comment.url }}">{{ comment.url }}</a></p>
    {% endif %}
    <p>{{ comment.message | newline_to_br }}</p>
    {% endif %}
    {% endfor %}
{% endif %}
    <h1>Add Comment</h1>
    <p id="comment-explain">Comments will be shown after approval.</p>
    <form id="contactform" accept-charset="utf-8" action="/cgi-bin/comment.rb" method="POST">
        <fieldset id="contactfieldset">
            <label for="name">Name:</label><input type="text" id="name" name="name" placeholder="Name" required>
            <label for="url">URL (optional):</label><input type="text" id="url" name="url" placeholder="URL">
            <label for="answer">Enter "fünf" in the field below:</label><input type="text" id="answer" name="answer" placeholder="Answer" required>
            <label for="message">Message:</label><textarea rows="10" id="message" name="message" placeholder="Message" required></textarea>
            <input type="hidden" id="slug" name="slug" value="{{ page.slug }}">
            <button type="submit">Send</button>
        </fieldset>
    </form>
</section>

From above you can see that you can simply traverse an autogenerated nested data array to access content. The data stems from the directory _data in your Jekyll base directory and in my case the comment files follow the layout _data/comments/<post-slug>/comment-<time>.yml. For each post there is an item in the array site.data.comments which itself is an array of comment objects. The base directory can easily be linked to another directory on your host to separate foreign from your own data. To upload the comment data using the form above I use a Ruby script using the good old CGI. There’s no need to build up a fully fledged web application using another web framework.

The comment.rb (which you may find below) simply parses the input and errors out if something was not pleasant to my needs. It works well with multiple domains and has some sort of protection against spam. Of course I will improve it further when I notice any security risks. But for now I’m quite happy to have a possibility for you to comment. Since by default the comment is not published I let me notify by email including the path to the newly created file. So I can ssh to my server, edit the file to have a preview and change it’s published-state. After that I just need to rebuild the website which handles Jekyll really fast. That’s it.

If you have any hints to improve the script right now, leave a comment!

#!/usr/bin/ruby

require "cgi"
require "date"
require "fileutils"
require "yaml"
require "open3"
cgi = CGI.new("html5")

vardir = "/var/www/jekyll"

cgi.host =~ /((.*\.|).+)\.(.+)/
config = $1

if config == nil
    raise StandardError, "Invalid hostname: " + cgi.host
end

fileprefix = "/srv/www/" + config

if !Dir.exist?(fileprefix)
    raise StandardError, "No such directory: " + fileprefix
end

if cgi.request_method != "POST"
    cgi.out("status" => "METHOD_NOT_ALLOWED", "type" => "text/html") { open(fileprefix + "/405.html", "r").read() }
end

match = false
patterns = [/http(|s):\/\/((.*\.|)elblandknipser)\.(de|photos)/, /http(|s):\/\/(.*\.|)isdba\.de/]

patterns.each  do |pattern|
    if cgi.referer =~ pattern
        match = true
    end
end

if !match
    cgi.out("status" => "FORBIDDEN", "type" => "text/html") { open(fileprefix + "/403.html", "r").read() }
    exit
end

correct = "fünf"
answer = cgi['answer'].downcase
name = cgi['name']
message = cgi['message']
url = cgi['url'].strip
slug = cgi['slug'].gsub(/[^a-zA-Z0-9\-]/, '')

if correct != answer
    cgi.out("status" => "PRECONDITION_FAILED", "type" => "text/html") { open(fileprefix + "/412.html", "r").read() }
    exit
end

output = Hash.new
date = DateTime.now

output['date']      = date.iso8601
output['slug']      = CGI::escapeHTML(slug)
output['name']      = CGI::escapeHTML(name)
output['message']   = CGI::escapeHTML(message)
output['published'] = 'false'

if url && !url.empty?
    output['url']   = url
end

datadir = vardir + "/" + config + "/_data/comments/" + slug
filename = "comment-" + date.strftime('%s') + ".yml"
FileUtils.mkdir_p(datadir)
File.open(datadir + "/" + filename, "w") do |f|
    f.write(output.to_yaml)
end

successmsg = open(fileprefix + "/success.html", "r").read()
successmsg = successmsg.sub('##backlink##', cgi.referer)

to = 'my@mail.srv'
from = 'my@mail.srv'

subject = 'Neuer Kommentar von ' + name + ' auf ' + cgi.server_name
Open3.popen2("/usr/sbin/sendmail -t -f " + from) {|i,o,t|
    i.print "To: " + to + "\n"
    i.print "From: " + from + "\n"
    i.print "Subject: " + subject + "\n\n"
    i.print message + "\n\n"
    i.print datadir + "/" + filename + "\n"
    i.close
}

cgi.out("status" => "OK", "type" => "text/html" ) { successmsg }