DEV Community


"Static" Comments with Gulp, Hugo & Netlify

Tom Doe
Originally published at on ・8 min read

“Static” Comments…?

Working for friends and family is always quite a bit of an extra challenge, as “no” generally doesn’t count. That’s probably how I ended up looking for what I’ll call “‘static’ comments” for now. Sounds weird at first, but refers to comments (= dynamically added feedback/discussion) within the context and technical limitations of a static website.

There’s a fair amount of “out of the box” 3rd party services you can use, an overview can be found in the Hugo Docs. That’s not what I was after though - I wanted something lightweight and free that also conforms with the requirements the GDPR brought along in 2018.

After some extensive research and hours of studying various services’ documentations, I came across an article called JAMstack Comments on Lucky for me, that was exactly what I was looking for, even more complex than needed.

So, the following article will describe what I’d summarize as “probably the easiest way of adding comments to your Hugo site”. So, taking into consideration that this site also has a comment section now, please feel free to let me know your opinion. 😉

TL;DR - Netlify demo is live at


As described in the linked article, this simple “comment engine” basically works based on 3 components:

  1. Netlify forms
  2. Gulp accessing Netlify via API
  3. a static site generator (Hugo in this case)

Here’s a compact flowchart detailing the process:

Flowchart comment engine

A more detailed version of this workflow can be found here: Static comments with Netlify

Hugo Configuration

First off, we’ll need to add a form to our website in order to collect our comment submissions.

Hugo supports so called Partial Templates that we’re going to use here:

<div class="row mt-3 mb-5">
  <div class="col-sm-12 mt-3">
  <div class="col-sm-12 col-lg-8 offset-lg-2">
    <form name="BlogComments" action="/comment-thanks" method="POST" netlify-honeypot="bot-field" netlify>
      <input name="path" type="hidden" value="{{ .RelPermalink }}">
      <input name="bot-field" type="hidden">
      <div class="form-group">
        <label for="inputName"><h4 class="mb-0">Name</h4></label>
        <input name="Name" type="name" class="form-control" id="inputName" placeholder="John Doe" required>
      <div class="form-group">
        <label for="inputEmail"><h4 class="mb-0">Email address</h4></label>
        <input name="Email" type="email" class="form-control" id="inputEmail" placeholder="" required>
        <span class="small">Confidential, will not be shared with anyone or published here.</span>
      <div class="form-group">
        <label for="inputComment"><h4 class="mb-0">Comment</h4></label>
        <textarea name="Comment" class="form-control" id="inputComment" placeholder="Your comment..." style="height: 150px; resize: none;" required></textarea>
      <button type="submit" class="btn btn-primary px-3"><i class="fas fa-comment"></i>&nbsp;&nbsp;Submit Comment</button>
Enter fullscreen mode Exit fullscreen mode

Excerpt of this site’s comments.html

The form is based on the requirements for Netlify’s form processing found in their docs: Forms setup

Netlify requires the action to be a string starting with a slash - in case of Hugo sites, that means that you need to have canonifyUrls = "false" in Hugo’s config.toml. Otherwise, Netlify won’t recognize your form and you’re stuck.

(Don’t ask how much time went into finding this…)

Keep the input named path in mind, we’ll need that later on. Also, don’t forget to include your newly created partial in your respective page’s template:

<div class="container">
  <div class="row mt-3">
      <div class="col pt-3 mkd">
          {{ .Content }}
  {{ partial "blog/meta.html" . }}
  {{ partial "blog/nextprev.html" . }}
  {{ partial "blog/comments.html" . }}
  {{ partial "blog/related.html" . }}
Enter fullscreen mode Exit fullscreen mode

Once you’ve deployed your site including the form, Netlify should automatically recognise it and create an entry for it in your account at It will be displayed there using the <form> element’s name property.

Feel free to test it now, we’ll need some submissions later on anyway.

There’s more to do with Hugo later - we’re still missing a section to display the comments. That’s easier though, if we have some comments first.

Netlify Configuration

Your form should now be showing up in Netlify, submissions should also end up there.

