Your site can keep the comments

Projects
Popsicleboat, Hugo
July 12, 2026

The last post about this described the arrangement: this blog has no comment system, and posts that want a discussion link to a thread on Popsicle Boat instead. One anchor tag, nothing loaded in your browser, conversation happens somewhere built for conversations.

There was one honest objection to that design, and I eventually made it to myself: the link sends readers away. The conversation about a post ends up living somewhere other than the post. For a comment section, that’s backwards — you should be able to read the comments where you read the thing they’re about.

We tried solving this once before with an embed — an iframe mirroring the thread onto the page — and buried it, for reasons that still hold: an embed is one more thing a reader’s browser has to trust, and replies written for a community read strangely when beamed somewhere else live. The problem with the embed wasn’t the where. It was the when.

Build time, not run time #

Every thread on the boat now publishes its public replies as plain JSON — the thread’s address plus /replies.json:

{
  "thread": {
    "title": "The panel that closed itself",
    "url": "https://www.popsicleboat.com/c/blogging/posts/175",
    "reply_count": 1
  },
  "replies": [
    {
      "author": "jake",
      "body_html": "<p>Test</p>",
      "posted_at": "2026-07-12T23:53:45Z",
      "url": "https://www.popsicleboat.com/c/blogging/posts/175#reply-176"
    }
  ]
}

A static site fetches that while it builds and prints the conversation into the page as ordinary HTML. This blog’s Hugo template does it in about ten lines:

{{ with try (resources.GetRemote (printf "%s/replies.json" .Params.popsicleboat)) }}
  {{ if and (not .Err) .Value }}
    {{ $replies := .Value.Content | transform.Unmarshal }}
    {{ range $replies.replies }}
      <article class="boat-reply">
        <p><strong>{{ .author }}</strong> · <a href="{{ .url }}">{{ dateFormat "January 2, 2006" (time .posted_at) }}</a></p>
        {{ .body_html | safeHTML }}
      </article>
    {{ end }}
  {{ end }}
{{ end }}

No script. No iframe. Nothing for a content blocker to block, because there is nothing running. The replies you may be reading below this very post arrived that way — view source; it’s just paragraphs.

What this actually changes #

Reading happens at home. The blog is where the conversation is read; the boat is where it happens. Readers who just want to see what people said never leave your page. Readers who want to say something click through to a place with names and norms.

And the no-lock-in promise stops being a promise and becomes a property of the files. Every build, your site writes its own copy of every conversation. If the boat sank tomorrow — or if you simply left — the comments would still be right there in your HTML, yours, forever. A comment service where leaving costs nothing and takes nothing with it is, as far as I know, a new kind of deal.

Fresh replies appear on your next build. Static sites rebuild all the time; the conversation is never staler than your deploy habit, and the live thread is one click away regardless.

The comments came home. The conversation kept its address.

Replies from the harbor

Discuss this post aboard Popsicle Boat →

Left one of the replies above? It stays yours — you can edit or delete it in the thread, and this page follows on its next build.