iron's blog

Comments

Articles are cool but comments can often add insightful and/or infuriating content. That is why I wanted to add comments to my webite. My website is currently just static HTML which is generated from a handful of templates and markdown files using Astro. However, comments are more dynamic than that and cannot be added to the HTML at compile time. I want my website to work on devices that do not run javascript, so using pure html/css on the client device is a requirement. This rules out (almost?) all SPA/CSR techniques such as React/Angular/Vue/Whatever. SSR (PHP, ASP.NET, etc.) would still do the trick, but they require the server to dynamically generate HTML on the fly, and this can dramatically increase server load.

Astro islands

Astro by default generates pure static HTML without any client-side javascript, but astro islands can add javascript libraries to isolated parts of the website. This means that the website can use CSR libraries such as react, but still function perfectly fine (besides the react parts!) if javascript is disabled. This way I can create a simple react component that can dynamically handle all comment-related parts of the website, while clients without javascript can still use the website without comments. If you scroll to the bottom of this page, you will find a comment section that is rendered by React, it is hydrated when the section becomes visible and it retrieves the comments run-time.

Adding React components to Astro is simple, Astro has a simple guide, after following the guide, you can simply import and use jsx files as if they were Astro files.

interface CommentsProps {
  articleId: string;
}

export const Comments: React.FC<CommentsProps> = ({ articleId }) => {
  const [message, setMessage] = useState<string>("");
  const [comments, setComments] = useState<Comment[]>([]);

  useEffect(() => {
    let fn = async () => {
      const response = await fetch(`https://my-api/${articleId}`);
      if (!response.ok) {
        setMessage(`Something went wrong: ${await response.text()}`);
        return;
      }
      setMessage("");
      setComments(await response.json());
    }
    fn();
  }, []);

  // TODO: Improve styling
  return <div>
    {message && <div>{message}</div>}
    {comments.map((comment, index) => {
      return <div key={index}>{comment.content}</div>
    })}
  </div>;
}
<Comments client:visible articleId={frontmatter.id} />

Dotnet

But ofcourse, the client cannot save or distribute the comments on its own. That is why a (small) backend is required to handle the processing of comments. The comments itself are stored in a very simple MariaDB table with only a few rows. And to ‘connect’ this database to the website, I created a simple C# Web API which can be used by the website to post and get the comments for a specific article. This backend also handles the admin-approval, since otherwise I would be afraid of spam and inappropriate comments.

Simply define a class which describes a comment

public enum CommentVisibility
{
    Unrated = 0,
    Approved = 1,
    Rejected = 2,
}


public class Comment
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public DateTime Date { get; set; }
    public CommentVisibility Visible { get; set; }
    public string ArticleId { get; set; } = "";
    public string Author { get; set; } = "";
    public string Content { get; set; } = "";
}

Create a DbContext

public class CommentDbContext : DbContext
{
    public DbSet<Comment> Comments { get; set; }

    public CommentDbContext(DbContextOptions<CommentDbContext> options) : base(options) { }
}

And its ready for use in a controller or service. It is generally speaking good practice to abstract the business-logic to a service, but since the entire application is so small, there is really no benefit to the additional level of abstraction.

[AllowAnonymous]
[HttpGet("{articleId}")]
public ActionResult<IEnumerable<Comment>> GetComments([FromRoute] string articleId)
{
    if (!siteService.Site.articles.Any(x => x.id.Equals(articleId)))
    {
        return BadRequest($"Article '{articleId}' is not known");
    }

    using (var dbContext = dbContextFactory.CreateDbContext())
    {
        return dbContext.Comments
          .Where(c => c.Visible == CommentVisibility.Approved && c.ArticleId == articleId)
          .OrderByDescending(c => c.Date)
          .ToList();
    }
}

And that is the basis of what is required to show comments on my website. In reality it is slightly more complex. and I also added Keycloak to support admin logins, where the admin can approve or reject comments, and email notifications to notify me when someone posted a comment. The full source to everything is ofcourse available on my Gitlab.

Thank you for reading this article.
If you spot any mistakes or if you would like to contact me, visit the contact page for more details.