Sharing pre-compiled templates between server and client with Hogan.js

By Dave Elkan

At work we use Closure Templates to render HTML on the client. Closure Templates boast the lovely feature of being able to compile templates down to simple javascript functions on the server for use on the client. This is good because:

  • it relieves the browser of having to compile your templates
  • you don't have to clutter your markup with template source and
  • you can render on the client and server with exactly the same template without sending HTML down the wire.

The last point offers a significant SEO benefit: By combining server rendered markup for the first page a user visits and client-rendered pushState delivered markup thereafter, you satisfy both your own desire to have a dynamic web app and also Google's desire to read static HTML.

I was surprised there wasn't any immediately obvious equivalent to Closure Templates for node.js. I went as far as trying (in vain) to get ejs to export stringified functions. I quickly threw out that pursuit when I found Hogan.js.

Hogan.js, from Twitter, is an implementation of the mustache templating language for use in the browser and on the server with node.js.

An example of creating and using shared templates with Hogan.js

This example will render a simple article page using a mustache template which can be used by the server and the client. All of the snippets below are taken from a working Express app available on github.

Loading and compiling the shared templates

The following function loads templates to share between the server and client from a specified directory and compiles them to a stringified javascript function.

/**
 * Reads and compiles hogan templates from the shared template
 * directory to stringified javascript functions.
 */
function readSharedTemplates() {
    var sharedTemplateFiles = fs.readdirSync(sharedTemplateDirectory);

    // Here we'll stash away the shared templates compiled script (as a string) and the name of the template.
    app.sharedTemplates = [];

    // Hogan like it's partials as template contents rather than a path to the template file
    // so we'll stash each template in a partials object so they're available for use
    // in other templates.
    app.sharedPartials = {};

    // Iterate over each sharedTemplate file and compile it down to a javascript function which can be
    // used on the client
    sharedTemplateFiles.forEach(function(template, i) {
        var functionName = template.substr(0, template.lastIndexOf(".")),
            fileContents = removeByteOrderMark(fs.readFileSync(sharedTemplateDirectory + template, "utf8"));

        // Stash the partial reference.
        app.sharedPartials[functionName] = fileContents;
        // Stash the compiled template reference.
        app.sharedTemplates.push({
            id: functionName,
            script: hogan.compile(fileContents, {asString: true}),
            // Since mustache doesn't boast an 'isLast' function we need to do that here instead.
            last: i === sharedTemplateFiles.length - 1
        });
    });
}

Delivering the pre-rendered templates

The result of the hogan.compile function when you pass {asString: true} (highlighted above) will return a stringified javascript function which can be sent to the browser. The name of the template file is used as the id of the template.

The output of readSharedTemplates is sent to the following mustache template which renders them as a simple javascript object.

var templates = {

    "": new Hogan.Template(}),

};

Note the triple mustaches around the script variable. This prevents the javascript being HTML escaped.

The output of this template looks something like this:

var templates = {
    "article": new Hogan.Template(function(c,p,i){i = i || "";var b = i + "";var _ = this;b += "## ";b += (_.v(_.f("headline",c,p,0)));b += "";b += "\n" + i;b += "<p>";b += (_.v(_.f("bodyText",c,p,0)));b += "</p>";b += "\n";return b;;})
};

Handling the requests

The following request handler responds with the template javascript file. You don't have to do it this way. You could easily add the template javascript as an inline script tag in the HTML response.

/**
 * Request handler for pre-compiled hogan.js templates.
 *
 * This function uses a hogan template of it's own which renders
 * calls to Hogan.Tempate. See views/sharedTemplates.mustache.
 */
app.get("/templates.js", readSharedTemplatesMiddleware, function(req, res, next) {
    var content = sharedTemplateTemplate.render({
        templates: app.sharedTemplates
    });
    res.contentType("application/javascript");
    res.send(content);
});

For each request to this handler the readSharedTemplatesMiddleware function is call which reloads the shared templates in development mode.

The other request handler renders the HTML for the initial page. It uses the contents of the shared article.mustache file as a partial which is in turn loaded by layout.mustache (highlighted below).

/**
 * Request handler for the homepage.
 *
 * Renders a hogan template on the server side which contains a form
 * which will update the article section on the client using the
 * pre-compiled template.
 */
app.get("/", function(req, res, next) {
    res.render("layout.mustache", {
        context: {
            headline: "This is a server-side rendered headline",
            bodyText: "This is some bodytext"
        },
        partials: {
            "article": app.sharedPartials["article"]
        }
    });
});

Rendering the shared template in the browser

On the client we can refer to the pre-rendered template function by calling the templates.article function. i.e.

templates.article({
    headline: "This is a client-generated headline",
    bodyText: "This is a client-generated body"
});

will generate:

    <h1>This is a client-generated headline</h1>
    <p>This is a client-generated body</p>

Running the example

To illustrate the use of a pre-compiled shared template on the client I've put a form in the output of layout.mustache which will update the server-rendered article using the client-side pre-compiled template.

Give it a run for yourself!

$ git clone git://github.com/dave-elkan/pre-compiled-hogan-templates.git
$ cd pre-compiled-hogan-templates
$ npm install -d
$ node app.js
comments powered by Disqus