In this post I will implement Staticman on my website. Using the following sources I was able to implement it:

Staticman

As Staticman works with REST form submissions we need to deploy a new instance of Staticman. We will deploy Staticman using Heroku which is recommended by Staticman. Heroku provides services and tools to build, run, and scale web applications.

Staticman uses a git repository to store comments. Since my website is built using GitLab and GitLab Pages I will also create a new git repo in GitLab to support my Staticman content. GitLab is supported in version 3 of Staticman API.

To enable Staticman to write code and create MRs to the new repo we need to supply credentials to the GitLab repo. Create a second account for Staticman to use. Give this user permissions to write to the repo and create MRs. Create a personal access token. As of writing this project access tokens in GitLab does not support editing permissions of token to our requirements.

Deploy application

With a git repo to store comments and a token to access the repo we are ready to deploy Staticman. Use the big purple button from the Staticman Github page. Create a new private key for Staticman on the Heroku app. Add the following config vars to the app:

NODE_ENV         production
GITLAB_TOKEN     YOUR PERSONAL ACCESS TOKEN
RSA_PRIVATE_KEY  YOUR PRIVATE KEY

Edit Git repo

The Staticman configuration file will be located in the new git repo we created to store Staticman comments. The contents of my staticman.yml is as follows:

# 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", "url", "comment"]

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

  # Text to use as the commit message or pull request title. Accepts placeholders.
  commitMessage: "Add Staticman data"

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

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

  # 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
      options:
        format: "timestamp-seconds"

  # 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: "eriksnartland.no"

  # Notification settings. When enabled, users can choose to receive notifications
  # via email when someone adds a reply or a new comment. This requires an account
  # with Mailgun, which you can get for free at http://mailgun.com.
  #notifications:
    # Enable notifications
    #enabled: true

    # (!) ENCRYPTED
    #
    # Mailgun API key
    #apiKey: "1q2w3e4r"

    # (!) ENCRYPTED
    #
    # Mailgun domain (encrypted)
    #domain: "4r3e2w1q"

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

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

  # 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

My git repo for storing comments is called blog_comments.

Edit Hugo site

To enable Staticman comments we need to make the following changes on our Hugo Website.

Add blog_comments (repo to store comments) as git submodule to the folder data/comments.

git submodule add https://gitlab.com/eriksnartland/blog_comments.git data/comments

Adding the repo as a submodule requires us to redeploy the website for every new comment.

Create file .gitmodules pointing to your submodule.

[submodule "data/comments"]
	path = data/comments
	url = https://gitlab.com/eriksnartland/blog_comments.git
	branch = main

Add the following to your config.yml file.

    staticman:
      api: https://staticman-erik.herokuapp.com/v3/entry/gitlab/eriksnartland/blog_comments/main/comments

Build GitLab Pages

Update the submodule as a build stage.

variables:
  GIT_SUBMODULE_STRATEGY: recursive

stages:
  - update
  - build
  - deploy

update_submodule:
  stage: update
  script:
    - apt-get update -qy && apt-get upgrade -qy
    - apt-get install -y git
    - git --version
    - git submodule update --remote data/comments
  artifacts:
    paths:
      - data/comments

build:
  image: registry.gitlab.com/pages/hugo:latest
  stage: build
  script:
  - hugo
  except:
  - master

pages:
  image: registry.gitlab.com/pages/hugo:latest
  stage: deploy
  script:
  - hugo
  artifacts:
    paths:
    - public
  only:
  - master

Add comments to page

Create the file themes/PaperMod/layouts/partials/comments.html.

<!-- {{ template "_internal/disqus.html" . }} -->
{{ partial "staticman.html" . }}

Create the file themes/PaperMod/layouts/partials/staticman-js-common.js.

function showComments() {
    // Remove button
    var staticmanButton = document.getElementById('staticman-button');
    staticmanButton.parentNode.removeChild(staticmanButton); 
    // Un-hide comments
    var staticmanComments = document.getElementById('staticman-comments');
    staticmanComments.style.display = 'block'; 
}

function checkForm(form){

    if(form.yourname.value == ""){
        form.warningComment.style.display = 'block'; 
        form.warningComment.innerText = "Please type your name";
        return false;
    }
    else if(form.yourcomment.value == ""){
        form.warningComment.style.display = 'block'; 
        form.warningComment.innerText = "Please type a comment";
        return false;
    }
    else{
        form.submitButton.disabled = true;
        form.warningComment.style.display = 'none'; 
        form.warningComment.innerText = "";
        form.submitButton.innerText = "Sending...";
        return true;
    }   
}

Create the file themes/PaperMod/layouts/partials/staticman.html.

{{ with .Site.Params.staticman.api }}
<aside aria-label="note" class="note">
    <section id="comment-thankyou" style="display: none; margin-left: 5px"> 
        Thank you for your comment, it will be displayed once it has been approved!
    </section>
