Metalsmith static blog with realtime comment features

metalsmith-static-with-realtime-comment-features-header.png

Sometime, somehow we have wanted to put up a Metalsmith static blog and host it somewhere quickly, no frameworks, no “2017 latest web tech” to build it with.

Introduction

Sometime, somehow we have wanted to put up a Metalsmith static blog and host it somewhere quickly, no frameworks, no “2017 latest web tech” to build it with. Just plain HTML and CSS; maybe some JavaScript, nothing too complex.

NOTE: In order to send messages to Pusher a server component is necessary. Github pages (or any other static hosts) will not allow you to have a real server. Therefore, to get one running, you can deploy the server that we will be creating in this article to Heroku.

It may be a single page portfolio site, listings for an event or a blog and the need for a Static Site Generator(SSG) is required. Instead of using Jekyll (built with Ruby), we choose Metalsmith because it is built with JavaScript. In this article, we are going to see how to add the realtime comment system to a static Metalsmith-driven blog using Pusher. We use Pusher to render realtime comments on posts.

Here is a glimpse of what we will be building:

What is MetalSmith?

Simply put, metalsmith is a light-weight and extremely pluggable static site generator. When I say extremely pluggable, it just has more to do with ready-made awesome plug-ins than HTML! Get that yet? Read on!

Using MetalSmith is pretty straightforward, simply create a source folder with source files in it written with markdown, metalsmith manipulates the files with the aid of plug-ins and converts the files to plain HTML in a destination folder ready to be served. Here’s an outline of how it really works:

  • Create markdown files in a source folder.
  • MetalSmith converts it to a JavaScript object.
  • metalsmith applies a set of manipulations in the form of plug-ins already supplied.
  • metalsmith outputs the manipulated file as an HTML file to be served.

Installation

I assume you have Node installed as well as npm so create a project folder like metalsmith-blog, and change directory to the project folder.

1mkdir metalsmith-blog && cd metalsmith-blog
2
3run
4
5
6    npm init
7    npm install metalsmith --save

These create a package.json file. We can see the metalsmith already included as a dependency in the package.json file.

In the project folder (root), create the folders src and build. The src folder would hold the source files (HTML, CSS and JS) whereas the build folder serves as the destination for the transformed source files.

Let’s install the necessary plug-ins. It’s getting sweet!

1npm install --save metalsmith-collections metalsmith-markdown metalsmith-assets metalsmith-templates metalsmith-permalinks jade

In brief:

  • metalsmith: Installs Metalsmith
  • metalsmith-markdown: Converts markdown files written in the source to HTML
  • metalsmith-templates: Enables the use of templates like Jade/Pug for layouts
  • metalsmith-assets: Includes static assets like CSS and JavaScript
  • metalsmith-permalinks: Customize permalink of files
  • metalsmith-collections: Helps you generate grouped content list for pages like Home
  • jade: preferred template engine for Metalsmith

There are a whole lot of plug-ins, thanks to the awesome metalsmith development community, you can find them here.

We got the basic folders now, and our package.json file looks like:

1{
2      "name": "realtime-static-metalsmith",
3      "version": "1.0.0",
4      "main": "index.js",
5      "license": "MIT",
6      "scripts": {
7        "serve": "serve build",
8        "build": "node build.js",
9      },
10      "dependencies": {
11        "jade": "^1.11.0",
12        "metalsmith": "^2.3.0",
13        "metalsmith-assets": "^0.1.0",
14        "metalsmith-collections": "^0.9.0",
15        "metalsmith-markdown": "^0.2.1",
16        "metalsmith-permalinks": "^0.5.0",
17        "metalsmith-templates": "^0.7.0",
18      }
19    }

Note the "``scripts``" property which was included. To serve the project on your local network, you must have serve installed else install it with:

1npm install serve --save-dev

Assign the value of "``serve build``" to the "``serve``" property so whenever we run serve in the command line, the build folder is served to our local network! Also the value "``node build.js``" is assigned to the property "``build``", this ensures that when we run

1npm run build

in the command line, the build.js file is run.

Note: The build.js file hasn’t been created yet.

Let’s get to creating the files!

Build Script for Metalsmith

