A week after the post about using the fediverse as this site’s comment system the thread under it had taught me more than the building did. One link a reader posted changed the widget twice. One thing a reader did showed me a hole I thought I couldn’t close. And I owe some people a credit.

Not a new idea
Among the comments was a link, with “that’s been around for a while” attached. Andreas Scherbaum wrote this up in May 2024 for his Hugo site, and he opens by crediting the people before him: Carl Schwan’s post from December 2020, which is the one most of the later ones trace back to, then Jan Wildeboer’s for Jekyll and Daniel Pecos Martínez’s. Four years of people arriving at the same shape: publish, announce on Mastodon, let the page fetch the replies to the announcement. I had arrived at it too, and written it up as if it were mine. It isn’t, and this is the correction. What I added was Bluesky as a second source and a Lemmy community mention to get the post in front of people, and neither of those is much.
I read Scherbaum’s post properly and took two things from it.
His comments are a thread. Mine were a flat list sorted by time, which read fine with four replies and fell apart at eighteen, when a reply to a reply landed six entries below the thing it answered. Each reply now carries the id of the one it answers, children render under their parent, oldest first, and anything deeper than three levels sits at three, because the indent on a phone had run out of room by then. A reply whose parent I can’t see, because it was deleted or isn’t public, goes to the top level rather than vanishing.
The other was when to load. The page used to ask Mastodon and Bluesky for the replies as soon as it opened, for every reader, including the ones who never scrolled past the second paragraph. The privacy page says the site contacts no third party. That was only true if you didn’t count the comments. Now an observer watches for the comments heading to come within about a screen of the viewport and fetches then. Someone who reads the first half and leaves causes no request to anyone. I checked that with a browser script against the live page: zero API calls before scrolling, three after.
Then I looked at the live post and saw a flat list. The new script was there, the page even said so, and my browser was running the old one. The web server tells browsers to keep scripts for a week, which is sensible for a file that never changes and the opposite of that for one I had just changed under the same name. Fresh browsers got the threads; anyone who had read the post before, which is the audience of a follow-up, got the old file for up to a week. The stylesheet already went through the build with a hash in its filename, so a change means a new name and the old one simply isn’t asked for again. The script now does the same. I had tested the change in a browser that had never seen the page, which is the one kind of browser a returning reader doesn’t have.
He also keeps a per-post list of comments to hide. I haven’t copied that. Moderation stays on the Mastodon side, where blocking an account should keep its replies out of the API too, though I haven’t had to find out, and the day a Bluesky reply needs hiding I’ll add a list for that.
The slow network blanks the fast one
While testing the threads I caught one page load where the section stayed empty for seconds with no error, and the requests had all gone out. It didn’t happen again and I never proved what it was. But the shape was clear enough: the code waited for both networks before drawing anything, with no limit on the wait. I held the Bluesky request back on purpose to see it, and sixteen Mastodon replies that had arrived in under a second sat invisible behind it.
Each network’s replies now draw as they come in, and a request gives up after ten seconds. The status line under the heading says which one failed, “Bluesky replies unavailable right now”, instead of claiming there are no replies, which is a different statement and a false one. Small change. The first post argued that you only meet this kind of thing on real networks with real readers, and here it took one slow request to prove it.
The replies that weren’t there
One reader on Bluesky replied to the announcement, I answered, and a little later they added a second reply under their first one. I got the notification. The blog never showed it, and the thing that watches the threads for me and pushes new replies to my phone didn’t see it either. My first guess was that the widget was broken.
A Bluesky reply stores two references: the post it answers, and the top of the thread it belongs to. Their first reply had both pointing at my announcement, which is right. The second had both pointing at their own first reply. The parent was correct, the root was not, and when you ask the API for the thread under my post, a reply with the wrong root simply isn’t in it. I tried from three directions and it came back from none of them.
My first draft of this post said I wouldn’t work around it. The only way I could see was to read every commenter’s recent posts from every reader’s browser and look for stragglers, which is slow and turns a comment widget into something that follows people. Then the same account did it again, on the eighth, this time under my answer, and I finally looked at the rest of its replies. Of its last fifty-three replies to other people, fifty-two set the root to the post they answer. That is only correct for a direct reply to a top-level post, which is why its first comment worked and anything deeper went missing.
The way out had been in my notifications the whole time. Bluesky knew where those replies belonged well enough to tell the blog’s account about them. So a small job on the server now reads that account’s notifications every ten minutes, keeps the replies whose parent is somewhere in one of the post threads but which the thread itself doesn’t contain, and writes their ids into the same small file the page already reads to find the announcements. The page fetches those posts directly and hangs each one under its parent. No reader’s browser looks at anyone’s other posts, and the reply watch and the stats read the same list. Both missing replies were back under the post within a few minutes of the first run.
It only catches what Bluesky tells me about. If a third person answered one of those lost replies with the same kind of software, nothing would notify me, and that reply would stay missing. I haven’t seen it happen yet.
There’s a smaller version of the same thing on the Mastodon side that I haven’t tested either. The public endpoint that returns a thread’s replies is said to stop at sixty for unauthenticated requests. My busiest thread has twenty. If one ever gets there, the last comments will drop off the page and I’ll find out from a reader.
What I’d say now
The first post said comments outliving the site was the point. It still is. But “the comments” turned out to mean “what two public APIs return for one post id”, and that’s a narrower thing than a comment thread. Most of the time they’re the same. Twice in a week they weren’t, both times for the one person who had bothered to write more than once, and it took the notification on my phone to see that the network knew better than its own thread.
Tools: Hugo, PaperMod, Mastodon’s public API, Bluesky’s AT Protocol API, Playwright with Chrome. The widget is one file of plain JavaScript; the scripts around it are specific to this setup and not published.
Comments
Comments are the replies to this post on Mastodon and Bluesky. Reply there and it shows up here.