McGarrah Technical Blog

Adding Comments to a Static Site: Why I Chose Giscus for Jekyll

· 13 min read

Jekyll is a static site generator. There’s no server, no database, no backend. When someone visits a page, they get pre-built HTML files served from a CDN. That’s the whole point — it’s fast, cheap, and secure.

But comments need state. Someone writes a comment, it has to be stored somewhere, and the next visitor needs to see it. Adding state to a stateless platform is a fundamental design tension — you need a data store, but you chose a platform specifically because it doesn’t have one.

The Problem

I wanted comments on this blog for a simple reason: readers ask good questions. When someone finds a gap in a Proxmox walkthrough or catches an error in a Ceph command, that feedback is valuable — not just to me, but to the next person reading the same post. Email works for one-to-one, but comments are one-to-many.

The requirements:

  1. No self-hosted infrastructure — I’m not running a database for blog comments
  2. Free or very cheap — This is a hobby blog
  3. GitHub-friendly — My readers are technical; most have GitHub accounts
  4. GDPR-compatible — No third-party tracking cookies
  5. Persistent — Comments survive site rebuilds and theme changes
  6. Searchable — Ideally indexed and findable
  7. Low maintenance — No moderation queue to babysit

The Alternatives I Evaluated

Disqus — The Default Choice (Rejected)

Disqus is the most common comment system for static sites. It’s easy to embed and has a large user base.

Why I rejected it:

The original Jekyll theme I forked (Contrast) actually had Disqus support built in. The dead code is still in my post.html layout — a disqus_thread div that never renders because site.comments.disqus_shortname is never set.

Isso — Self-Hosted Alternative (Rejected)

Isso is a self-hosted, lightweight commenting server. It stores comments in a SQLite database and has a clean, minimal interface.

Why I rejected it:

The theme also had Isso support built in. Same dead code situation — isso_domain is never configured.

GitHub Issues API — Custom Lambda (Evaluated Deeply)

This approach uses GitHub Issues as the comment store. Each blog post maps to a GitHub Issue. Comments on the Issue appear as comments on the post. Aleksandr Hovhannisyan’s implementation is the best-known version.

I went deep on this one — deep enough to have ChatGPT convert the original Netlify serverless function to a Python Lambda. The full prototype:

def get_comments_for_post(event, context):
    """
    Lambda function to fetch comments for a GitHub issue dynamically.
    """

    try:
        # Extract query parameters
        query_params = event.get("queryStringParameters", {})
        issue_number = query_params.get("id")
        github_url = query_params.get("url")

        if not issue_number or not issue_number.isdigit():
            return {
                "statusCode": 400,
                "body": json.dumps({"error": "You must specify a valid issue ID."}),
            }

        # Determine owner and repo
        if github_url:
            owner, repo = extract_owner_and_repo(github_url)
        else:
            owner = query_params.get("owner")
            repo = query_params.get("repo")

        if not owner or not repo:
            return {
                "statusCode": 400,
                "body": json.dumps({"error": "You must specify 'owner' and 'repo' or provide a valid GitHub URL."}),
            }

        issue_number = int(issue_number)

        # Check API rate limit
        rate_limit = octokit.request("GET /rate_limit")["rate"]
        remaining_requests = rate_limit["remaining"]
        print(f"GitHub API requests remaining: {remaining_requests}")
        if remaining_requests == 0:
            return {
                "statusCode": 503,
                "body": json.dumps({"error": "API rate limit exceeded."}),
            }

        # Fetch comments for the given issue
        comments_response = octokit.paginate(
            "GET /repos/{owner}/{repo}/issues/{issue_number}/comments",
            {"owner": owner, "repo": repo, "issue_number": issue_number},
        )

        # Process comments
        response = []
        for comment in comments_response:
            response.append({
                "user": {
                    "avatarUrl": comment["user"]["avatar_url"],
                    "name": escape(comment["user"]["login"]),
                    "isAuthor": comment["author_association"] == "OWNER",
                },
                "dateTime": comment["created_at"],
                "dateRelative": str((datetime.now() - datetime.fromisoformat(
                    comment["created_at"].replace("Z", ""))).days) + " days ago",
                "isEdited": comment["created_at"] != comment["updated_at"],
                "body": escape(markdown(comment["body"])),
            })

        return {
            "statusCode": 200,
            "body": json.dumps({"data": response}),
        }

    except Exception as e:
        print(f"Error: {e}")
        return {
            "statusCode": 500,
            "body": json.dumps({"error": "Unable to fetch comments for this post."}),
        }

This prototype handled input validation, GitHub API rate limiting, pagination, relative date formatting, edit detection, and Markdown rendering. It worked — but it was a lot of moving parts for blog comments.

Why I ultimately rejected it:

Utterances — GitHub Issues, Client-Side (Close Second)

Utterances solves the serverless function problem by using a GitHub App to authenticate directly from the client. It still uses GitHub Issues as the backend but doesn’t need a proxy.

