Static Comments with Serverless and Staticman - Part 1

Jordan Patterson
Jordan Patterson

I started out using disqus for commenting on this blog but found it really slowed down the page load and added a lot of javascript. I remembered testing out a static commenting service previously but couldn't remember what it was called. Some quick googling jogged my memory and I rediscovered Staticman. This is how I added it to my site.

You Know What Happens When You Assume...

I made a few assumptions before I started that turned out to be incorrect. So I thought this was going to be relatively easy but it turned out to be much more difficult.

First, I assumed that the preferred method for using this was as a self-hosted service. It is not. Staticman offers a free hosted version and their documentation directs you to use the hosted option. Second, I assumed it was built to be run as a serverless function. It is not. Their suggested method for running the self-hosted version is clicking their "Deploy to Heroku" button on the GitHub readme.

Third, I assumed regardless of how it would be hosted, there would be good documentation. There is not. This one is the most frustrating. It turns out that this is an awesome project and it works very well. It is a shame that their documentation is bad and out of date.

Choosing the Path Less Travelled

Given my self imposed restriction of "owning the stack", the hosted option will not work. In the rest of this post we will:

  • Understand how Staticman works
  • Setup Serverless Framework
  • Configure our serverless function
  • Deploy our serverless function to AWS Lambda

How Does Staticman Work?

Staticman works by accepting comments at an api endpoint and then committing them to your repository. Once in your repository, you can parse them and display them on your site. If you are using CI/CD then the process works like this:

  1. Add a comment to a blog post
  2. The comment is posted to the Staticman api endpoint
  3. Staticman commits the comment to your repository
  4. Your CI/CD is triggered by the new commit and rebuilds and deploys your site
  5. The comment is live on your website

It's rather genius and works very well. Since I chose Hugo for my blog, this happens extremely fast. It usually take just a few seconds for the comment to appear.

Setting up Serverless Framework

If anyone has read my post on the cost of serverless this might feel contrary to my beliefs. However, since this blog doesn't get much traffic (and even fewer comments), the function likely won't be running very often. Serverless is a great way to keep my costs at essentially zero for keeping this site online.

Let's install serverless framework and initialize a new project.

$ npm install -g serverless
$ serverless create --template aws-nodejs --path static-comments
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/jordan/code/scratch/static-comments"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v2.13.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

This will create a basic nodejs project, configured to use AWS Lambda and Node.js.

$ cd static-comments
$ tree -a
.
├── .gitignore
├── handler.js
└── serverless.yml

0 directories, 3 files

Preparing the Serverless Function

Since we are going to be deploying Staticman, we don't need handler.js. That can be deleted. And we will need to clone Staticman.

# in ~/static-comments
$ git init
$ rm handler.js
$ git submodule add https://github.com/eduardoboucas/staticman.git staticman

Now create a new directory for the function handler and initialize it as an npm package.

# in ~/static-comments
$ mkdir app && cd app
$ npm init -y

As I mentioned above, Staticman doesn't work as a serverless function out of the box. We can easily fix that using the serverless-http npm package and creating a new hander.

# in ~/static-comments/app
$ npm i -s serverless-http
$ touch index.js

In index.js add the following contents:

const serverless = require("serverless-http");

let api;
try {
  const StaticmanAPI = require("../staticman/server");
  api = new StaticmanAPI();
} catch (e) {
  console.error(e);
  process.exit(1);
}

module.exports.handler = serverless(api.server);

This wraps the express server with the serverless handler so that we can use express in our serverless function.

Configuring Serverless Framework

The function needs to be added to serverless.yml. The complete file should look like this

service: static-comments
frameworkVersion: "2"

provider:
  name: aws
  runtime: nodejs12.x
  region: us-east-2
  stage: ${opt:stage, 'dev'}
  apiGateway:
    shouldStartNameWithService: true

functions:
  app:
    handler: app/index.handler
    events:
      - http:
          path: /
          method: ANY
          cors: true
      - http:
          path: /{proxy+}
          method: ANY
          cors: true
    environment:
      GITHUB_TOKEN: ${env:STATICMAN_GITHUB_TOKEN}
      RSA_PRIVATE_KEY: ${env:STATICMAN_RSA_PRIVATE_KEY}

As you can see from the yaml above, we will need a couple of environment variables. GITHUB_TOKEN is a personal access token from a GitHub account that has repo permissions on the repository that the comments will be stored in. The most secure way to do this is create a new GitHub account and add it as a collaborator on your repository. Create a personal access token for the new account and use that token here.

RSA_PRIVATE_KEY is exactly what it sounds like. You can create a new RSA private key like this:

$ ssh-keygen -t rsa -f staticman.key -m PEM

We will set these environment variables later in our GitHub Actions configuration.

Deploying to AWS Lambda

Since I am using GitHub and GitHub Actions is included, I'll be using it to deploy the function. Create the following file.

# ~/static-comments/.github/workflows/deploy.yaml
name: Deploy master branch

on:
  push:
    branches:
      - master

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - name: Install serverless
        run: sudo npm install -g serverless@2.13.0
      - name: Config credentials
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: serverless config credentials --provider aws --key ${AWS_ACCESS_KEY_ID} --secret ${AWS_SECRET_ACCESS_KEY}
      - name: Install node_modules
        run: |
          cd app && npm install && cd ..
          cd staticman && npm install && cd ..          
      - name: Deploy
        env:
          STATICMAN_GITHUB_TOKEN: ${{ secrets.STATICMAN_GITHUB_TOKEN}}
          STATICMAN_RSA_PRIVATE_KEY: ${{ secrets.STATICMAN_RSA_PRIVATE_KEY}}
        run: serverless deploy --stage prod --verbose

There are four secrets that will need to be set:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • STATICMAN_GITHUB_TOKEN
  • STATICMAN_RSA_PRIVATE_KEY

You can set these by going to Settings and then Secrets on github.com for the repository.

You should use the GitHub personal access token that we created earlier for STATICMAN_GITHUB_TOKEN. And use the contents of the staticman.key file that we created earlier for STATICMAN_RSA_PRIVATE_KEY. For the AWS variables, you will need IAM credentials with administrator access.

That's It!

All that is left to do is commit and push to GitHub. In 60-90 seconds your Staticman function will be ready to accept comments.

Should you run into any trouble, all of the code mentioned above can be seen her https://github.com/jpatters/static-comments. Or... comment below.

Part two will be coming soon and will show you how to make use of this on your static website!