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:
initConfigconfigures individual Grunt tasks. The emptyassembleblock gets us started with Assemble’s Grunt package.grunt.loadNpmTasksloads Assemble.registerTaskgroups tasks together under a label. These labels can be called on the command line with thegruntprogram. The “default” task can be invoked withgruntorgrunt 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
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.
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
src/layouts/partials/footer.hbs
Now modify the default template to include those partials:
src/layouts/default.hbs
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
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
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
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
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:
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:
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.
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:
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:
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!