The burning question: why build a static blog? After attending the Sydney AWS Bootcamp last month, I’ve been itching to host something on S3. I’ve also been on a Node.js kick lately, so building a static blog seemed like a fun first project. Enter Assemble, a Node.js alternative to the popular static publishing tool, Jekyll.

This article is a complete walkthrough for building a basic static blog with Assemble — installation, layouts, partials, custom helpers, static assets, Bootstrap styling, and Disqus comments.

Note: The code for this tutorial is hosted on GitHub at https://github.com/webercoder/assembleio-blog. There are Git checkpoints throughout the article that link to a tag in the example repo, so you can follow along at any point.

The General Idea

S3 hosting, Route 53, and CloudFront make for an incredibly cheap and robust combination — hard to beat for static files.

So how do you build a blog — usually a dynamic, database-driven endeavor — as a static site? It’s simple, really: configure Grunt.js to filter Markdown files through Handlebars templates. All the heavy lifting is done by Assemble’s Grunt package. It’s easy to set up; I was up and running in less than four hours of real coding time.

Grunt

To get started, you need to set up Grunt. If you’re already familiar with Grunt, skip ahead.

Grunt is a Node.js task manager. It handles things like minification, CSS preprocessor compilation, concatenation, linting, running unit tests, S3 deployment, and much more.

To install Grunt, first install Node.js from the official Node website. Then open a command prompt and run the following:

% npm install --global grunt-cli

grunt-cli is an npm package that runs the locally configured and installed Grunt for your project. Grunt itself is installed in each Grunt-enabled project. Let’s do that now:

% mkdir blog
% cd blog
% npm init
# ... just choose all the defaults and a package.json file will be created.
% npm install --save-dev grunt

As mentioned in the comment above, npm init creates a file called package.json. This file records the npm packages required by your project. Commit this file to your code repository.

Next, configure Grunt by creating a Gruntfile.js.

module.exports = function(grunt) {

	"use strict";

	grunt.initConfig({

		// Tasks here
		assemble: {},

	});

	// Load plugins for the above tasks
	grunt.loadNpmTasks('assemble');

	// The default task or other custom tasks
	grunt.registerTask("default", ["assemble"]);

};

The highlights:

  • initConfig configures individual Grunt tasks. The empty assemble block gets us started with Assemble’s Grunt package.
  • grunt.loadNpmTasks loads Assemble.
  • registerTask groups tasks together under a label. These labels can be called on the command line with the grunt program. The “default” task can be invoked with grunt or grunt default.

Git Checkpoint: The code above is available on the tag example-1 in the example Git repository.

Assemble

The Gruntfile references “assemble” in a loadNpmTasks call, so we’ll need to install that.

% npm install --save-dev assemble

Now it’s time to create a basic assembly. First, create the following directory structure:

blog/
├── Gruntfile.js
├── package.json
├── build/
└── src/
    ├── content/
    └── layouts/

Open Gruntfile.js and fill in that empty assemble task:

Gruntfile.js

module.exports = function(grunt) {

	"use strict";

	grunt.initConfig({

		// Tasks here
		assemble: {
			options: {
				layout: 'default.hbs',
				layoutdir: './src/layouts/'
			},
			blog: {
				files: [{
					cwd: './src/content',
					dest: './build/',
					expand: true,
					src: ['**/*.md']
				}]
			}
		},

	});

	// Load plugins for the above tasks
	grunt.loadNpmTasks('assemble');

	// The default task or other custom tasks
	grunt.registerTask("default", ["assemble"]);

};

The options in the assemble block define our layout directory and the default layout. Assemble will pass our content pages through the layout of our choosing. In this case, we’re using the default layout, default.hbs.

Next, create the default layout we referenced in the Gruntfile.

