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

pandasekh

Alessio Franceschi

Posted on October 9, 2021

Build a Free NextJS 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
    })
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

πŸ’– πŸ’ͺ πŸ™… 🚩
pandasekh
Alessio Franceschi

Posted on October 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related