Hugo + Staticman - embedded comments system

Staticman is a comment system built for static sites. Static sites with superpowers - a claim that points out the directions of the new wave of services around static site generators. Staticman ships user comments directly in your repository and can be used as SaaS or self-hosted version.

Get a first impression

Staticman handles user-generated content for you and transforms it into data files that sit in your GitHub repository, along with the rest of your content.

To get the goal of staticman we recommend you to have a look at the staticman documentation.

Staticman currently only works if your repository is hosted on Github.

Setup Staticman for Hugo

For staticman, you need to have three crucial parts to setup:

  • a base configuration file in your root directory called staticman.yml
  • a form that sends data to staticman
  • a page or partial rendering the data

The configuration file

For this part, we add a file called staticman.yml in our root directory where you define all configuration necessary to run staticman.

# Name of the property. You can have multiple properties with completely
# different config blocks for different sections of your site.
# For example, you can have one property to handle comment submission and
# another one to handle posts.
comments:
  # (*) REQUIRED
  #
  # Names of the fields the form is allowed to submit. If a field that is
  # not here is part of the request, an error will be thrown.
  allowedFields: ["name", "email", "body"]

  # When allowedOrigins is defined, only requests sent from one of the domains
  # listed will be accepted.
  allowedOrigins: ["localhost", "example.com"]

  # (*) REQUIRED
  #
  # Name of the branch being used. Must match the one sent in the URL of the
  # request.
  branch: "master"

  # List of fields to be populated automatically by Staticman and included in
  # the data file. Keys are the name of the field. The value can be an object
  # with a `type` property, which configures the generated field, or any value
  # to be used directly (e.g. a string, number or array)
  generatedFields:
    date:
      type: date

  # The format of the generated data files. Accepted values are "json", "yaml"
  # or "frontmatter"
  format: "json"

  # Whether entries need to be appproved before they are published to the main
  # branch. If set to `true`, a pull request will be created for your approval.
  # Otherwise, entries will be published to the main branch automatically.
  moderation: true

  # Name of the site. Used in notification emails.
  name: "Hugo + Staticman"

  # (*) REQUIRED
  #
  # Destination path (directory) for the data files. Accepts placeholders.
  path: "data/comments/{options.entryId}"

  # (*) REQUIRED
  #
  # Destination path (filename) for the data files. Accepts placeholders.
  filename: "{@id}"

  # Names of required files. If any of these isn't in the request or is empty,
  # an error will be thrown.
  requiredFields: ["name", "body"]

  # List of transformations to apply to any of the fields supplied. Keys are
  # the name of the field and values are possible transformation types.
  transforms:
    email: md5

posts:
  allowedFields: ["title", "body"]
  allowedOrigins: ["localhost", "example.com"]
  branch: "master"
  generatedFields:
    date:
      type: date
    slug:
      type: slugify
      options:
        field: title
    tags: ["user-generated"]
  format: "frontmatter"
  moderation: true
  name: "Hugo + Staticman"
  path: "content/post"
  filename: "{fields.slug}"
  requiredFields: ["title", "body"]
  transforms:
    body: "frontmatterContent"

We have copied the above example configuration from an example repository of staticman+hugo.

Adjust the parameters as you need them. A full list of available parameters can be found in the documentation.

If you need support how to write a yaml-file we recommend you a quick look at our TOML vs. YAML vs. JSON article.

The form partial for comments

The structure of the form is pretty straightforward and needs to POST data to a custom staticman endpoint. That can be your endpoint or the SaaS endpoint from staticman.net.

To make it a little bit more configurable in the whole Hugo environment we should set a couple of paths in our config.toml from Hugo:

[staticman]
  endpoint = "https://api.staticman.net/v2/entry/"
  username = "yourgithubusername"
  repository = "reponame"
  branch = "master"

Next, create a partial for our comment form called layouts/partials/staticman/form-comments.html (for sure you can name it how you wish) with the following content:

<form method="POST" action="https://api.staticman.net/v2/entry/{{ .Site.Params.staticman.username }}/{{{ .Site.Params.staticman.repository }}/{{ .Site.Params.staticman.branch }}/">
    <input type="hidden" name="options[redirect]" value="{{ .Permalink }}#comment-submitted">
    <input type="hidden" name="options[entryId]" value="{{ .UniqueID }}">
    <input name="fields[name]" type="text" placeholder="Your name">
    <input name="fields[email]" type="email" placeholder="Your email address">
    <textarea name="fields[body]" placeholder="Your message. Feel free to use Markdown." rows="10"></textarea>
    <input type="submit" value="Submit">
  </form>

After a successful comment, the user gets redirected to the current page including a hashtag #comment-submitted - from this point you are free to interact to provide appropriate feedback to the user or redirect to another thank you page you like.

Show the comments

As soon as a comment is received staticman pushes the comment in the format defined in the configuration to our data files so Hugo can access them by {{ .Site.Data }}.

Let’s create another partial for rendering the comments on our page - this time call itlayouts/partials/staticman/show-comments.html with the following content:

  {{ $comments := readDir "data/comments" }}
  {{ $.Scratch.Add "hasComments" 0 }}
  {{ $entryId := .UniqueID }}

  {{ range $comments }}
    {{ if eq .Name $entryId }}
      {{ $.Scratch.Add "hasComments" 1 }}
      {{ range $index, $comments := (index $.Site.Data.comments $entryId ) }}
<blockquote>
  <p>{{ .body | markdownify }}</p>
  <cite>
    <img src="https://www.gravatar.com/avatar/{{ .email }}?s=100">
    <strong>{{ .name }}</strong><br>{{ dateFormat "02/01/2006" .date }}  
</cite>
</blockquote>
      {{ end }}       
    {{ end }}
  {{ end }}

  {{ if eq ($.Scratch.Get "hasComments") 0 }}
    <p>Hey, be the first who comment this article.</p>
  {{ end }}

This partial above renders the user comments including a beautiful picture from Gravatar if available.

Now all together

Let’s see what we have made so far. Configuration - check - Form - check - Comments rendering - check.

The only thing missing is a simple stitch-everything-together page. Lets make a small example with the default single-template:

<h1>{{ .Title }}</h1>
{{ .Content }}
<hr>
<h2>Comments</h2>
{{ partial "staticman/form-comments" . }}
{{ partial "staticman/show-comments" . }}

It’s easy isn’t it?