Create the file build.js in the root directory, this serves as the engine of the site, as all the plug-ins are to be configured here. It’s built as thus:

1 – Load the plug-ins

1var metalsmith  = require('metalsmith');
2    var markdown    = require('metalsmith-markdown');
3    var templates  = require('metalsmith-templates');
4    var permalinks = require('metalsmith-permalinks');
5    var collections = require('metalsmith-collections');
6    var assets = require('metalsmith-assets');

2 – Call the metalsmith function metalsmith() and chain all the plug-ins loaded to it with the use() method, passing their required arguments as well.

Take note of the order in which the plug-ins are passed as the source files will be manipulated in that order.

1metalsmith(__dirname)
2      .source('src')
3      .use(collections({
4        articles: {
5          pattern: 'articles/**/*.md',
6          sortBy: 'date',
7          reverse: true
8        }
9      }))
10      .use(markdown({
11        gfm: true,
12        tables: true,
13      }))
14      .use(assets({
15        source: 'src/assets/',
16        destination: './'
17      }))
18      .use(permalinks())
19      .use(templates({
20        engine: 'jade',
21        directory: 'templates'
22      }))
23      .destination('build');

Note the source and the destination directories. For this article, we used Jade as the templating tool. Feel free to try something else!

3 – Error Handling in case of build error

1...
2    .destination('build')
3    .build(function (err) {
4        if (err) {
5          throw err;
6        }
7      });

Home and Article Layout with Jade

Jade is used as the templating engine to pre-process the HTML to be written. Install Jade with:

1npm install --save jade

Create an article.jade file in the templates folder to the server as a layout template for markdown articles:

