NextJS Free Commenting System using Github [Part 2/2]

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:

  1. Check if the comment has a parent or not.
  2. If it has a parent, then we need to append this comment to the parent
  3. 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.

Full Series:

Categories:

Next.js   Javascript   React   Github   Comments