Next, head over to Settings -> Build & deploy ( and create a Build hook. You’ll have to give it a name and select the branch for it to deploy your site from. Once created, it will provide a unique URL that you can send a POST request to in order to trigger a build.

In you’ll find a section called Form notifications that you can use to have Netlify notify you via Email when a new submission arrives. You should also create an Outgoing webhook here, which will send a POST request to your previously created Build hook. That means every comment will now lead to rebuild of your site, making sure it shows up where it was posted.

In the original css-tricks article, they also made use of Slack notifications approving/rejecting comments collected by Netlify. I decided not to go down that route and won’t be covering that part here. If you’d like to make use of that, just add the respective bits of code where necessary.

By now, you should receive an email notification for new submissions and you should also be able to see your site being rebuilt after each submission.

We need two more things though:

  • an API key
  • the form ID

To get an API key, head over to and create a new Personal access token. Make sure to save that token properly, as it can’t ever be displayed again afterwards.

The form ID can be found at A click on the respective form’s name will show its ID in your URL bar. You’ll need that for the gulp configuration, so make sure you write it down.

Gulp Configuration

I’m going to assume that your Hugo site is already working with gulp, as I won’t be covering gulp’s setup here.

Specific packages you’ll need are:

  • request
  • dotenv

Based on that, gulpfile.js starts like this now:

var gulp = require('gulp'),
  request = require('request'),
  fs = require('fs'),
  config = require('dotenv').config();
Enter fullscreen mode Exit fullscreen mode

For dotenv to work, we’ll need to work with the API key and the form ID from Netlify. Make sure you’re in your project’s root directory and create a file called .env. Don’t forget to add it to your .gitignore file, as this is merely for local builds/testing and shouldn’t be pushed to your (public) repository.

In .env, create a line each for your Personal access token and your Netlify form ID.

Mine looks like this:

Enter fullscreen mode Exit fullscreen mode

Now, on to gulpfile.js - we’ll first add var buildSrc = "./";, then proceed to a new gulp task called get-comments:

var buildSrc = "./";

gulp.task("get-comments", function (done) {
  // set up the request with appropriate auth token and Form ID
  var url = `${process.env.COMMENT_FORM_ID}/submissions/?access_token=${process.env.API_AUTH}`;
  // get the data from Netlify's submissions API
  request(url, function(err, response, body){
    if(!err && response.statusCode === 200){
      console.log("Submissions found");
      var body = JSON.parse(body);
      var comments = {};
      // shape the data
      for(var item in body){
        var data = body[item].data;
        var comment = {
          name: data.Name,
          comment: data.Comment,
          path: data.path,
          date: body[item].created_at
        // Add it to an existing array or create a new one
        } else {
          comments[data.path] = [comment];
      // write our data to a file where Hugo can get it.
      fs.writeFile(buildSrc + "data/comments.json", JSON.stringify(comments, null, 2), function(err) {
        if(err) {
        } else {
          console.log("Comments data saved.");
    } else {
        console.log("Couldn't get comments from Netlify");
Enter fullscreen mode Exit fullscreen mode

var url is a template literal making use of our .env file and its respective content. Make sure to change that accordingly in case your files entries are named differently.

fs.writeFile(buildSrc + "data/comments.json", creates a new JSON file in Hugo’s data directory that we’ll pull the comments from. If you want your JSON data file to be called differently, change it here. In terms of the GDPR, this file should probably not be available in your (public) repository. It’s sufficient to have this as a build-time resource only, i.e. not having a local and/or committed copy of it at all.

You can test this gulp task now - gulp get-comments should connect to Netlify successfully and then proceed to create a comments.json file in you data directory.

Displaying the Comments

As mentioned above, the display section for our comments is still missing.

In order to get that done, we’re going to make Hugo access our JSON data file, pulling the comments for the respective post out of it and rendering them below the comment form.

{{ $thisPost := .RelPermalink }}
{{ $comments := .Site.Data.comments }}
{{ $.Scratch.Set "counter" 0 }}
{{ range $comments }}
    {{ range . }}{{ if eq .path $thisPost }}{{ $.Scratch.Set "counter" (add ($.Scratch.Get "counter") 1) }}{{ end }}{{ end }}
{{ end }}
{{ if gt ($.Scratch.Get "counter") 0 }}{{/*  only show comment section if there are comments  */}}
  <div class="row mb-5">
  {{ range $comments }}
    {{ range . }}
      {{ if eq .path $thisPost }}
      <div class="col-sm-12 py-3">
        <div class="shadow-sm px-4 py-4">
          <i><span class="small">{{ .name }}</span><span class="small"> on {{ .date | dateFormat "Mon, 02 Jan. 2006, 15:04 MST" }}</span></i>
          <p class="px-3 mt-3 mb-0">{{ .comment }}</p>
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}
Enter fullscreen mode Exit fullscreen mode

Note that we need $thisPost in order to compare the post’s permalink to the path we stored when the comment was submitted.

Regardless of that, we’re going to range through the comments twice, first to figure out if there are any and a second time to render them if there are.

The range within a range construction is necessary because of the array-within-array structure of the JSON file - i.e. an array of the posts and a second array of comments therein.

By now, you should have your posts displayed with a comment form and the respective comments pulled from Netlify based on the comments.json file created by gulp.

Configuring Netlify to Include Gulp

I haven’t mentioned it earlier, but we’ll have to do one last thing in order to let Netlify know that we’d like to include our gulp task in the build process.

First, head over to your gulpfile.js again and create another task:

gulp.task('build', gulp.series('your-other-stuff-if-any','get-comments'));
Enter fullscreen mode Exit fullscreen mode

Then, open your package.json and make sure to include this task in the scripts section:

"scripts": {
    "deploy": "gulp build && hugo --minify"
Enter fullscreen mode Exit fullscreen mode

Finally, update (or create) your netlify.toml:

  publish = "public"
  command = "npm run deploy"
Enter fullscreen mode Exit fullscreen mode

Once committed and pushed to the branch your site deploys from, Netlify should process the comments based on your gulp configuration.


As stated in the introduction, this implementation of a comment engine for a Hugo site is about as lightweight as it can possibly be. However, features like comment moderation etc. that are most likely required for larger sites are missing, as I chose that that’s not required for the time being. As described in the css-tricks article though, this can easily be accomplished using Slack and Netlify functions together with the core functionality described here.

Overall, I’d say this is good enough for now and I’m happy to have found a “minimum viable solution”.

PS: there’s a repository in my GitHub account that you can clone and deploy to try all of that yourself: GitHub Repo

The demo site mentioned above is based on the code in that repository.

Discussion (0)