This post introduces ‘Bramble MVC’, a prototype static site generator. It aims to be a little different from existing static site generators by having an API resembling a dynamic web server. Its API removes the need for excessive configuration and affords developers many of the same benefits that come from using a dynamic web server.

The post walks through the process of building a simple blog site using Bramble MVC.

Bramble MVC Screenshot

Benefits of Bramble MVC

A static site generator is a program that generates the HTML pages of a website in one go. This is contrary to most modern websites that generate a dynamic HTML response for every request. Static sites provide several key advantages over dynamic sites:

  • They are robust, there are no external dependencies such as databases.
  • They are fast, the pages are precompiled so servers need only serve static files.
  • They are easy and cheap to host, only a static file server is required.

In addition to these points, Bramble MVC additional advantages compared to many other static site generators:

  • There are no configuration files.
  • There is next to no learning curve.
  • Clean urls - pages can be accessed without files extensions e.g. ‘/shop/outdoor/green-tent’ instead of ‘/shop/outdoor/green-tent.html’.
  • All pages are generated from code rather than configuration. You are not forced to use files as a data source for content. Use a database or a web service if you like.
  • It is not just for blogs - you can structure your urls and pages in any way you like.
  • Features such as pagination are easy and don’t require plugins.
  • It uses Node.js which is fast and familiar to many web developers.

Building a Blog

To demonstrate how to use Bramble MVC I’m going to walk through making a blog site. I was on the fence about using a blog as an example. On one hand I’d like to stress that Bramble MVC can be used for more than just blogs, but on the other hand it is one of the most common use cases for static site generators so it makes a good comparison.

Reading Posts

Bramble MVC does not force you to store posts on the file system. I can read my posts from anywhere but again, for the sake of familiarity, I’m going to get them from a folder of markdown files. The contents of a single post, welcome-to-the-blog.md, is shown below:

---
title: Welcome to the Blog
author: isullivan
published: 2014-1-15
preview: Drumstick filet mignon biltong jerky swine landjaeger venison.
---
Bacon ipsum dolor sit amet flank porchetta fatback, beef ham prosciutto venison. Beef landjaeger boudin biltong pig andouille brisket ground round...

The file consists of some yaml metadata followed by the content of the post which is just meat filled gibberish (generated by Bacon Ipsum).

I’ll create a module called blog-source.js for reading these files. The code in this file is completely independent of Bramble MVC and simply provides a single method called getPosts that returns a list of posts. The file uses the marked and front-matter npm modules to parse markdown and yaml.

var fs = require('fs'),
    fm = require('front-matter'),
    path = require('path'),
    marked = require("marked"),
    postPath = path.join(__dirname, "../posts"),
    posts = [];
 
fs.readdirSync(postPath).forEach(function(fileName) {
    var postDetails = fm(fs.readFileSync(path.join(postPath, fileName), "utf8"));
    posts.push({
        title: postDetails.attributes.title,
        author: postDetails.attributes.author,
        preview: postDetails.attributes.preview,
        published:  Date.parse(postDetails.attributes.published),
        uniqueName: fileName.split('.')[0],
        content: marked(postDetails.body)
    });
});

posts.sort(function(a, b) {
    return b.published - a.published; 
});
 
exports.getPosts = function() {
    return posts;
};

Creating Views

The default templating engine is Nunjucks and is both concise and powerful. Lets define a view called postlist.html that uses Nunjucks to render a list of blog posts. Views are templates that can access properties on the objects that are passed to them. The following view expects an object with a single property called posts which is a list of blog posts:

{% extends "layout.html" %}

{% block content %}

<section id="post-list">

    {% for post in posts %}
    
    <article>
        <h2>{{ post.title }}</h2>
        <section class='post-info'>
            <p class='author'>{{ post.author }}</p>
            <p>{{ post.preview }}</p>
        </section>
    </article>
    
    {% endfor %}
	
</section>

{% endblock %}

The first line specifies the parent template is called layout.html. It contains HTML that is common across multiple pages for example, the head tag, styles and some container elements. This layout defines a placeholder called content which is where our view inserts itself. The view itself iterates over the posts property and renders each post’s details.

Creating Controllers

The last thing we need to implement is the controller, the glue that ties the model (our blog posts) to the view. Bramble MVC uses routes and handlers much like a dynamic server might. Here is build.js, the entry point for building and generating the site.

