I finally started writing a comment system for my static blog.

DIY Comments - Step 1

1,150 words.

DIY Comments - Step 1

This is a post about adding a homegrown DIY comment system to my static blog. It’s a pretty narrow audience that would be interested in this.

The easiest way to add comments to a static blog like mine is to use one of those third-party comment platforms like Disqus, or setup your own private comment server with something like Remark42.

But there are other ways, for those who are inclined toward getting their hands dirty with HTML and Javascript and serverless cloud computing platforms and doing stuff that’s never been done before. Unfortunately for you, the reader, I’m one of those people.

First of all, why not use Disqus, et al?

  • I have no control over what a third-party comment service like Disqus or CommentBox or GraphComment does with your comments. Free services are notoriously limited and or riddled with ads. Paid services undermine the entire reason for running an incredibly cheap-to-operate static blog.
  • I can recover control of the comment data by using a self-hosted comment system like Remark42. However, it’s a pain to set them up, they may not work quite the way I want, and it costs money to run the server, which, again, undermines the cheapness of a static blog.

So, writing my own comment system is the only way to do everything the exact way that I want to do it.

I have technically done this before, in the 2000s with a classic PHP-and-MySQL solution, but I’m doing it entirely differently this time so it will work with a static blog.

For the rest of this post, I’ll be assuming you know the basics of creating and using AWS Lambda functions and API Gateways.

Comment Form

Step one of this long, multi-part journey is to create a comment form. The easiest thing in the world to add to an HTML page. It looks something like this, simplified. I’ll leave the styling to the reader’s imagination.

<form action="#" method="POST">
    <input type="text" id="name" name="name" required>
    <textarea id="comment" name="comment" rows="4" required></textarea>
    <input type="submit" value="Submit">
</form>

Immediately we run into problems as a static blog. A static blog means there is no server running the blog (well, there is, but it only serves static, pre-generated files–it doesn’t run any code).

So what do we put in that action property? Where do we post this form? I can’t post it back to the same page, because nothing will happen. Readers can enter comments all day but they won’t go anywhere. I need somewhere to post to.

AWS Lambda Function

For this experiment, I turned to AWS again. Doing stuff with AWS or Google or Azure is pretty much the bread and butter of web development these days.

Unfortunately this probably gets me kicked out of the IndieWeb club, because, while they would undoubtedly applaud a DIY comment system, AWS would surely be considered part of the “corporate web” and thus off-limits. Ah well. Convenience wins out over ideology yet again.

Here’s a simplified Lambda function to process a form request, stripped of everything but the bare bones. I used Node.js. I don’t like writing in Javascript that much but it’s pretty easy to write single functions.

import querystring from 'querystring';

export const handler = async (event) => {
    const body = querystring.parse(event['body-form']);

    const name = body.name;
    const comment = body.comment;

    // TODO: Save the comment somewhere
    console.log(`Received: name="${name}" comment="${comment}"`);

    const response = {
        statusCode: 200,
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            message: `Comment received!`,
            data: {
                name: name,
                email: comment,
            }
        }),
    };

    return response;
};

We aren’t going to concern ourselves with saving the comment yet, or validating that it isn’t spam. That’s for later.

The Lambda function is the easy part. The hard part is the API Gateway. You can’t call a Lambda function directly (or, at least, you shouldn’t), you have to put it behind an API Gateway.

AWS API Gateway

In order to provide a public endpoint to post form data, I created an AWS API Gateway. Among many other things, these are used when you want to provide an API for things like reading and writing to databases.

I created a POST /comment endpoint for the API and added my Lambda function as the backend integration. This is where things got tricky.

By default, AWS APIs expect to use JSON data. The API passes a JSON request payload to the Lambda function, and the Lambda function returns a JSON result object. This is how we like to do things in the modern world of development.

Unfortunately, HTTP FORMs are time travelers from the ancient past, and don’t work that way. They use an application/x-www-form-urlencoded request body, which is essentially a url query string stuff in the request body. It’s ugly and yucky.

Side note: A brief discussion with ChatGPT reveals that there have been discussions about adding application/json as a new enctype for forms, but it’s not here yet, and the traditional solution for submitting form data as json is to use Javascript. One of my design goals here is not to require Javascript in the browser, so that’s out.

So I had to dig through Google and ChatGPT to figure out how to pass a non-JSON request body from the API to the Lambda function. This was the most difficult part so far.

To make a long story short, because I’m not teaching a class here, the key area to investigate is the integration request and integration response part of the API Gateway configuration. A template mapping is required to put the request body urlencoded string into a json object to pass to the Lambda function for decoding.

I don’t fully understand the inner workings, but the mapping looks like this:

#set($allParams = $input.params())
{
    "body-form" : $input.json('$')
}

After using this mapping, you can get the form data in the Lambda function with this:

const body = querystring.parse(event['body-form']);

Updating the Form

Now that we have an API Gateway backed by a Lambda function, we can finally update the action in our HTML FORM:

<form action="https://api-gateway-execution-point" method="POST">
    <input type="text" id="name" name="name" required>
    <textarea id="comment" name="comment" rows="4" required></textarea>
    <input type="submit" value="Submit">
</form>

That’s it for now. Future steps include:

  • Checking for spam
  • Styling the comment form
  • Writing comments to a Dynamo table
  • Reading comments from the Dynamo table with another API and Lambda function
  • Client-side Javascript to fetch and display recent comments from the API
  • Importing my blog comment history into local data files inside my Hugo content directory
  • Build-time Hugo templates to query local comment data archives to render older comments as static HTML
  • Administrative tools so I can manage comments
  • A way to submit comments using some kind of third-party OAuth like Google if you want to
  • The list goes on and on

P.S. While I’m very motivated right now, there is a very real possibility I might suddenly lose interest one day and just stop working on it.

Read Part 2

Related

Archived Comments

UV-Tester 2024-09-08T13:51:54Z This is another statically-generated comment, written in Markdown, so we have to remember to process it as Markdown instead of plain text. This was also statically generated as part of the site build. That part was easy. The hard part is figuring out how to download comments from the AWS Dynamo table to a file on my laptop.

This is a homegrown DIY comment system I'm working on. It technically works but it hasn't been through extensive testing yet. Good luck. Go here to enter a comment on this post without Javascript.