Wednesday 14th of September 2022

This blog was originally set up using Ghost 0.3.2, and then pretty much never updated. There have been many, many updates to Ghost since that 2015 release, which I’ve basically ignored as I wasn’t using the managed installation on their servers, and had made several modifications to the default Casper theme, and honestly couldn’t be bothered to deal with the hassle of upgrading.

Now, Ghost was pretty cool, being a fairly basic blog engine with a nice editor, good-looking default theme and decent performance. It’s since grown way beyond that, turning into some kind of Wordpress-like do-everything behemoth, and I just have no interest in something like that.

Enter Rust: I’d been curious about the language for a while, but without a project to work on and really get stuck in learning it, I didn’t get anywhere. Then I got to thinking about Ghost, and how it’d always bothered me that there was no native comments feature (there may be by now, I haven’t checked), and so the idea of writing my own blog engine in Rust came about. Hence Velum, which this blog is now running on!

Luckily, Ghost has an Export feature, allowing me to extract everything in its database (posts, tags, author info etc.) and dump it into a JSON file. I wrote a short JavaScript program (see below) to read this JSON and convert it into a bunch of Markdown files that my blog software can read, and lo, the new software has all the same content as Ghost did.

The idea was inspired by Jekyll, which almost does what I want, but it also doesn’t do comments, and that combined with my desire to have project I can learn Rust with led to the creation of Velum. It is of course reinventing the wheel to some extent, but what can I say, I’m a software developer.

Velum isn’t finished yet, but it’s finished enough that I feel comfortable using it for my live blog which you’re reading now. Most of the rest of the work is not public-facing – it’s stuff for the blog admin like article and image uploading, article management, and possibly an editor. Plus I’ll no doubt end up tweaking the CSS endlessly.

One thing I’m really proud of is Velum’s performance: it is, as the meme goes, blazingly fast. Rendering articles takes basically zero time, and rendering index pages takes low single-digit milliseconds even on the ancient Intel Atom N2800 CPU of the server hosting the blog. Part of this is just the nature of Rust programs, but a lot of it is because the articles are all stored in memory, so there’s no delays making database connections and the like. Given the content is almost entirely text, the memory load is pretty minimal – the full text of all 180 articles on this blog barely even adds up to 1MB.

It’s been an enlightening and occasionally frustrating experience, learning Rust but moreso learning the Warp web framework. Both are very different to what I’m used to with JavaScript and Ruby on Rails, and the initial learning curve for them is very steep. It’s been worth it though, as I really like Rust. It’s more work to get stuff done, but it instills an amazing sense of confidence once you do so. It feels like building a machine out of finely-crafted and well-oiled steel components, as opposed Ruby which feels a bit like building things with Lego.

Once I’ve got Velum to a point where I no longer feel the need to constantly add to and tweak it, I want to continue my Rust journey. I currently have a couple of ideas: first, a game using Bevy, and second some kind of GUI app that does… something, I haven’t decided yet.

I also really should do more of those backlog posts; there’s four years of photos to show!


Here’s the JavaScript program to extract article data from the Ghost JSON dump:

const fs = require('fs');
const json = JSON.parse(fs.readFileSync('blog-andyf-me.ghost.2022-06-15.json'));
const d = json.db[0].data;

const posts = d.posts.map(p => {
    const tags_for_post = d.posts_tags
        .filter(pt => pt.post_id === p.id)
        .map(pt => d.tags
            .filter(t => t.id === pt.tag_id)
            .map(t => t.name));
    return {
        title: p.title,
        tags: tags_for_post.map(tfp => tfp[0]),
        slug: p.slug,
        content: p.markdown,
        timestamp: p.created_at
    };
});

const filestrings = posts.map(p => {
    return [
        '# ' + p.title,
        '|' + p.tags.join(', ') + '|',
        '',
        p.content
    ].join('\n');
});

posts.forEach((p, i) => {
    const name = `content/articles/${p.slug}.md`;
    fs.open(name, 'w', (err, fd) => {
        if (err) return;
        const ts = p.timestamp / 1000;
        console.log(`Writing ${name}...`);
        fs.writeSync(fd, filestrings[i]);
        fs.futimesSync(fd, ts, ts);

        fs.close(fd);
    });
})