Revamping 1klb comments part four: submitting comments

Published
15 September 2021
Tagged
Part of a series: Revamping 1klb comments

Over the last few posts on this blog(1 2 3) I've been introducing my system for collating, storing, and displaying comments on my static site blog using Javascript + HTML for the front-end and FaunaDB for the backend. We've gone through the basic outline of the system, set up the database, and talked about how we can fetch and display comments. In this post, I'll show how you can easily build a comment submission form in HTML, and create a solid, secure comment submission system with server-side functions.

A basic comment form

It's pretty easy to build a basic comment submission form in html:

<form class="comment-form">
  <p>
    <label>Name:</label>
    <input type="text" name="submitter" />
  </p>
  
  <p>
    <label>Comment:</label>
    <textarea name="body" rows=10 cols=50></textarea>
  </p>

  <button type="submit">Send</button>
</form>

Note that we're not chucking an action on our form: instead, we'll be handling form submission through javascript. Here's a real quick-and-dirty example which will grab the relevant data, but doesn't yet have the capability to insert records into our database:

let formElem = document.querySelector(".comment-form")

formElem.addEventListener("submit", function(e) {
  e.preventDefault()

  let formData = new FormData(formElem)

  let payload = JSON.stringify(Object.fromEntries(formData))

  console.log({payload})
})

So to summarise the above:

  • In the HTML snippet, we create a form with two inputs, a name and a comment.
  • In the Javascript snippet, we wait until the user submits the form, collate the data into a JSON object, and (right now, at least) display that to the console.

Now you're probably thinking: "this is where we make a FaunaDB call, right? And pass it that form information?"

Actually, we're going to put one other bit in the way, and that's a Netlify server-side function.

Cheating at static sites

I always feel like server-side functions are a bit of a cheat's solution to static site problems, since they're really making your static site, well, not quite as static as all that. Effectively, a server-side function is a bit of javascript (or a similar language) which can be called via a URL, will run on the server which hosts your site, and may also return some JSON or similar. AWS has them, so Netlify has them too.

Why use a server-side function here? A few reasons:

  1. We can choose to run our submitted comments through filters (think spam detection or the like)
  2. We don't expose the database to the reader at all
  3. We can do other stuff while we're pushing it to the database!

Here's a basic server-side function, without much actual function. It takes two arguments - the event which triggered it (ie the http call we make to the server, including any body we supply), and the context provided via the web app itself. It should return an object with two properties, those being an HTTP statusCode and a response body.

exports.handler = async function(event, context) {

  // The content of the comment is contained in the event body.
  // Parse it, and if the parsing fails, return a 400 status message.
  let eventBody = null

  try {
    eventBody = JSON.parse(event.body)
  } catch(e) {
    console.log(`ERROR: Invalid JSON - ${e.message}`)
    return {
      statusCode: 400,
      body: "Request body must comprise a valid JSON string."
    }
  }

  // Ensure it contains the relevant pieces
  if (!(eventBody.submitter && eventBody.body && eventBody.post_slug && eventBody.post_title)) {
    console.log(`ERROR - Required fields not defined.`)
    return {
      statusCode: 400,
      body: "Required fields not defined. event body must contain the fields 'submitter', 'body', 'post_title', and 'post_slug'"
    }
  }

  return {
    statusCode: 200,
    body: "I guess everything is good."
  }
}

This sits in your functions directory (by default, a folder in the root of your site, called functions/) - mine is called functions/submit-comment.js. The next time you push your site to netlify, the function will be deployed, and you'll be able to call it through the URL <yoursite>/.netlify/functions/<function-name>.

Kicking the tires

Let's make sure this works. We'll modify the existing javascript as follows:

let formElem = document.querySelector(".comment-form")

formElem.addEventListener("submit", function(e) {
  e.preventDefault()

  let formData = new FormData(formElem)

  let payload = JSON.stringify(Object.fromEntries(formData))

  fetch(
      "/.netlify/functions/submit-comment",
      {
        method: "POST",
        body: payload,
        headers: {'Content-Type': 'application/json;charset=utf-8'}
      }
    )
    .then(resp => {
      console.log(resp)
    })
})

So now when we submit the data through our form, we'll call the server-side function, and hopefully get a response!

Of course, it takes a decent amount of time to commit your changes, push them to Netlify, wait for your site to compile, and then visit it just to test a comment form (and chances are your static site generator's "preview" functionality won't also test server-side functions). Thankfully, there's a quicker way to test your server-side functions, through Netlify dev. I won't go into too much detail about Netlify dev here, as the link above will give you all the juice you need - but I figure it's worth mentioning here as I spent a good amount of time testing the commenting script the long way, and life's too short for that.

Hooking in to Fauna

Right, so! If you're following along at home, you should by now have a comment field that, when you click submit, fires the relevant inputs to the server-side javascript function. How do we let the server-side function do something with the data you've supplied?

