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 emptyassemble
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 typegrunt deploy
to execute the tasks defined by that alias. In the Gruntfile above, I’ve registered the “default” task, which can be called by runninggrunt
orgrunt 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
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.
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!