1html(lang='en')
2      head
3        meta(charset='utf-8')
4        meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
5        meta(name='viewport', content='width=device-width')
6        title= title
7        link(rel="stylesheet", href="/css/styles.css" type="text/css")
8        link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css")
9      body
10        .container-fluid#app
11          a(href="/index.html" style="text-decoration:none")
12            h1.text-primary.text-center Raccoon Blog
13          .main(style="margin:50px 20%")
14            h2= title
15            p.text-info= author
16            div.timestamp= date
17            br
18            article!= contents
19            article-comment
20        script(src="https://unpkg.com/vue")
21        script(src="https://js.pusher.com/4.1/pusher.min.js")
22        script(src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js")
23        script(src="/js/app.js")

Create another jade file in for the Home page layout named index.jade in the same template directory:

1html(lang='en')
2      head
3        meta(charset='utf-8')
4        meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
5        meta(name='viewport', content='width=device-width, initial-scale=1')
6        title= 'Raccoon Blog'
7        link(rel="stylesheet", href="css/styles.css" type="text/css")
8        link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css")
9      body
10        .container-fluid
11          h1.text-primary.text-center Raccoon Blog
12          p.text-center A blog by Christian Nwamba
13          .main
14            each article in collections.articles
15              article.content-article
16                header
17                  p
18                    span.timestamp= article.date
19                  h3
20                    a(href='/' + article.path)= article.title
21                hr

Both article have some common features — same stylesheets and head elements. The article layout displays a single article while the index layout displays a collection of articles.

In the src directory, create an index.html file — this is the root HTML and it is the entry point for the Metalsmith project. The content just points to the index.jade:

1---
2    template: index.jade
3    ---

Adding Article Pages

To start seeing articles in action, create an src/articles/ directory. The markdown files for our articles will be in this folder.

To test that the articles are generated, create two example MD files in the src/articles folder you just created with an example content like the following:

1**welcome.md**
2
3    ---
4    title: An intro to Lorem Ipsum Text.
5    date: 2017-08-29 00:01
6    author: by Christian Nwamba
7    template: article.jade
8    ---
9    **Lorem Ipsum for the gods**
10    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

and the second:

1**helloworld.md**
2
3    ---
4    title: Section 2 of Lorem Ipsum.
5    date: 2017-08-29 12:28
6    author: by William Imoh
7    template: article.jade
8    ---
9    **Section 2**
10    Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet...

We have the front-matter on top, this contains details of the blog post and is used by article.jade during preprocessing. The front-matter is a list of meta-information about a particular document. MetalSmith picks the information up and uses it to dynamically generate a layout for each article.

Run the build command to generate output

1npm run build

Swoosh! The source files in markdown have been converted to HTML files in the build directory. Serve the build directory with:

1npm serve build

Now relieve the joy of seeing your static blog serving on localhost! Here is what it looks like:

Now click on one of the articles and you should see something like:

Let’s get to the more fun part.

Creating comments with Vue

You didn’t see that coming yeah, not to worry, we wouldn’t go too deep into Vue. We only need to create the Javascript file app.js in /src/assets/js. In /template/article.jade we import Vue at the bottom of the script from a CDN by putting in,

1script(src="https://unpkg.com/vue")
2    script(src="/js/app.js")

The second script tag links the local JavaScript file to the jade template.

App.js

In app.js, a new Vue instance was created with a Vue component (article-comment). This component contains the template for the comment form as well as event functions to handle submission and on-screen display of comments. Check it out here.

1var articleComment = {
2      template:  `
3        <div>
4          <h3>Comments</h3>
5          <div class="form-group">
6            <input type="text" class="form-control" placeholder="Name" v-model="name" />
7          </div>
8          <div class="form-group">
9            <textarea class="form-control" placeholder="Comment here..." v-model="content"></textarea>
10          </div>
11          <button class="btn btn-info" @click="onSubmit()">Submit</button>
12          <h4 v-if="comments.length > 0">Existing comments</h4>
13          <div class="list-group">
14            <a class="list-group-item" v-for="comment in comments">
15              <h4 class="list-group-item-heading">{{comment.name}}</h4>
16              <p class="list-group-item-text">{{comment.content}}</p>
17            </a>
18          </div>
19        </div>
20      `,
21      data() {
22        return {
23          comments: [],
24          name: '',
25          content: ''
26        }
27      },
28      methods: {
29        onSubmit: function() {
30          this.comments.push(
31            {name: this.name, content: this.content}
32          )
33          this.name = '';
34          this.content = '';
35        }
36      }
37    }
38    var app = new Vue({
39      el: '#app',
40      components: {
41        'article-comment': articleComment
42      }
43    })

Here’s what actually happens:

  • Once a comment is created and the submit button is clicked, the onSubmit() method is called.
  • The name and content variables are updated with the name and comment in the form input.
  • The name:content pair is pushed into Vue’s comment array.
  • These values are passed to the DOM via Vue template.

Run:

1npm run build
2    npm serve build

Notice that the JavaScript folder and file has been created in /build/assets. That is how seamless metalsmith can be.

Now we can create comments and have them display on-screen!

Is this the best part yet? Hell no! Let’s get to it.

Implementing Realtime comments display with Pusher

Why Pusher?

Being leaders in realtime technology, Pusher gives us the power to implement realtime features in our blog with their simple hosted API.

Create a pusher account

Go on to Pusher and create an account. Create a new app and obtain an app key, this would come in very handy soon enough.

Install Pusher and Axios

Pusher is installed in two parts, the clients-side, and the server side. For the client side we install pusher and Axios by importing the script with:

1script(src="https://js.pusher.com/4.1/pusher.min.js")
2    script(src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js")

Axios enables us to make HTTP requests to our local node server. There are lots of alternatives to Axios but we are sticking to it because of its simplicity and the promise based API.

Update Vue App with Pusher Realtime Features

Now we have to update the /src/assets/js/app.js file to include server configurations for the node server.

First, two new variables will be added to the data() method and assigned a value of null. This is to ensure that the pusher and channelName variable are accessible in the Vue instance.

1data() {
2        return {
3          comments: [],
4          name: '',
5          content: '',
6          pusher: null,
7          channelName: null,
8        }
9      },

We want to configure Pusher immediately Vue is loaded. To do so, we configure Pusher in Vue’s created lifecycle method and pass the value to the pusher property:

1new Vue({
2    ...
3      created() {
4        this.pusher = new Pusher('PUSHER-KEY', {
5          encrypted: true
6        });
7    ...
8    })

also channelName is assigned the path of any article in view with:

1this.channelName = window.location.pathname.replace(new RegExp('/', 'g'), '-');

The path assigned is modified to use a dash( – ) to indicate paths instead of forward slash ( / ). This is because pusher channel names doesn’t recognize paths with strokes but with dashes.

A variable channel with a local scope in the method is created and is assigned the result of calling the pusher.subscribe() **method with this.channelName as parameter.

channel is bound to the response received from the server new-comment, and passed a callback function with an argument comment which is the data from the response.

This data is parsed and pushed into the comments array on the Vue instance!

1this.channelName = window.location.pathname.replace(new RegExp('/', 'g'), '-');
2    var channel = this.pusher.subscribe(this.channelName);
3    channel.bind('new-comment', (comment) => {
4      this.comments.push(
5        {name: comment.name, content: comment.content}
6      )
7    });

Now, the method onSubmit() still doesn’t feel right as we still can’t be handling form submissions locally without sending data over to the server right?

We need to send POST request to the server so that Pusher can emit realtime events to all connected clients.
To do so, simply send a POST request with the form values to the server using axios:

1onSubmit() {
2       const payload = {name: this.name, content: this.content, channel: this.channelName};
3       axios.post('http://localhost:2000/comment', payload)
4          .then(response => {
5            console.log(response);
6          })
7    }

Configure Server

On the server side, In addition to pusher, there are other dependencies required to build our realtime feature, they are Body-Parser, CORS and Express Server. We install these with:

1npm install --save body-parser pusher cors express

These dependencies are added to our package.json file and it looks like this:

1{
2      "name": "realtime-static-metalsmith",
3      "version": "1.0.0",
4      "main": "index.js",
5      "license": "MIT",
6      "scripts": {
7        "serve": "serve build",
8        "build": "node build.js",
9        "watch": "nodemon --ignore build/ build.js",
10        "server": "node server.js"
11      },
12      "dependencies": {
13        "body-parser": "^1.17.2",
14        "cors": "^2.8.4",
15        "express": "^4.15.4",
16        "jade": "^1.11.0",
17        "metalsmith": "^2.3.0",
18        "metalsmith-assets": "^0.1.0",
19        "metalsmith-collections": "^0.9.0",
20        "metalsmith-markdown": "^0.2.1",
21        "metalsmith-permalinks": "^0.5.0",
22        "metalsmith-templates": "^0.7.0",
23        "pusher": "^1.5.1"
24      }
25    }

In our root folder, we should create a node server (server.js) and configure it as given below:

1//Load tools
2    const express = require('express');
3    const bodyParser = require('body-parser')
4    const Pusher = require('pusher')
5    const cors = require('cors')
6
7    const app = express();

The build tools are first loaded, then we configure the express server using cors and bodyParser as such:

1//configure Express
2    app.use(cors())
3    app.use(bodyParser.urlencoded({ extended: false }))
4    app.use(bodyParser.json())

Next up is configuring pusher using app details obtained during account creation on Pusher:

1const pusher = new Pusher({
2      appId: 'PUSHER-ID',
3      key: 'PUSHER-KEY',
4      secret: 'PUSHER-SECRET',
5      encrypted: true
6    });

Now to the most important part, configuring the express route. On receiving a POST HTTP request for a new-comment, the request body is logged on the console, we use the trigger method to open a Pusher channel based on the channel value on the req.body object. The event is named new-comment and the payload sent from the client is sent out to all connected clients via that channel. Lastly the server is created to listen on port 2000 of our localhost!

1app.post('/comment', (req, res) => {
2      console.log(req.body);
3      pusher.trigger(req.body.channel, 'new-comment', req.body);
4      res.send('Pushed');
5    })
6
7    app.listen(2000, () => console.log('Listening at 2000'));

To run the server, you can update your package.json include a run script for sever.js:

1"scripts": {
2      "server": "node server.js"
3    }

Execute the following to rebuild the Metalsmith app:

1npm run build

Then use this to start the server:

1npm run server

Here is what the realtime comments look like:

We now have our blog listening for comments. To try this out, open a separate browser, run an instance of the blog on the browser, add a new comment in the previous browser and watch it update in the new browser…in real time!

You can find the full build here.

Conclusion

It has surely been awesome building a static blog with metalsmith, using Vue for the comments and Pusher for the realtime updating of the comments.
Feel free to add your styles to the blog. Try out other templating engines too, maybe pug. Also play around with other MetalSmith plug-ins. Have fun.