lewisdale.dev
Colour scheme

Adding comments to my blog

Yet more Eleventy-related things!

I like to have a central place where people can interact with my posts if they wanted to. Right now, that place is Twitter, but it's sort of imploding at the minute and I'm unsure if I want to keep using it as much moving forward.

So, the easiest thing for me to do is to include a (very basic) comments system! At least then it allows people to leave a quick comment on the page.

What this does:

Allows people to leave a comment on a post

What this doesn't do:

This isn't going to be a fully-featured comments system to replace something like Disqus - there are no accounts, threads, tagging etc. It's just a simple display name + message combination.

I tried two approaches to adding comments before settling on the current one.

Option 1: Using Netlify Forms with Functions

This was the first approach I used, which is called the Jamstack Comments Engine. It uses Netlify Forms as a storage solution, which trigger a serverless function when a user comments. It puts the comments into a queue, which have to be manually approved. Once approved, they go into a different Form storage, which triggers a full build of the site. Whew.

Pros

Cons

I didn't stick with this for very long purely because I didn't like how long the builds were taking.

Option 2: Netlify Edge Functions

This was my second choice, and in my opinion works much better. It uses Netlify Edge Functions with Eleventy Edge to save and retrieve comments on the edge.

Edge Functions & Eleventy Edge mean you can handle dynamic content, but parse it with Eleventy, and it's injected before the page is served. Provided the connection is fast, it shouldn't give any noticeable slowdown, but means that we're still only serving HTML & CSS.

Edge Functions run in Deno, so they can be written as Javascript or Typescript.

Data storage

I could still use Netlify Forms for this, and just handle the comment retrieval using Edge functions. But instead, I decided to use Supabase as a data store for now. This means my data is just in Postgres, which makes migrating much easier. It also has a sizable free tier that should let me handle more comments than this site is ever likely to see.

Handling submissions

After following the setup instructions in the Eleventy Edge documentation, I was left with a directory called netlify/edge-functions/. I added a new file, comments.ts, and then added the following:

import type { Context } from "https://edge.netlify.com";
import * as supabase from 'https://deno.land/x/supabase_deno/mod.ts';


const sb = new supabase.supabaseClient(
Deno.env.get("DATABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_API_KEY")
);

export default async (request: Request, context: Context) => {
const url = new URL(request.url);

if (url.pathname.includes("/post/") && request.method === "POST") {
// Save the comment
const body = await request.clone().formData();
const data = Object.fromEntries(body);

const comments = sb.tables().get("comments");

const res = await comments.items().add({
name: data.name,
comment: data.comment,
post: url.pathname,
});

return new Response(null, {
status: 302,
headers: {
location: url.pathname,
}
})
}

return context.next();
};

All this script is doing is checking that the request was sent to a Blog Post page, via a POST request. Then it attempts to store the comments in Supabase, and then redirects the user back to the Blog Post.

Then in my blog post template, I just need a simple form:

<form action="/post/adding-comments-to-my-blog/" method="POST">
<h2>Add a comment</h2>

<label for="name">Your name:</label>
<input type="text" name="name" id="name" />

<label for="comment">Your comment:</label>
<textarea name="comment" id="comment"></textarea>

<button type="submit">Add comment</button>
</form>

When a user submits this form, it'll trigger the edge function.

Displaying comments

I used a second edge function for this, which also uses the EleventyEdge function to allow me to inject some basic filters, data, and templates.

import * as supabase from 'https://deno.land/x/supabase_deno/mod.ts';

import {
EleventyEdge,
precompiledAppData,
} from "./_generated/eleventy-edge-app.js";

const sb = new supabase.supabaseClient(
Deno.env.get("DATABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_API_KEY")
);


export default async (request, context) => {
try {
const url = new URL(request.url);
const comments = await sb.tables().get("comments").items().get("post", url.pathname);

let edge = new EleventyEdge("edge", {
request,
context,
precompiled: precompiledAppData,

// default is [], add more keys to opt-in e.g. ["appearance", "username"]
cookies: [],
});

edge.config((eleventyConfig) => {
eleventyConfig.addFilter("json", obj => JSON.stringify(obj, null, 2));
eleventyConfig.addFilter("date", dateStr => new Date(dateStr).toLocaleDateString("en-GB", {
dateStyle: "medium"
}));

eleventyConfig.addGlobalData("comments", comments);
});

return await edge.handleResponse();
} catch (e) {
console.log("ERROR", { e });
return context.next(e);
}
};

Again, all this does is load the comments for a Post, and adds them to the globally-available data via eleventyConfig.addGlobalData.

Then, I can display comments using the edge shortcode:

{% edge "njk" %}
<h2>Comments</h2>
<ul>
{% for comment in comments %}
<li>
<strong>{{ comment.name }}</strong> <span>{{ comment.created_at | date }}</span>
<p>{{ comment.comment }}</p>
</li>
{% endfor %}
</ul>
{% endedge %}

Routing

Finally, I just needed to enable my edge functions in netlify.toml:

[[edge_functions]]
function = "eleventy-edge"
path = "/post/*"

[[edge_functions]]
function = "comments"
path = "/post/*"

And that's... more or less it! This works pretty nicely, but naturally has some flaws. For a start, there's limited-to-no spam protection. There's also no way to verify users are who they say they are. So there are definitely some improvements I'll need to make going forward.

But as a first (well, second) pass, it was a good effort and a nice intro to using Edge Functions.

Add a comment