First: rather than try doing this all through GraphQL, we're going to give in and use the FaunaDB javascript library. This is pretty easy to set up, and because the user won't be downloading and running this code, there's no performance hit for adding libraries to our server-side functions.

Your server-side function is actually evaluated using node (it makes sense - node is designed for people to run javascript on the server-side, which is exactly what we're doing here). This means we get access to all of node's lovely support framework, including npm and all the packages that gives us access to.

To get node requiring packages from npm, we'll have to sort out our package.json file. This file tells node which libraries to install, and actually means you won't need to install and then send a bunch of node libraries with your static site.

As someone who doesn't use node (and npm) much, I suspect you can get away with setting up your package.json by cding into your functions folder and then typing:

npm install faunadb

You can then delete your node_modules/ folder and package-lock.json file, or .gitignore them. What matters is that you've now set up a package.json file for Netlify to install off of.

While we're here, let's grab sanitize-html as well:

npm install sanitize-html

With these installed, we can pull them in to our server-side function:

const faunadb = require("faunadb")
const q = faunadb.query

const sanitizeHtml = require("sanitize-html")

Now we have these modules installed, we can use them. Let's set up a basic piece of code to (a) run the body of our comment through an HTML sanitiser, and (b) submit it to FaunaDB:

exports.handler = async function(event, context) {
  // ...
  // Our setup code above, parsing the event's body code into the
  // object `eventBody`

  let client = new faunadb.Client({secret: 'your FaunaDB API key here'})

  await client.query(q.Call('submit_comment', eventBody))

  return{
    statusCode: 200,
    body: "OK"
  }
}

Now at this stage you're probably asking two things:

  1. What's the best way to get my FaunaDB API key into this function?
  2. Wait, have we even built the submit_comment function in Fauna?

We'll deal with these one at a time.

Storing and retrieving your FaunaDB API key

First things first: storing your API key in your code repository, like storing a password in a code repository, is bad practice. But Netlify will let you store build-time constants - like API keys - in your app proper. To do so:

  1. Log in to your Netlify account and go to your app
  2. In the sidebar, click Build & deploy and then Environment

Here you can add any environment variables you want. For example, if you had an API key for Fauna, you could store it as FAUNA_API_KEY. Now, at any point in your server-side script, you can pull your API key into your script:

const FAUNA_KEY = process.env.FAUNA_KEY

Much more secure than just hoping no one checked out your git repo!

Building the submit_comment function

Right, so let's build the submit_comment function in Fauna. Here's something really basic:

createFunction({
  "name": "submit_comment",
  "body": Query(
    Lambda(
      "comment",
      Create(
        Collection("Comment"),
        {
          data: Var("comment")
        }
      )
    )
  )
})

We can type this snippet in to the Fauna shell and create the most basic possible comment creation function. It will take the data we submit and just ram it into the Comment collection. But, since we're here, we can also set some default values. I'm going to set two:

  • A creation date (always equal to "Now")
  • A numeric "status" flag, showing whether I've reviewed and approved the comment.
createFunction({
  "name": "submit_comment",
  "body": Query(
    Lambda(
      "comment",
      Create(
        Collection("Comment"),
        {
          data: Merge(Var("comment"), { status: 0, created_at: Now() })
        }
      )
    )
  )
})

This means that every comment, on creation, will have a created_at field (set to the time it was created), and a status field (set to 0).[1]

Putting it together

Now all you need to do it hook it up. This will require a few steps. You'll need to:

  • Ensure you have a role in Fauna which has the authority to run the submit_comment function, and add records to the Comment collection.
  • Generate an API key for this role.
  • Embed this API key in your Netlify site.

Thankfully this isn't too hard. Log into your Fauna account, and under Security → Roles click + New Role. Name it appropriately, and make sure it has the authority to call your submit_comment function and read from/write to your Comment collection.

You can then select the Security → Keys section and click +New Key to create a new key under your role. Copy that API key - Fauna won't keep a copy of it, so this is your one chance to grab it!

You can now embed this key in your site as described above, making it available to your script.

At this point, you'll get to test things. netlify dev is an absolute godsend when you're configuring all this. In my experience, even following strict guidance will only get you 80% of the way. You'll always run into weird niggles where the only way to fix things is to spend five or ten minutes playing around with the settings.

The final product

So here we are! If that all worked, you should have a comment submission box that, when triggered, fires a server-side event that in turn routes the comment to Fauna. And in turn, the comment display that we've made previously should then display that comment on your site!

Of course, it isn't all that straightforward. Most of the time there's at least one niggle in your setup that gives you half an hour of grief. This is part of the learning process. But hopefully this post has given you enough of a start.

There's one thing left for us: a website-based moderation system. It's surprisingly not too hard! It doesn't require any fancy CMS or anything, and in our next post on the subject we'll look at how you can build it.


  1. "Wait!" you're saying. "Comment moderation?" Yes, friend, comment moderation. Not today - perhaps in the next chapter on this tutorial, assuming that also doesn't take six months for me to type up and publish. ↩ī¸Ž