In the previous article we built the basic interface for our commenting system. Now we need to program the API endpoints to communicate with Github's API in order to save and retrieve the comments.
Save Comments
Let's start by saving some comment. In the pages/api
path, let's create a new folder named comments
, inside of which we'll create another folder named save
and finally inside that a file named [slug].ts
. Of course you can change the naming as you wish. You can also create a single path (for example, /api/comment
) and then call different functions depending on the method used.
To save a comment, we need to:
- Check if the comment has a parent or not.
- If it has a parent, then we need to append this comment to the parent
- Else, we can insert this comment into the array of comments we might already have
In both cases, we first need to request the data we already have, modify it and then update the repo.
Prepare the data
Let's start from a basic NextJS API function.
import type { NextApiRequest, NextApiResponse } from "next";
export default (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
return new Promise(async (resolve) => {
// Our code here
})
}
Inside this function, we'll first prepare the data sent to the API.
// Import the modules we need
import { encrypt } from "@lib/encryption/crypto";
import Comment from "@interfaces/Comment";
const newComment: Comment = {
date: req.body.date,
parentCommentId: req.body.parentCommentId,
id: req.body.id,
username: req.body.username,
email: encrypt(req.body.email as string),
content: req.body.content,
children: req.body.children,
};
const { slug } = req.query;
Merge parent and child comments
We need a function that will merge a child comment with its parent. Because we work with a basic Javascript object, we'll need
to use recursion to find the actual parent.
function appendToParent(comments: Array<Comment>, newComment: Comment): Array<Comment> {
comments.forEach((comment) => {
if (comment.id === newComment.parentCommentId) {
comment.children.push(newComment);
} else if (comment.children && comment.children.length > 0) {
comment.children = appendToParent(comment.children, newComment);
}
});
return comments;
}
Update the data
Now we have the new comment data, so we need to get the previous data and modify it.
To communicate with Github's API I used the official library @octokit/request
. From now on we'll work inside a try
block.
import { request } from "@octokit/request";
try {
// Here we request the document in JSON (vnd.github.v3+json) because
// with raw we don't have the file sha
const prevComments = await request(
// we request a GET on this path
"GET /repos/{owner}/{repo}/contents/{path}",
{
headers: {
// github private token
authorization: `token ${process.env.GITHUB_TOKEN}`,
// how we want the file. In this case, we want a JSON
accept: "application/vnd.github.v3+json",
},
// Owner of the repo
owner: "PandaSekh",
// Name of the repo
repo: "my-blog-repo",
// the path. I save the comments in a folder named comments in the root
path: `comments/${slug}.json`,
// the branch
ref: "prod",
}
).catch((e) => {
// We accept and will handle a 404 because not every post will have
// comments. For any other error statusCode, throw an error.
if (e.status !== 404) throw new Error(e);
});
// [...] We'll add more code here
}
Now that we have the new comment and, if present, the previous comments we can merge them and save the updated data. How we do this depends on the presence of previous comments.
// Still in the try block
// if prevComments is undefined, there are no previous comments. This is the first possibility.
if (prevComments) {
// get the data from the base64 encoded content and parse it as JSON.
let data = JSON.parse(
Buffer.from(prevComments.data.content, "base64").toString("ascii")
);
// Save the sha. We need it to update the file later on
const { sha } = prevComments.data;
// Merge the new comment to the parent if it has one. Else, simply add it to the array.
if (newComment.parentCommentId) {
data = appendToParent(data, newComment); // Merge the parent and the child comment
} else {
data.push(newComment);
}
// Save the updated comments to Github
const update = await request(
"PUT /repos/{owner}/{repo}/contents/{path}",
{
headers: {
// github private token
authorization: `token ${process.env.GITHUB_TOKEN}`,
// how we want the file. In this case, we want a JSON
accept: "application/vnd.github.v3+json",
},
// Owner of the repo
owner: "PandaSekh",
// Name of the repo
repo: "my-blog-repo",
// the path. I save the comments in a folder named comments in the root
path: `comments/${slug}.json`,
branch: "prod",
message: `Updated comment on post ${slug}`, // Git commit message
sha, // The sha we saved before
content: Buffer.from(JSON.stringify(data), "ascii").toString("base64"),
}
);
res.status(200).json(JSON.stringify(update));
resolve();
And now we write the else in case there were no comments before the new one.
else {
const data = [newComment];
// Save the new comment to Github
const update = await request(
"PUT /repos/{owner}/{repo}/contents/{path}",
{
headers: {
// github private token
authorization: `token ${process.env.GITHUB_TOKEN}`,
// how we want the file. In this case, we want a JSON
accept: "application/vnd.github.v3+json",
},
// Owner of the repo
owner: "PandaSekh",
// Name of the repo
repo: "my-blog-repo",
// the path. I save the comments in a folder named comments in the root
path: `comments/${slug}.json`,
branch: "prod",
message: `New comment on post ${slug}`, // Git commit message
content: Buffer.from(JSON.stringify(data), "ascii").toString("base64"),
}
);
res.status(200).json(JSON.stringify(update));
resolve();
}
} catch (e) {
res.status(500).json(e);
resolve();
}
Full API method
Below the complete API method for reference.
import { request } from "@octokit/request";
import type { NextApiRequest, NextApiResponse } from "next";
import Comment from "@interfaces/Comment";
import { encrypt } from "@lib/encryption/crypto";
function appendToParent( comments: Array<Comment>, newComment: Comment ): Array<Comment> {
comments.forEach((comment) => {
if (comment.id === newComment.parentCommentId) {
comment.children.push(newComment);
} else if (comment.children && comment.children.length > 0) {
comment.children = appendToParent(comment.children, newComment);
}
});
return comments;
}
export default (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
return new Promise(async (resolve) => {
const newComment: Comment = {
date: req.body.date,
parentCommentId: req.body.parentCommentId,
id: req.body.id,
username: req.body.username,
email: encrypt(req.body.email as string),
content: req.body.content,
children: req.body.children,
};
const { slug } = req.query;
try {
const prevComments = await request(
"GET /repos/{owner}/{repo}/contents/{path}",
{
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
accept: "application/vnd.github.v3+json",
},
owner: "PandaSekh",
repo: "my-blog-repo",
path: `comments/${slug}.json`,
ref: "prod",
}
).catch((e) => {
if (e.status !== 404) throw new Error(e);
});
if (prevComments) {
let data = JSON.parse(Buffer.from(prevComments.data.content, "base64").toString("ascii"));
const { sha } = prevComments.data;
if (newComment.parentCommentId) {
data = appendToParent(data, newComment);
} else {
data.push(newComment);
}
const update = await request(
"PUT /repos/{owner}/{repo}/contents/{path}",
{
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
accept: "application/vnd.github.v3+json",
},
owner: "PandaSekh",
repo: "my-blog-repo",
path: `comments/${slug}.json`,
branch: "prod",
message: `Updated comment on post ${slug}`,
sha,
content: Buffer.from(JSON.stringify(data), "ascii").toString(
"base64"
),
}
);
res.status(200).json(JSON.stringify(update));
resolve();
} else {
const data = [newComment];
const update = await request(
"PUT /repos/{owner}/{repo}/contents/{path}",
{
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
accept: "application/vnd.github.v3+json",
},
owner: "PandaSekh",
repo: "my-blog-repo",
path: `comments/${slug}.json`,
branch: "prod",
message: `New comment on post ${slug}`,
content: Buffer.from(JSON.stringify(data), "ascii").toString(
"base64"
),
}
);
res.status(200).json(JSON.stringify(update));
resolve();
}
} catch (e) {
res.status(500).json(e);
resolve();
}
});
};
Get Comments
The method to retrieve comments depends on how you want to build your website. As I expected very few comments and I wanted the website to be fully static, I get the comments in a GetStaticProps
method inside the [slug].tsx
page. Every new comment triggers a re-deploy and rebuild the site. This is not the best approach if you expect a moderate amount of comments, in that case it might be a better idea to use GetServerSideProps
.
// This method will vary depending on your needs
export async function getStaticProps({ params }: { params: { slug: string }}): Promise<{ props: Props }> {
const comments = await getComments(params.slug);
return {
props: {
comments,
},
};
}
async function getComments( slug: string ): Promise<Array<Comment> | null> {
try {
const comments = await request(
"GET /repos/{owner}/{repo}/contents/{path}",
{
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
accept: "application/vnd.github.v3.raw",
},
owner: "PandaSekh",
repo: "your-blog-repo",
path: `../../comments/${slug}.json`,
ref: "prod",
}
);
return JSON.parse(comments.data as unknown as string);
} catch (e) {
return null;
}
}
That's all! This is how I built my free static commenting system.
If you have any doubts you can comment here or write me on my social media.
Top comments (3)
Do you have a working demo? Where is the proof that it's even working on GitHub pages? Your blog don't use this system only utteranc.es.
Never said It'll work on Github Pages. If you can host a NextJS project on pages it should work, but note that you still need some API endpoint to make a request to.
This commenting system was used for a private project, not my personal blog. I see that there's a need for a public demo, so I'll build one asap.
It should must be hosted...