var bramble = require('bramble-mvc'),
    blogRepository = require('./lib/data/blog-repository'),
    ...;
	
bramble.get('/', function(req, res) {
    res.view("postlist", {
        posts: blogRepository.getPosts()
    });
});
 
bramble.build(viewPath, outputDirectory);

bramble.get is used to add a controller function that generates the page at the url ‘/’. The controller function just passes a list of all blog posts to the view called postlist.

Calling bramble.build will kick off the build process and will output generated HTML files to the specified directory.

After sprinkling some CSS, the site now looks like the image shown at the top of this post.

Multiple Pages

Currently the blog resembles twitter, but lets pretend the posts have interesting content amounting to more than 140 characters. The list of posts on the front page only show a preview of posts and should link pages showing the full post content. The posts should be accessed with a nice url e.g. ‘post/post-title’ therefore we need to add a new controller function in build.js

bramble.get('/post/:postName', function(req, res) {
    var post = blogRepository.getPosts().filter(function(p){
        return p.uniqueName == req.parameters.postName;
    })[0];
    res.view("post", post);
});

The same controller is used for all posts therefore the second part of the url :postName is a variable denoted by the :. The variable url parameters are accessible in the req.parameters object. The controller finds the correct post given the url and passes it to a view named post. This is a common pattern often found in server code.

Bramble MVC needs to know what files to create when build is called. The single page site worked because the page at the url ‘/’ is always generated. For example, there is a post called ‘welcome-to-the-blog’ therefore we want the url ‘/post/welcome-to-the-blog’ to be processed. We can manually build a list of urls to process but that is an example of excessive configuration that make many statically generated sites difficult to maintain. Bramble MVC views have access to a function called url that registers a url to be processed and returns the same url. All we need to do is make a slight modification to the view to generate the post pages:

<a href="{{ url('/post/' + post.uniqueName) }}"><h2>{{ post.title }}</h2></a>

I’ve wrapped the header in an anchor tag and set the href attribute to point to the post page. By passing the url through the url function, Bramble MVC knows the target url should be rendered. This turns broken links into build time errors! Running build.js will now generate a page for each post that is linked to.

Aside: I mentioned above that pages in the built site can be accessed by using a nice url without file extensions e.g. ‘post/weclome-to-the-blog’. This is because the url ‘post/weclome-to-the-blog’ will output a file called ‘post/welcome-to-the-blog/index.html’.

Paging

Paging can be very tricky in statically generated sites as many static site generators have a one-to-one mapping between source files and output files. Bramble MVC doesn’t impose this restriction and therefore paging is quite easy. The main controller needs to be changed to only return a single page of posts (before it was returning all posts). This can be achieved by adding an pageNumber url parameter:

var pageSize = 3;
bramble.get('/:pageNumber', {defaults:{pageNumber:"1"}}, function(req, res) {
    var page = parseInt(req.parameters.pageNumber),
        startIndex = (page-1) * pageSize,
        endIndex = startIndex + pageSize
    
    res.view("postlist", {
        posts: blogRepository.getPosts().slice(startIndex, endIndex),
        hasNextPage: endIndex < blogRepository.getPosts().length,
        nextPage: page + 1
    });
});

The controller now has an optional pageNumber variable and the route has been given a default value to make it optional. A different set of posts will now be returned depending on the url: ‘/’ returns the first three posts, ‘/2’ returns the second three posts etc. Again, Bramble MVC needs to know it should render the post pages and it infers this by using the url function in the postlist view:

{% if hasNextPage %}
<section id="paging">
    <a class="page-button" href="{{ url('/' + (nextPage)) }}">Next</a>
</section>
{% endif %}

This link will only be rendered to the page if hasNextPage is true and therefore Bramble MVC will only process the route if there is another page that needs rendered. This is again the same kind of pattern you would use to implement paging on a server. The home page now looks like:

Bramble MVC Paging

Now we have a working, paged blog site built with very little code!

Conclusion

The work is still a prototype; I haven’t developed any large sites using it. I would be extremely interested to hear any comments about Bramble MVC or even general idea.

Full source for a more in depth multi-user blog is available on github. The full source of Bramble MVC is also available and accepting feature and pull requests.