Software Imagineer's blog

Side project: uSite — tiny static site generator

Tomas Grubliauskas | Mon Apr 03 2017

Last time I wrote about my struggles with unfinished side projects and what I am going to do about it. Today its time to cover first fruits of success following the new action plan.

“uSite” started with one of the best type of use-cases a project can have — a personal pain. I was looking for a static site generator for my personal blog www.agilesaturday.com, but after going through some of the popular engines on www.staticgen.com I decided to create a new one. My biggest frustration with existing static site generators was the amount of documentation and conventions I needed to learn before using one (e.g. gohugo.io). I was looking more for a content transformation API. Instead of a tool which requires significant knowledge of concepts like Git, extensions, themes and data exchange types, but pretends that I still don’t know how to make a website.

I thought that “content transformation API” is a cool concept and so uSite was born. In essence uSite has two parts: a set of functions for transforming content and a recipe for a blog. The cool part about this separation is that you can use uSite like any other static site generator with its own stupid conventions and all. But if you know some basic JS programming, you can copy and tailor my recipe for your own blog needs or completely rewrite it from scratch. The recipe is only 70 lines of code and could be even smaller if I have not added RSS and Sitemap generation.

This is how www.agilesaturday.com is made:

var RSS = require('rss20');

// Read global settings
uSite.global = uSite.loadOptions('website.json');

// Collect and extract meta data from posts
var posts = uSite.loadContent('content/post/*', (entry) => {
    var file = entry.loadString();
    var fileParts = file.split('+++', 2);

    entry.meta = entry.parseOptions(fileParts[0]);
    entry.slug = entry.meta.slug || entry.generateSlug(entry.meta.title);

    var content = fileParts[1];
    var contentParts = content.split('<!-- excerpt -->')
    entry.content = entry.parseMarkdown(content);
    entry.excerpt = entry.parseMarkdown(contentParts[0]);

    entry.relativeUrl = 'post/' + entry.slug;
}).sort((a, b) => { return b.meta.date - a.meta.date; });

// Emit html page per post
posts.emit('template/single.njk', 'www/post/{slug}');

// Paginate posts. 10 posts per page
var postGroup = posts.group((post, index) => {
    return Math.floor(index / 10);
});

// Create html page for each page of posts
postGroup.emit('template/list.njk', 'www/posts/{groupKey}');

var firstPage = postGroup.filter((groupKey) => {
    return groupKey == 0;
});

// First 10 posts is landing page
firstPage.emit('template/list.njk', 'www/index.html');

uSite.copy('template/res', 'www');

// Generate RSS feed
firstPage.emit((groupContext) => {
    var feed = new RSS.Feed();
    feed.title = groupContext.global.title;
    feed.description = groupContext.global.description;
    feed.link = groupContext.global.url || '';
    feed.pubDate = (new Date()).toGMTString();

    groupContext.entries.forEach((entry) => {
        var item = new RSS.Item();
        item.title = entry.meta.title;
        item.description = entry.excerpt;
        item.link = feed.link + entry.relativeUrl;
        item.pubDate = (new Date(entry.meta.date)).toGMTString();
        feed.addItem(item);
    });

    return feed.getXML();
}, 'www/rss.xml');

// Generate sitemap
posts.group(() => 0).emit((groupContext) => {
    var sitemapContent = '';
    if (groupContext.global.url) {
        sitemapContent += groupContext.global.url + '\n';
    }
    Object.keys(postGroup.allGroups).forEach((k) => {
        sitemapContent += (groupContext.global.url || '') + 'posts/' + k + '\n';
    });
    groupContext.entries.forEach((entry) => {
        sitemapContent += (groupContext.global.url || '') + entry.relativeUrl + '\n';
    });
    return sitemapContent;
}, 'www/sitemap.txt');

And this is how I completed my first side project in 2017. Without complications, overthinking or geeking out on cool new language or paradigm. Just picking a clear problem, selecting the simplest tool and implementing the essential functionality.

uSite is currently tailored for www.agilesaturday.com needs and does not have a documentation. I am still exploring how content transformation API could look like and do not want to set API in stone just yet. There is also some blog features left unfinished like: media handling, single content pages and pagination. Moreover, I would really like to hear what other people are considering as a must have in a static site generator. So, please create an issue at https://github.com/v3nom/uSite with your comments.