Why I almost chose it:

Why I chose Giscus instead:

Staticman — Git-Based Comments (Rejected)

Staticman takes a different approach: comments are submitted via a form, processed by a bot, and committed to your repository as data files (YAML/JSON). Jekyll then renders them at build time.

Why I rejected it:

GDPR-Compliant Approaches

The Jekyll Codex GDPR-compliant comments guide was useful for understanding the privacy landscape. Any solution that loads third-party JavaScript or sends user data to external servers needs consent management.

Why Giscus Won

Giscus uses GitHub Discussions as the comment backend. It’s a single <script> tag that embeds a widget powered by the GitHub Discussions API via a GitHub App.

The key insight: everything stays in the GitHub ecosystem. The blog source is on GitHub. The build runs on GitHub Actions. The site deploys to GitHub Pages. Comments live in GitHub Discussions. There’s no external service, no separate database, no additional account.

What sealed the decision:

Implementation

Step 1: Enable GitHub Discussions

In the repository settings (mcgarrah/mcgarrah.github.io), enable the Discussions feature and create an “Announcements” category.

Step 2: Install the Giscus GitHub App

Go to giscus.app, select your repository, and configure the options. It generates the <script> tag and gives you the repo_id and category_id values.

Step 3: Add Configuration to _config.yml

giscus:
  repo: mcgarrah/mcgarrah.github.io
  repo_id: R_kgDOKBKIdw
  category: Announcements
  category_id: DIC_kwDOKBKId84Cq3DK
  mapping: pathname
  strict: 0
  reactions_enabled: 1
  emit_metadata: 0
  input_position: bottom
  theme: preferred_color_scheme
  lang: en
  loading: lazy

Key configuration choices:

Step 4: Add the Widget to the Post Layout

In _layouts/post.html:

{%- if site.giscus -%}
<section class="page__comments">
  <script src="https://giscus.app/client.js"
          data-repo="{{ site.giscus.repo }}"
          data-repo-id="{{ site.giscus.repo_id }}"
          data-category="{{ site.giscus.category }}"
          data-category-id="{{ site.giscus.category_id }}"
          data-mapping="{{ site.giscus.mapping }}"
          data-strict="{{ site.giscus.strict }}"
          data-reactions-enabled="{{ site.giscus.reactions_enabled }}"
          data-emit-metadata="{{ site.giscus.emit_metadata }}"
          data-input-position="{{ site.giscus.input_position }}"
          data-theme="{{ site.giscus.theme }}"
          data-lang="{{ site.giscus.lang }}"
          data-loading="{{ site.giscus.loading }}"
          crossorigin="anonymous"
          async>
  </script>
</section>
{%- endif -%}

Every _config.yml value is templated via Liquid — no hardcoded values in the layout. The {%- if site.giscus -%} guard means the widget only renders if Giscus is configured, so the theme works without it.

Legacy Dead Code

The post layout still contains the original theme’s Isso and Disqus support:

{% if page.comments != false and site.comments.isso or site.comments.disqus %}
  {% if site.comments.isso_domain %}<div id="isso-thread"></div>{% endif %}
  {% if site.comments.disqus_shortname %}<div id="disqus_thread"></div>{% endif %}
{% endif %}

This never renders because neither site.comments.isso_domain nor site.comments.disqus_shortname is set in _config.yml. It’s harmless dead code from the Contrast theme fork. I’ve left it in case someone forks this blog and wants to use those systems instead.

The GitHub Ecosystem Advantage

What I find elegant about this setup is how the pieces reinforce each other:

Component Service Data Location
Source code GitHub repository mcgarrah/mcgarrah.github.io
Build & deploy GitHub Actions .github/workflows/jekyll.yml
Hosting GitHub Pages mcgarrah.org via CNAME
Comments GitHub Discussions Same repository
Security scanning GitHub CodeQL .github/workflows/codeql.yml
Dependency updates GitHub Dependabot .github/dependabot.yml

Everything is in one place. One login, one set of permissions, one backup strategy (the git repository itself). If GitHub goes down, the whole blog is down anyway — there’s no additional point of failure from the comment system.

Comments are also version-controlled in a sense — GitHub Discussions have full edit history, and they’re tied to the repository. If I ever migrate the blog, the Discussions come with the repo.

The broader pattern here is worth noting: keeping your entire operational surface area in a single ecosystem — source, build, hosting, comments, security scanning, dependency management — reduces the number of vendor relationships, authentication boundaries, and failure domains you have to manage. It’s the same principle behind choosing a single cloud provider for tightly coupled services.

What I’d Do Differently


About the Author: Michael McGarrah is a Cloud Architect with 25+ years in enterprise infrastructure, machine learning, and system administration. He holds an M.S. in Computer Science (AI/ML) from Georgia Tech and a B.S. in Computer Science from NC State University, and is currently pursuing an Executive MBA at UNC Wilmington. LinkedIn · GitHub · ORCID · Google Scholar · Resume