If you are reading this, you probably like getting feedback for your work. For a blog-specific use case, comments are something that can bring in feedback easily. In this post I will talk about how I implemented it for my own website.
Features
The comments component I made has three major parts:
- Authentication
- Database to manage comments across multiple posts
- Comments and Replies
Authentication
This is an important step if you want to have any kind of protection. Although minimal, I have an auth wall to stop people from just spamming the api. I am using here, and you can probably use any auth system you would like.
I currently only allow sign ins from Google OAuth, and this probably discourages people who want to stay anonymous to comment on my posts. This is a trade-off that I believe would be better in the long run for my portfolio website, because it filters out comments that are fully anonymous.
Database
For my portfolio website, I am using to handle my database. Here is what my comments schema looks like:
typescriptexport const comments = createTable( "comment", { id: varchar("id", { length: 255 }) .notNull() .primaryKey() .$defaultFn(() => crypto.randomUUID()), content: text("content").notNull(), postSlug: varchar("post_slug", { length: 255 }).notNull(), userId: varchar("user_id", { length: 255 }) .notNull() .references(() => users.id, { onDelete: "cascade" }), parentId: varchar("parent_id", { length: 255 }), // No direct reference here createdAt: timestamp("created_at", { withTimezone: true }) .default(sql`CURRENT_TIMESTAMP`) .notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( () => new Date() ), isDeleted: boolean("is_deleted").default(false).notNull(), }, (comment) => ({ userIdIdx: index("comment_user_id_idx").on(comment.userId), postSlugIdx: index("comment_post_slug_idx").on(comment.postSlug), parentIdIdx: index("comment_parent_id_idx").on(comment.parentId), }) );
Two things that I want to talk about here is the parentId and isDeleted columns.
I intentionally designed parentId without a direct foreign key constraint to avoid any referential constraints on replies. By omitting the direct reference, I gained flexibility to handle comment hierarchies, including scenarios involving soft-deleted parent comments.
The isDeleted boolean column implements a soft delete strategy, which means I am marking the comment as deleted, rather than deleting the row from the database. The advantages to this is that I can keep the comment thread's structural integrity, as well as maintaining historical context for nested replies. This also prevents cascaded deletion issues when a comment with replies is deleted. One thing that I would like to add in the future is in cases where there are no replies or all replies are also deleted, I fully delete the thread from the database. This can probably be done during deletion by checking if all parent/child/sibling comments are also soft deleted.
Comments and Replies
Under my /posts/[slug]/page.tsx route is where I have my Custom MDX, using SSG. Right under the MDX content, I have the comments component!
tsx<Suspense fallback={ <div className="text-muted-foreground text-sm">Loading comments...</div> } > <HydrateClient> <Comments slug={slug} session={session} /> </HydrateClient> </Suspense>
slug here is used to pass to the database to store the specific post the comment is made under, and session is used to authenticate the user.
You can scroll down to see what the comments look like! Most of the UI is written using shadcn and a little bit of AI.
typescriptconst shouldShowThread = (comment: Comment): boolean => { // If the parent comment is not deleted, we should always show the thread if (!isCommentDeleted(comment)) return true; // If parent is deleted, check if any replies exist and are not deleted if (comment.replies && comment.replies.length > 0) { return comment.replies.some((reply: Comment) => !isCommentDeleted(reply)); } // Parent is deleted and no replies or all replies are deleted return false; };
Remember soft-deleting the comments in the database? This helper function is used to determine which comments will be rendered to the user.
Conclusion
Building a comments component for my blog has been an interesting learning experience. The implementation combines authentication, database design, and React components to create a functional commenting system that allows readers to engage with my content.
If you're building your own blog or content platform, I hope this breakdown of my comment system implementation helps you create something that works for your specific needs. Comments are a valuable way to build community around your content and receive direct feedback from readers.
Feel free to leave a comment below if you have any questions or suggestions about this implementation!
Comments