src/layouts/default.hbs

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>{{title}} - My Blog</title>
		<meta charset="utf-8">
		<meta name="viewport" content="initial-scale=1.0">
	</head>
	<body>
		{{#markdown}}
			{{>body}}
		{{/markdown}}
	</body>
</html>

This layout is a Handlebars template. In addition to being a basic HTML page, it uses a few Handlebars expressions: {{title}}, {{#markdown}}...{{/markdown}}, and {{>body}}.

  • {{title}} uses the title defined in the Markdown file below.
  • The content between {{#markdown}} and {{/markdown}} will be interpreted as Markdown and converted to HTML.
  • {{>body}} will be replaced with the body of the Markdown file.

Now let’s create an index page using Markdown:

src/content/index.md

---
title: Home
---

This is the home page of my blog. I hope you enjoy it!

The top of this file is known as YAML Front Matter (YFM). Jekyll, another static publishing engine, also uses YFM in its templates. YFM is usually a key: value list, and each of those keys is exposed in our assembly as a Handlebars expression (for example, {{title}}).

The section below the YFM is the body of the page. Assemble will replace {{>body}} in the default layout with this content.

Let’s run this thing.

% grunt
# ... or...
% grunt assemble

This will create a new file in the build directory called index.html. It should look like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Home - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>
<p>This is the home page of my blog. I hope you enjoy it!</p>
    </body>
</html>

And with that, our first assembly is complete.

Git Checkpoint: The code above is available on the tag example-2 in the example Git repository.

Blog Posts

The next step is to create a few blog entries — handy things to have in a blog.

First, make a directory to hold the posts:

% mkdir src/content/posts

Let’s add two for now.

src/content/posts/my-first-day-as-a-wallaby.md

---
title: My First Day as a Wallaby
created: 2015/05/01 5:45 pm
---

On my first day as a Wallaby, I ran around the bush for a while and ate shrubs.
It was a pleasurable experience.

src/content/posts/my-second-day-as-a-wallaby.md

---
title: My Second Day as a Wallaby
created: 2015/05/01 5:45 pm
updated: 2015/05/01 7:53 pm
---

Today was entirely different from yesterday, having narrowly escaped a crocodile
attack. It was a harrowing experience.

src/layouts/default.hbs

We also need to expand the default template to account for the created and updated dates, and to add a heading.

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>{{title}} - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>

{{#markdown}}

# {{title}}

{{#if created}}
*Posted: {{created}}
{{#if updated}}
<br>Updated: {{updated}}
{{/if}}*
{{/if}}

{{>body}}

{{/markdown}}

    </body>
</html>

With two entries, assemble them once again by running grunt. If successful, the build directory will look like this:

build/
├── index.html
└── posts/
    ├── my-first-day-as-a-wallaby.html
    └── my-second-day-as-a-wallaby.html

Your second day as a Wallaby looks nice:

build/my-second-day-as-a-wallaby.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>My Second Day as a Wallaby - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>

<h1 id="my-second-day-as-a-wallaby">My Second Day as a Wallaby</h1>
<p><em>Posted: 2015/05/01 5:45 pm
<br>Updated: 2015/05/01 7:53 pm
</em></p>
<p>Today was entirely different from yesterday, having narrowly escaped a crocodile
attack. It was a harrowing experience.</p>

    </body>
</html>

Our index page is still the same, though. Without a posts list, you’ll have very little traffic!

Git Checkpoint: The code above is available on the tag example-3 in the example Git repository.

Post List

To fix that, we’ll generate a list of posts using Assemble’s collections feature. Collections collate an array of pages for use when generating lists. The pages collection is available by default and includes all the pages in our current assembly.

In the example below, Assemble will iterate over the pages collection and output a list item for each page. Pages that define exclude: true in YFM will be skipped because I’ve added {{#unless data.exclude}} to the template.

The YFM properties for each item in the collection are attached to a data object instead of the root scope like before. This avoids YFM collisions that would otherwise occur if both the looped page and the current index page defined the same YFM key. For example, without data.title, Handlebars would have a hard time evaluating {{title}} since title would be ambiguous.

src/content/index.md

---
title: "Home"
exclude: true
---

{{/markdown}}

<ul>
{{#withSort pages "data.created" dir="desc"}}
	{{#unless data.exclude}}
		<li>
			<a href="posts/{{basename}}.html">{{data.title}}</a>
			({{data.created}})
		</li>
	{{/unless}}
{{/withSort}}
</ul>

{{#markdown}}

Note that I’ve also disabled Markdown rendering in this page. It would be much better to render this page with a separate template — we’ll do exactly that shortly.

After creating the new index file, run grunt. Your new list should look like this:

build/index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Home - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>

<h1 id="home">Home</h1>

<ul>
		<li>
			<a href="posts/my-second-day-as-a-wallaby.html">My Second Day as a Wallaby</a>
			(2015/05/02 6:05 pm)
		</li>
		<li>
			<a href="posts/my-first-day-as-a-wallaby.html">My First Day as a Wallaby</a>
			(2015/05/01 5:45 pm)
		</li>
</ul>


    </body>
</html>

And with that, we have a very basic blog.

Git Checkpoint: The code above is available on the tag example-4 in the example Git repository.

Partials

Our layout above includes everything from the doctype to the closing body tag. That approach is unmaintainable — it breaks DRY. Every site has common components, and with Assemble it’s best to break these out into partials.

There are several issues with the current template:

  • It assumes all content will be written in Markdown.
  • It assumes we want the title to appear at the top of the entry as an <h1>.
  • If we want to add more layouts later, we’ll have to duplicate the header and footer.

A better approach is to split this layout into header and footer partials, plus two layout files: one for Markdown blog entries and one for vanilla Handlebars pages.

First, change to the root directory of the blog and create a directory for partials:

% mkdir src/layouts/partials

Now create two new partials:

src/layouts/partials/header.hbs

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>{{title}} - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>

src/layouts/partials/footer.hbs

</body>
</html>

Now modify the default template to include those partials:

src/layouts/default.hbs

{{>header}}

{{#markdown}}

# {{title}}

{{#if created}}
*Posted: {{created}}
{{#if updated}}
<br>Updated: {{updated}}
{{/if}}*
{{/if}}

{{>body}}

{{/markdown}}

{{>footer}}

Finally, tell Grunt where to find the partials:

Gruntfile.js

// ...
assemble: {
    options: {
        layout: 'default.hbs',
        layoutdir: './src/layouts/',
        partials: './src/layouts/partials/**/*.hbs'
    },
    blog: {
        files: [{
            cwd: './src/content',
            dest: './build/',
            expand: true,
            src: ['**/*.md']
        }]
    }
}
// ...

Now run grunt to confirm everything works. The blog should look exactly as it did before, but the code is much easier to maintain.

Git Checkpoint: The code above is available on the tag example-5 in the example Git repository.

Adding a Layout

Now that the header and footer have their own partials, let’s split our layout into one for normal pages and another for blog entries. In a flat-file blog like this one, we’d expect to have many more blog entries than other pages, so we’ll continue using the default layout for blog entries. Let’s create a new layout for other types of pages:

src/layouts/page.hbs

{{>header}}
{{>body}}
{{>footer}}

I’ve removed the created and updated block, the title, and the Markdown triggers, allowing for maximum customization on each page.

Now modify the home page:

src/content/index.md

---
title: "Welcome to My Blog"
layout: page.hbs
exclude: true
---

<h1>{{title}}</h1>

<ul>
{{#withSort pages "data.created" dir="desc"}}
	{{#unless data.exclude}}
		<li>
			<a href="posts/{{basename}}.html">{{data.title}}</a>
			({{data.created}})
		</li>
	{{/unless}}
{{/withSort}}
</ul>

I’ve also added {{title}} and changed the title itself. The original index page disabled Markdown by closing and then re-opening it with Handlebars expressions (a super hack), but our new template doesn’t have to do that, so those Markdown expressions can be removed.

Finally, rename src/content/index.md to src/content/index.hbs since the home page is no longer processed as Markdown.

Run grunt to test. The blog should look the same aside from the modified home page title.

Git Checkpoint: The code above is available on the tag example-6 in the example Git repository.

Improving the Home Page

Most blogs have other pages such as a landing page or an “about” page. Using the new layout, we can create those fairly easily.

For the home page, let’s show the latest blog entry with a summary and a Read More link.

First, copy the content from src/content/index.hbs into src/content/blog.hbs and change the title to “Blog”:

src/content/blog.hbs

---
title: "Blog"
layout: page.hbs
exclude: true
---

<h1>{{title}}</h1>

<ul>
{{#withSort pages "data.created" dir="desc"}}
	{{#unless data.exclude}}
		<li>
			<a href="posts/{{basename}}.html">{{data.title}}</a>
			({{data.created}})
		</li>
	{{/unless}}
{{/withSort}}
</ul>

src/content/blog.hbs will now serve as the entries list page instead of the home page.

Next, add a brief summary to each blog entry in the YAML Front Matter.

src/content/posts/my-first-day-as-a-wallaby.md

---
title: My First Day as a Wallaby
created: 2015/05/01 5:45 pm
summary: My first day as a Wallaby was quite an experience.
---

On my first day as a Wallaby, I ran around the bush for a while and ate shrubs.
It was a pleasurable experience.

src/content/posts/my-second-day-as-a-wallaby.md

---
title: My Second Day as a Wallaby
created: 2015/05/02 6:05 pm
updated: 2015/05/03 7:53 pm
summary: |
  During my second day, I hopped quickly, exploring the land. I encountered many
  dangerous reptiles during my journey.
---

Today was entirely different from yesterday, having narrowly escaped a crocodile
attack. It was a harrowing experience.

Note how the summary in the second entry is formatted. To create multi-line YFM entries, use a pipe symbol and indented content.

Now that each post has a summary, change the index page to show the latest entry.

src/content/index.hbs

---
title: "Welcome to My Blog"
layout: page.hbs
exclude: true
---

<h1>{{title}}</h1>

<h2>Latest Entry</h2>
<article>
	{{#getLatestEntries pages 1}}
		<h3><a href="posts/{{basename}}.html">{{data.title}}</a></h3>
		<p>
			<em>{{data.created}}</em>
		</p>
		<p>
			{{data.summary}}
			<a href="posts/{{basename}}.html">Read More</a>
		</p>
	{{/getLatestEntries}}
</article>

<p><a href="blog.html">View All Posts</a></p>

The Handlebars expression {{#getLatestEntries}} is a custom block helper. It accepts a collection and a number n as parameters, sorts the collection by date, executes the enclosed markup n times, and returns the result.

To include this helper in your blog, first configure Grunt to load helpers:

Gruntfile.js

// ...
assemble: {
    options: {
        layout: 'default.hbs',
        layoutdir: './src/layouts/',
        partials: './src/layouts/partials/**/*.hbs',
        helpers: './src/helpers/**/*.js'
    },
    blog: {
        files: [{
            cwd: './src/content',
            dest: './build/',
            expand: true,
            src: ['**/*.md', '**/*.hbs']
        }]
    }
}
// ...

I’ve added the helpers: line above.

Next, create the helper file src/helpers/helpers.js and add the helper:

src/helpers/helpers.js

var moment = require("moment");

module.exports.register = function(Handlebars, options) {
	'use strict';

	/**
	 * Get the first enabled items of a list.
	 * @param items An array of items.
	 * @return A subset of items.
	 */
	Handlebars.registerHelper('getLatestEntries', function(items, limit, options) {

		var rv = "";
		var count = 0;

		// Sort items by date
		var sortedItems = items.sort(function(a, b) {
			var momentA = moment(a.created);
			var momentB = moment(b.created);
			return momentB.diff(momentA);
		});

		if (!limit) {
			limit = 10;
		}

		// Pull out the most recent
		for (var i = 0; i < sortedItems.length && count++ < limit; i++) {
			if ("exclude" in sortedItems[i] && sortedItems[i].exclude === true) {
				continue;
			}
			rv += options.fn(sortedItems[i]);
			count++;
		}

		return rv;

	});

};

Two things worth noting. First, Handlebars passes options.fn to helpers. It takes an argument and runs it through the template you provide. In the example above, I’m looping through each item in a list and rendering one block of markup per item. The output of options.fn is a rendered string, so I combine all the outputs into one string and return that from my helper.

Second, sorting dates is tricky business when unusual date formats are involved. I decided to use Moment.js for that — it makes sorting a breeze with the Moment.diff function. We’ll need to install the Moment.js npm module:

% npm install --save-dev moment

With all that saved, run grunt. Your old index page should be called blog.html and the new index.html should look like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Welcome to My Blog - My Blog</title>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1.0">
    </head>
    <body>


<h1>Welcome to My Blog</h1>

<h2>Latest Entry</h2>
<article>
		<h3><a href="posts/my-second-day-as-a-wallaby.html">My Second Day as a Wallaby</a></h3>
		<p>
			<em>2015/05/02 6:05 pm</em>
		</p>
		<p>
			During my second day, I hopped quickly, exploring the land. I encountered many
dangerous reptiles during my journey.

			<a href="posts/my-second-day-as-a-wallaby.html">Read More</a>
		</p>
</article>

<p><a href="blog.html">View All Posts</a></p>

    </body>
</html>

Git Checkpoint: The code above is available on the tag example-7 in the example Git repository.

Static Assets

All blogs have static assets such as images, scripts, and stylesheets that need to be deployed alongside the blog engine. Grunt can be configured to handle and deploy these assets.

Let’s start by adding a favicon to the site. I’ve picked this single-color Rubik’s cube icon for now. First, create the static resources directory and add the favicon to it:

% mkdir src/static
% mv ~/location/of/favicon.ico src/static

Let’s configure Grunt to copy resources from our static folder to the build folder. We’ll use grunt-contrib-copy for this task. First, install the package:

% npm install --save-dev grunt-contrib-copy

Open Gruntfile.js and add the copy configuration:

module.exports = function(grunt) {

    'use strict';

    grunt.initConfig({

        // Tasks here
        assemble: {
            options: {
                layout: 'default.hbs',
                layoutdir: './src/layouts/',
                partials: './src/layouts/partials/**/*.hbs',
                helpers: './src/helpers/**/*.js'
            },
            blog: {
                files: [{
                    cwd: './src/content',
                    dest: './build/',
                    expand: true,
                    src: ['**/*.md', '**/*.hbs']
                }]
            }
        },

        copy: {
          'static-assets': {
            files: [{
              expand: true,
              src: [
                './**/*'
              ],
              dest: 'build/',
              filter: 'isFile',
              cwd: 'src/static/'
            }]
          }
        }

    });

    // Load plugins for the above tasks
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-copy');

    // The default task or other custom tasks
    grunt.registerTask('default', ['assemble', 'copy']);

};

I’ve added the copy block, grunt.loadNpmTasks('grunt-contrib-copy'), and registered copy to run as part of the default grunt.registerTask call. The copy block will copy everything in src/static into build/.

Now run grunt and load the site in your browser (/build/index.html). You should see the favicon in your address bar or near the page title.

Note: The favicon will not appear if you’re browsing the site from a subdirectory. Browsers automatically load favicons from the website’s root directory, and since favicon.ico is not in the root above but in /build, the browser won’t retrieve it. You can get creative with Assemble’s relative linking and add a <link> element to your header, or load the blog from the root of your web server.

Git Checkpoint: The code above is available on the tag example-8 in the example Git repository.

Basic Styling

The visual design of the example blog is as barebones as it gets. With Bootstrap, we can give the site a clean new look in minutes rather than days.

Note: Bootstrap uses a 12-column grid to simplify creating layouts. I won’t be covering this in detail; if you’d like to know more, check out the official Bootstrap documentation.

First, download Bootstrap from Bootstrap’s website and place bootstrap.min.css into src/static/css.

Next, change src/layouts/partials/header.hbs to look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>{{title}} - My Blog</title>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1.0">
    <link href="{{relative page.dest 'build/css/bootstrap.min.css'}}" rel="stylesheet">
  </head>
  <body>
    <nav class="navbar navbar-inverse">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="{{relative page.dest 'build/index.html'}}">My Blog</a>
        </div>
      </div>
    </nav>
    <div class="container">

When linking to bootstrap.min.css, I used the {{relative page.dest ...}} Handlebars tag to create a relative link from the current page location. This keeps our code portable so it can be run from any directory.

Additionally, the new tags inside the body define a Bootstrap navbar and a container. The navbar is very basic, containing only a single link to the home page. <div class="container"> is required by Bootstrap for styling purposes and is closed off in src/layouts/partials/footer.hbs:

</div> <!-- container -->
</body>
</html>

Next, add a row and column to the page template, src/layouts/page.hbs. This adds our content to Bootstrap’s grid, allowing it to flow the way Bootstrap’s authors designed.

{{>header}}
<div class="row">
	<div class="col-sm-12">
		{{>body}}
	</div>
</div>
{{>footer}}

You can later add other columns inside the row as long as the col-*-n classes add up to 12. For example, we could do a two-column layout using the following code:

<div class="row">
	<div class="col-sm-4">
		...
	</div>
	<div class="col-sm-8">
		...
	</div>
</div>

In the snippet above, the second column will be 2x the width of the first.

With all that saved, run grunt and browse to the build directory like before. It looks much nicer! If you’d like to know more about Bootstrap, start with their Getting Started guide.

Git Checkpoint: The code above is available on the tag example-9 in the example Git repository.

Disqus Comments

Since we’re using a static publishing engine, writing our own database-powered commenting system defeats the purpose. You could do something fancy like creating a JAWS Stack with AWS services or baking your own server-side solution, or you can use Disqus. We’ll go with the latter for simplicity.

First, sign up for a Disqus account at disqus.com. At the time of writing, you can register a new site with Disqus by clicking the top-right gear to expose a menu, then clicking “Add Disqus to Site.” Fill in the required information and click submit. Finally, grab the universal code and add it to src/layouts/partials/footer.hbs inside a new Bootstrap row and column:

  {{#unless data.exclude}}
    <div class="row">
      <div class="col-sm-12">

        <div id="disqus_thread"></div>
        <script type="text/javascript">
            /* * * CONFIGURATION VARIABLES * * */
            var disqus_shortname = 'The shortname that you picked';

            /* * * DON'T EDIT BELOW THIS LINE * * */
            (function() {
                var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
                dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
                (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
            })();
        </script>
        <noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>

      </div>
    </div>
  {{/unless}}

</div> <!-- container -->
</body>
</html>

After running grunt once again, visit the site and view a blog entry. Comments should be live!

Git Checkpoint: The code above is available on the tag example-10 in the example Git repository.

Conclusion

This walkthrough covered building a complete static blog with Assemble.io — from installing Grunt through partials, a custom Handlebars helper, static assets, Bootstrap styling, and Disqus comments. The full source is on GitHub at webercoder/assembleio-blog with tags for each checkpoint along the way.

Thanks for reading!