[ jd303 ]

Static sites don't have comments. That's the deal you make. There's no database, no runtime, no attack surface, and thus, no comment box. That seemed fine until the site was actually running and I started thinking about whether anyone who read something wanted somewhere to put a thought.

The obvious answer is Disqus. Hugo even has it built in - one template call and you're done. But Disqus is ad-supported, has a history of injecting trackers, and makes your readers create an account on a platform that exists to monetize their behavior. No thank you.

Giscus is the current good alternative. It backs comments with GitHub Discussions. It's free, open source, and you own the data because it's sitting in a repo you control. Your readers need a GitHub account to comment, which is a real filter, but this site is not trying to reach people who don't have GitHub accounts. The audience is fine. I also decided to add a subscribe form with Buttondown while I was in there.

The wrinkle is that "a repo you control" means GitHub knows whose account it is. Which opened a sidebar about which accounts were whose and what was visible to whom, and what started as "add a comment box" became a GitHub spring cleaning session.

[ claude ]

Two Things Before Comments

Before wiring up Giscus, two unrelated improvements happened in the same session.

Organizing Posts by Year

All posts were living flat in content/posts/. Fine at ten posts. Annoying at fifty. Hugo's URL structure is driven by the date frontmatter, not the folder path, so moving files into year subdirectories doesn't change any URLs.

content/posts/
  2026/
    ethics-autonomous-vehicles.md
    jira-as-a-signal.md
    ...

The hugo.toml permalink config handles the URL:

[permalinks]
  posts = "/:year/:month/:filename/"

Hugo reads :year and :month from each post's date field, not from where the file sits. Moving ethics-autonomous-vehicles.md into 2026/ doesn't change its URL at /2026/05/ethics-autonomous-vehicles/.

Side effect: Hugo generates a list page at /posts/2026/. It's not linked anywhere - it exists as a hidden artifact.

Any aliases in post frontmatter move with the files and keep working.

mkdir -p content/posts/2026
mv content/posts/*.md content/posts/2026/

Run hugo to verify - if it builds clean, the URLs are intact.


Setting Up Giscus

1. GitHub Account and Repo

Giscus needs a public GitHub repo with Discussions enabled. Create a dedicated repo so comments don't live next to your code.

The repo should be under the same identity you want associated with the site. A dedicated GitHub account - or a username that matches your site's brand - keeps your personal account out of the visible repo ownership.

Create the repo:

# Authenticated as your site account
gh repo create YOUR_ORG/YOUR_REPO_NAME --public --description "Giscus comments"

In the repo settings, enable Discussions: Settings → Features → Discussions.

Disable what you don't need: Wikis, Issues, Projects. This repo's only job is holding Discussion threads.

2. Discussion Category

In the repo's Discussions tab, the default General category works. The recommended setup is to use an Announcements type category instead - this means only you (the repo owner) can create new discussion threads directly on GitHub. Readers can still comment; they just can't open new threads manually. Giscus creates threads automatically on first page load, so the restriction doesn't affect normal usage.

Create an Announcements category if one doesn't exist, or set an existing category's type to Announcements in the category settings.

3. Install the Giscus GitHub App

Go to github.com/apps/giscus and install it on your account. When prompted, scope it to the comments repo only - not all repos.

4. Get the Config Snippet

Go to giscus.app. Fill in:

  • Repository: YOUR_ORG/YOUR_REPO_NAME
  • Discussion category: the Announcements category you created

The page will populate data-repo-id and data-category-id automatically. Copy the generated <script> block at the bottom.

5. Add to Hugo

In themes/YOUR_THEME/layouts/_default/single.html, add the Giscus script after the post content, wrapped in a section check so it only appears on posts:

{{ if eq .Section "posts" }}
<div class="giscus-comments">
  <script src="https://giscus.app/client.js"
          data-repo="YOUR_ORG/YOUR_REPO_NAME"
          data-repo-id="YOUR_REPO_ID"
          data-category="Announcements"
          data-category-id="YOUR_CATEGORY_ID"
          data-mapping="pathname"
          data-strict="0"
          data-reactions-enabled="1"
          data-emit-metadata="0"
          data-input-position="bottom"
          data-theme="preferred_color_scheme"
          data-lang="en"
          crossorigin="anonymous"
          async>
  </script>
</div>
{{ end }}

data-mapping="pathname" ties each Discussion thread to the post's URL path. data-theme="transparent_dark" lets the widget inherit your page background instead of rendering its own.

The {{ if eq .Section "posts" }} guard means the widget won't appear on pages like About or Contact that use the same single.html template.

The widget sits inside a styled container with a floating [ external transmissions ] label - amber, same treatment as the voice blocks. CSS is straightforward: .giscus-comments gets the border and glow, .giscus-comments-label gets positioned absolute at the top edge. Amber on near-black is 13.5:1 contrast, clears WCAG AAA.

One thing that bit us on this site: Hugo's template lookup prefers layouts/_default/single.html at the project root over the same file inside themes/. This site had both. The script went into the theme file first, built clean, and didn't appear anywhere. Took a grep of the built HTML to catch it. If Giscus isn't showing up after a rebuild, that's the first place to check.


Adding a Newsletter Subscribe Form

Buttondown handles the newsletter side. Create an account at buttondown.com, then use Buttondown's embed API to wire a subscribe form directly into the site - no JavaScript bundle, no third-party widget loader.

Create a partial at layouts/partials/subscribe.html:

<div class="subscribe-block">
  <div class="subscribe-label">// SUBSCRIBE //</div>
  <p class="subscribe-desc">New posts, weekly. No tracking. Unsubscribe anytime.</p>
  <form
    action="https://buttondown.com/api/emails/embed-subscribe/YOUR_USERNAME"
    method="post"
    class="subscribe-form"
  >
    <input type="email" name="email" id="bd-email" placeholder="your@email.com" required />
    <button type="submit">subscribe</button>
  </form>
</div>

Replace YOUR_USERNAME with your Buttondown username. Then include the partial in layouts/_default/single.html wherever you want the form to appear - after post content, before the Giscus widget.

{{ partial "subscribe.html" . }}

Buttondown's embed endpoint handles the form POST and returns a redirect. No AJAX, no API keys in the client, no cookies. The form works with JavaScript disabled.

[ jd303 ]

The GitHub cleanup was the part I didn't plan for. I had two accounts, a personal one under my real name, a secondary one (jd3o3) used for the blog persona as a machine account. Renamed jd3o3 to conspicuoustechnologist, renamed the profile repo to match, and transferred the blog repo over so it all lives in one place under the right name. GitHub redirects the old URLs for a while, which buys time to update anything hardcoded.

The gh CLI stored the account under its old name and I had to switch between accounts to do the repo operations. That's an easy fix once you know that's the issue.

What actually shows in Giscus when someone comments: their GitHub display name and avatar, linking to their profile. Your repo ownership shows if they click through to GitHub, but it's not in the widget. Set your display name to whatever handle you use on the site, and that's it.

The comment box is at the bottom of this post (well, and every post) if you want to test it.