</aside>
<script type="application/javascript">
    if (/comment-thankyou/.test(window.location.href)) {
        document.getElementById('comment-thankyou').style.display = 'block';
    }else{
        document.getElementById('comment-thankyou').style.display = 'none';
    }
</script>

<button id="staticman-button" onclick="showComments()">Show comments</button>

<section id="staticman-comments" class="js-comments staticman-comments">
    <script type="application/javascript">
          {{ partial "staticman-js-common.js" . | safeJS }}
      </script>
      <noscript>Enable JavaScript to view comments.</noscript>

    {{ $slug := replace $.RelPermalink "/" "" }}
  
    {{ if $.Site.Data.comments }}
      {{ $comments := index $.Site.Data.comments $slug }}
      {{ if $comments }}
        {{ if gt (len $comments) 1  }}
          <h3>{{ len $comments  }} comments</h3>
        {{ else }}
          <h3>{{ len $comments  }} comment</h3>
        {{ end }}
      {{ else }}
        <h3>No comments</h3>
      {{ end }}
  
  
      {{ $.Scratch.Set "hasComments" 0 }}
      {{ range $index, $comments := (index $.Site.Data.comments $slug ) }}
        {{ if not .parent }}
          {{ $.Scratch.Add "hasComments" 1 }}
          <article id="comment-{{ $.Scratch.Get "hasComments" }}" class="static-comment">
            <div class="comment-author">
                <strong>{{ .name }}</strong>
            </div>
            <div class="comment-timestamp">
                <a href="#comment-{{ $.Scratch.Get "hasComments" }}" title="Permalink to this comment">
                    <time datetime="{{ .date }}">{{ dateFormat (default "2 Jan 2006 15:04" .Site.Params.dateformat) .date}}</time>
                </a>
            </div>
            <div class="comment-content"><p>{{ .comment | markdownify }}</p></div>
          </article>
        {{ end }}
      {{ end }}
    {{ end }}
  
  
  
  <form class="js-form form" method="post" action="{{ $.Site.Params.staticman.api }}" onsubmit="return checkForm(this);">
    <fieldset>
        <input type="hidden" name="options[redirect]" value="{{ $.RelPermalink | absURL }}#comment-thankyou">
        <input type="hidden" name="options[slug]" value="{{ replace $.RelPermalink "/" "" }}">
        <input type="hidden" name="options[parent]" value="">

        <label for="fields[name]">Name:</label>
        <input name="fields[name]" id="yourname" type="text" placeholder="Your name"/>
        <label for="fields[comment]">Comment:</label>
        <textarea name="fields[comment]" id="yourcomment" placeholder="What are you thinking?"></textarea>

        <output id="warningComment"></output>
        <button class="button" id="submitButton">Submit</button>
    </fieldset>
  </form>
</section>
{{ end }}

Edit styling format CSS

Add the following code to themes/PaperMod/assets/css/common/main.css.

.staticman-comments {
    margin-bottom: 2rem;
}
.staticman-comments .comment-author, .comment-content, .comment-timestamp {
    margin-top: 0;
    font-size: 0.85rem;
}
.staticman-comments article{
    display: grid;
    grid-template-columns: 0.5fr 0.5fr;
}
.staticman-comments .comment-author {
    grid-column: 1;
    grid-row: 1;
}
.staticman-comments .comment-timestamp {
    grid-column: 2;
    grid-row: 1;
    text-align: right;
}
.staticman-comments .comment-content{
    margin-top: 0.5rem;
    grid-column: 1/3;
    grid-row: 2;
}

.staticman-comments .comment-content p {
    padding: 5px 1rem 5px 1rem;
}

.staticman-comments input {
    color: var(--content)
    font-size: 13px;
    line-height: 36px;
    border-radius: calc(36px / 2);
    border: 1px solid var(--primary);
    padding: 4px 5px;
    width: 100%;
    font-family:inherit;
    font-size: 0.85rem;
    margin-top: 0.5rem;
    margin-bottom: 1rem;
}

.staticman-comments textarea {
    color: var(--content)
    font-size: 13px;
    line-height: 36px;
    border: 1px solid var(--primary);
    border-radius: calc(36px / 2);
    padding: 4px 5px;
    vertical-align: top;
    height: 10em;
    width: 100%;
    font-family:inherit;
    font-size: 0.85rem;
    margin-top: 0.5rem;
    margin-bottom: 1rem;
}

.staticman-comments label, .staticman-comments button {
    margin-top: 0.5rem;
    font-size: 0.85rem;
}

fieldset {
    border:none;
}
#warningComment{
    margin-top: 1rem;
    margin-bottom: 1rem;
    width: 100%;
    display: block;
}
#staticman-button {
    width: 100%;
    padding: 0.25em 0.75em;
    margin-bottom: 1rem;
}
#staticman-comments {
    display: none; 
}