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

This is the first in an arc of entries that will explain how to create a basic static blog with Assemble, similar to the one you are reading now.

Note: The code in this tutorial is hosted on GitHub at https://github.com/webercoder/assembleio-blog. There are four checkpoints throughout this post that explain how to access the code up to that point. Feel free to follow along if you’d like, or make you own!

The General Idea

I’ll go over S3 hosting in my next post, but suffice it to say that Route 53, S3, and Cloudfront make for an incredibly cheap and robust hosting option. They simply can’t be beat for static files.

So how do you build a blog–usually a dynamic database-driven endeavor–as a static site? Simple really: configure Grunt.js to filter Markdown files through Handlebars templates. All the heavy lifting is carried out by Assemble’s grunt package. It’s all very easy to setup; I was up and running in less than four hours of real coding time. Hard to beat that, especially when the Assemble technology was new to me.

Grunt

To start out, you need to setup Grunt. If you are already familiar with Grunt, don’t bother reading this section.

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

To install Grunt, install Node.js from the official Node website. The Node team provides a standard installer for your OS.

Next, open a command prompt (a terminal or Powershell window) and type the following:

% npm install --global grunt-cli

grunt-cli is an Node Package Manager (npm) package that runs the locally configured and installed Grunt for your project. Strange, I know! Grunt is something that you actually install in each of your Grunt-enabled projects. So 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 that are required by your project. Commit this file to your code repository.

Next, let’s configure Grunt by making 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 from what’s happening above:

  • initConfig configures individual Grunt tasks. I’ve included an empty assemble block above to get us started using Assemble’s grunt package.
  • grunt.loadNpmTasks loads Assemble.
  • registerTask groups tasks together by label. These labels can be called on the command line with the grunt program. For example, if I registered the task “deploy,” I could type grunt deploy to execute the tasks defined by that alias. In the Gruntfile above, I’ve registered the “default” task, which can be called by running grunt or grunt default since it’s the default. ;)

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

Assemble

In the section above, we installed and created a base Gruntfile.js configuration.

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 let’s 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 of the assemble block define our layout directory and the default layout. Assemble will pass our content pages through a layout of our choosing. In this case, we’re just using the default layout, default.hbs; however, we could define custom ones if we wanted. This will do for now.

Next create the default layout that we referred to in our 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 below in our Markdown file.
  • The content in between {{#markdown}} and {{/markdown}} will be interpreted as Markdown and converted to HTML.
  • {{>body}} will be replaced with the body of our Markdown file.

Now let’s create an index page with Markdown:

src/content/index.md

---
title: Home
---

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

The top part of this file is known as YAML Front Matter (YFM). Jekyll, another static publishing engine, also uses YFM in its templates. The YFM is usually a key: value list, and each of those keys is exposed in our assembly as Handlebars expressions (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.

With all of that out of the way, let’s run this thing.

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

This will create a new file in our 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 – useful 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 our 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 very 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 lists, 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 to be used when generating lists. The pages collection is available by default and includes all of 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 YPM will be excluded because I’ve added {{#unless data.exclude}} to the template.

The YPM properties for each item in the collection are attached to a data object instead of the root scope like before. This avoids YPM collisions, which would occur if both the page in the loop and the current index page both defined the same YPM value. For example, without data.title, Handlebars would have a difficult 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, but for now I’d rather just keep one template for simplicity.

After creating this 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’re done with our very basic blog. Be sure to watch for the next entry in this series!

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

Conclusion

This post explained how to create a basic, static blog using the Node.js package, Assemble.

In Building an Assemble.io Blog: Part 2, I improve the blog by creating partials, splitting the default template into two, and using a custom Handlebars helper to display a blog summary on the home page.

Thanks for reading!