Introduction

I have used ikiwiki since four years as my blog engine. I like it very much and really enjoy the fact that everything is stored in a git repository. There are lots of plugins to deal with different sorts of directives to publish content more easily. With Ikiwiki I've been more focused on content than on publishing processus.

But there is one point I needed to change and here it is how I achieve it !

What's the problem ?

I really wanted to have a sidebar as a navigation tool in the blog. For me, a true sidebar needs a calendar, or as I said: a way to navigate in the blog using time.

But this has the great drawback to launch a rebuilding of the whole site... I took time to understand what was the problem. The symptom was: whatever was published or modified in the blog part of the site, the blog was always completely rebuilding itself.

I modified the sidebar content to follow advices founded on Ikiwiki web site. But it changed nothing. Then I started to understand: whenever an article is published, the calendar needs to be rebuilded. As there is a calendar directive in sidebar.mdwn, the sidebar is rebuilt, then eveything in the blog is rebuilt as everything in the blog depends on sidebar !

Until now, it was not really a problem because all of my articles were very light with very few pictures. Since I post the Ireland article, the site took about 45 minutres to rebuild completely on my Sheevaplug. It was time to find a solution.

How I solved it

I am not a Perl developer anymore. I used to learn Perl more than ten years ago but now, I have learned Python and I don't want to rediscover the crap of Perl semantic and grammar. My workaround could not been a Perl code. So what about something in HTML+Javascript thing ? After all, ikiwiki is an html compiler !

So I just build a Javascript dynamic calendar. The code is charged everytime you load a page on the blog. It uses the CSS class of the original ikiwiki calendar plugin so there will be few changes on the rendering of the calendar. There is a main object called ikiwiki_cal which just build itself in the render method. It is initialized with the date of the current day or the month of the archive page if you are in a archive page (URL likes ../archives/2014/08). When you click on the next or prev month, calendar content is dynamically changed without reloading the page where you are.

On loading, an XMLHTTPRequest is made on a file named calendar.json. This file stores the list of all the articles published on the blog in a JSON format. With articles come date of publication (like printed in meta directives) and URL of the article. This JSON is parsed when you load the page and its content is used to build the calendar. When an article has been published during the current calendar month, the day by which it has been published becomes a link to the article.

It was my first Javascript/AJAX/JSON code. I just can say that some things were really fast to code and others (AJAX) was a real pain in the ass. To be short, I hate the grammar to declare a class (anonymous functions) and I took some time to really understand callback function mechanism...

Here is the code:

/*
 * Javascript Calendar for Ikiwiki
 * You have to define a JSON file that grab all of the articles date
 * to populate the calendar.
 * The goal of this code is to make ikiwiki sidebar plugin compatible with calendar
 *
 * Copyright 2014 Médéric RIBREUX
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

Date.prototype.isLeap = function() {
    var year = this.getFullYear();
    return year % 400 === 0 || ( year % 4 === 0 && year % 100 !== 0);
}

Date.prototype.getDaysInMonth = function() {
    this.daysInMonth = [31,28,31,30,31,30,31,31,30,31,30,31][this.getMonth()]
    // Feb on leap year
    if (this.isLeap() && this.getMonth() == 1)  {
        this.daysInMonth += 1;
    }
    return this.daysInMonth;
}

//var articles;

var ikiwiki_cal = {
    init: function(json_data) {
    this.days = ["Monday" ,"Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    this.months = ["January","February","March","April","May","June","July","August","September","October","November","December"];

    //get URL:
    var url = window.location;
    this.month = this.year = null;
    // get arguments of the URL
    if (url.pathname.length > 1) {
        slashes = url.pathname.substr(1)
        if (slashes.charAt(slashes.length-1) == '/')
        slashes = slashes.substr(0,slashes.length-1);

        url_elem = slashes.split('/');
        if (url_elem.length > 1) {
        if (url_elem.indexOf("archives")) {
            month = url_elem[url_elem.indexOf("archives")+2];
            year = url_elem[url_elem.indexOf("archives")+1];
            if (!(isNaN(parseInt(month)) || isNaN(parseInt(year)))) {
            this.month = parseInt(month)-1;
            this.year = parseInt(year);
            }
        }
        }
        }

    if (this.month == null || this.year == null) {
        var today = new Date();
        this.month = today.getMonth();
        this.year = today.getFullYear();
    }

    this.element = document.getElementById('calendar');

    this.articles = json_data;

    },
    change_date: function(month, year) {
    this.month = month;
    this.year = year;
    this.render();
    },
    prev_month: function() {
    if (this.month == 0) {
        this.month = 11;
        this.year--;
    } else
        this.month--;

    this.render();
    },
    next_month: function() {
    if (this.month == 11) {
        this.month = 0;
        this.year++;
    } else
        this.month++;

    this.render();
    },
    articles_from_month: function(json_articles) {
    var lst = [];
    for(var i = 0; i < json_articles.length; i++) {
        var dt = new Date(Date.parse(json_articles[i].date))
        if (dt.getFullYear() == this.year && dt.getMonth() == this.month) {
        lst.push(json_articles[i]);
        }
        }

    return lst;

    },

    render: function() {
    var html = ''
    var articles = this.articles_from_month(this.articles);

    // what is the previous month ?
    var m = (this.month+1).toString();
    if (m.length == 1)
        m = "0"+m;
    if (this.month == 0) {
        pm = 12;
        py = this.year-1;
    } else
        pm = this.month

    // What's the next month ?
    if (this.month == 11) {
        nm = 1;
        ny = this.year+1;
    } else
        nm = this.month+2

    if (pm.toString().length == 1)
        pm = "0"+pm.toString();

    if (nm.toString().length == 1)
        nm = "0"+nm.toString();

    // header
    html = '<table class="month-calendar">\n'
    html += '<tbody><tr>\n'
    html += '<th class="month-calendar-arrow">'
    html += '<a href="#" onclick="ikiwiki_cal.prev_month(); return false;" title="' + this.months[parseInt(pm)-1] +'">←'
    html += '</a></th>'
    html += '<th class="month-calendar-head" colspan="5">'
    html += '<a href="/blog/archives/' + this.year + '/' + m + '/" title="' + this.months[this.month]+'">'+this.months[this.month].substring(0,3)+' '+this.year+'</a></th>'
    html += '<th class="month-calendar-arrow">'
    html += '<a href="#" onclick="ikiwiki_cal.next_month(); return false;" title="' + this.months[parseInt(nm)-1] +'">→'
    html += '</a></th>'
    html += '</tr>'

    // days header:
    html += '<tr>'
    for (var i=0; i < this.days.length; i++) {
        html += '<th class="month-calendar-day-head '+this.days[i]+'" title="'+this.days[i]+'">'+this.days[i].substring(0,1)+'</th>\n'
    }
        html +='</tr><tr>'

    // days body:
    var first_day_date = new Date(this.year, this.month, 1, 0, 0, 0);
    var first_day_num = first_day_date.getDay() == 0 ? 6 : first_day_date.getDay()-1;
    var last_day = first_day_date.getDaysInMonth();
    var last_day_date = new Date(this.year, this.month, last_day, 0, 0, 0, 0);
    var last_day_num = last_day_date.getDay() == 0 ? 6 : last_day_date.getDay()-1;

    for (var i = 0; i < (first_day_num+last_day+(6-last_day_num)); i++) {
        if (i > 1 && i % 7 == 0 && i < (first_day_num + last_day)) {

        html += '</tr><tr>';
        }
        if (i<first_day_num || i>(first_day_num + last_day -1)) {
        html += '<td class="month-calendar-day-noday '+ this.days[i%7] + '">&nbsp;</td>';
        }
        else {
        var article = null;
        for(var j = 0; j < articles.length; j++) {
            var art = articles[j];
            var dt = new Date(Date.parse(art.date))

            if (dt.getDate() == (i-first_day_num+1)) {
            article = art;
            }
        }
        if (article != null)
            html += '<td class="month-calendar-day-link '+ this.days[i%7] + '"><a href="'+article.url+'" title="'+article.title+'">'+(i-first_day_num+1).toString()+'</a></td>';
        else
            html += '<td class="month-calendar-day-nolink '+ this.days[i%7] + '">'+(i-first_day_num+1).toString()+'</td>';


        }

    }
    html +='</tr>';


    // End:

    html += '</tbody></table>\n'

    this.element.innerHTML = html;
    }
}

// JSON Loading function:
function json_request(callback) {
    var req = new XMLHttpRequest();

    req.onreadystatechange = function() {
        if (req.readyState == 4 && (req.status == 200 || req.status == 0)) {
        callback(JSON.parse(req.responseText));
        }
    };

    req.open("GET", "/calendar.json", true);
    req.send();
}

// Callback function to deal with Ajax response:
function json_do(jsondata) {
    //articles = jsondata;
    ikiwiki_cal.init(jsondata);
    ikiwiki_cal.render();
}

// Function to go to a precise month
function go_to_month(month, year) {
    ikiwiki_cal.change_date(month, year);
}

// Main code
window.addEventListener('load', function() {

    // Verify if we have a calendar:
    var calendar = document.getElementById("calendar");
    if (calendar != null) {
    // Just call the JSON request and create our Calendar
    json_request(json_do);
    }
});

How to deploy it ?

First, you have a Javascript file named calendar.js. Put it at the root of your repository. Ikiwiki just compile it by doing nothing. So it will be available at the root of your website.

The second step is to change some templates. The first one is the page template. Because of HTML scrutinization from Ikiwiki, you can't add a '''''' inside a page. Ikiwiki just erase it. The only way I've found to put this balise was to edit the page template. Just create a "templates" directory at the root of your repository and copy the file /usr/share/ikiwiki/templates/page.tmpl inside. Edit the file: I just add a script balise to charge calendar.js in the header only if it is in the blog.

Then you have to modify the archives templates. Just copy /usr/share/ikiwiki/templates/calendarmonth.tmpl in your repository templates directory. I've just modified the template to show a better sidebar for archive pages. Remember that the Javascript code is able to show the good calendar dynamically. I've faced a problem: whatever change you make in the calendar templates, they are never propagated to the corresponding .mdwn files under your archives directory. The solution is quite radical: delete your archives directory and then launch ikiwiki-calendar to regenerate it.

Finally, you have the JSON file generation. I just modified a little bit the git hooks from ikiwiki. I've written a simple Shell script to grep into mdwn files and get the meta directives for title and date of the articles in the blog. Once the code is able to generate JSON content, the git part comes to place.

For those who don't know, there is a special git hook in the bare repository of your ikiwiki instance. It is generated by Ikiwiki when you launch a rebuild of your site. The name of the hook is the content of the git_wrapper variable of your ikiwiki setup file. The content of this hook is a C compiled binary which just update the source directory of your ikiwiki instance and launch a refresh of the site. I just modified the name of the hook (post-update by default) to post-update.ikiwiki and replace post-update file with the content of my JSON generation shell script. At the beginning of this script, it just launches post-update.ikiwiki. When the bare repository is refreshed (pushed in), the script generates calendar.json in the web server directory. So calendar.json is not in any ikiwiki repository, just in the web server served directory.

Here is the code of this hook:

#!/bin/bash

# Script to generate calendar.json file

# make the update by ikiwiki
/path/to/git/bare/repository/post-update.ikiwiki


# global variables
echo "Compiling calendar.json..."
srcdir="/your_ikiwiki_src_repository"
destfile="/var/www/calendar.json"
blogrep="$srcdir/blog"

# Get all the mdwn in blog directory
lst_files=$(find $blogrep -type f \( -iname '*.mdwn' ! -regex '.*/archives/.*' \))

echo "[" > $destfile

# For each mdwn, get:
for file in $lst_files
do
    meta=$(grep '\[\[!meta' $file)
    url=${file#$srcdir}
    url=${url%.mdwn}
    title=""
    echo $meta | grep -q "title"
    if [ "$?" -eq "0" ]
    then
    title=$(echo $meta | sed 's/.*title="\([^"]*\)".*/\1/')
    fi

    date=$(echo $meta | sed 's/.*date="\([^"]*\)".*/\1/')
    date=${date:0:10}

    if [ "${#title}" -ne "0" ]
    then
    cat >> $destfile <<EOF
  { "url" : "$url",
    "title" : "$title",
    "date" : "$date" },
EOF

    fi


done

cat >> $destfile <<EOF
  { "url" : "/",
    "title" : "Start",
    "date" : "1977-01-01" }
]
EOF



exit 0

Conclusion

As a conclusion, this workaround has been very hard to elaborate. I just have to understand very well the internal mechanism of Ikiwiki (for the hook part). Then I just have to learn Javascript a little bit more than what I knew before. But I am proud to find a way to circumvent this real problem in less than a day of work !