<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>comments – ./blog.sh</title>
    <link>https://blogsh.app/tag/comments/</link>
    <atom:link href="https://blogsh.app/tag/comments/rss.xml" rel="self" type="application/rss+xml" />
    <description>Posts tagged comments on Daniel Šnor&#39;s personal site.</description>
    <language>en</language>
    <lastBuildDate>Fri, 04 Sep 2026 16:52:00 +0200</lastBuildDate>
    <item>
  <title>Where a reader carries a post</title>
  <link>https://blogsh.app/posts/2026/where-a-reader-carries-a-post/</link>
  <guid isPermaLink="true">https://blogsh.app/posts/2026/where-a-reader-carries-a-post/</guid>
  <pubDate>Fri, 04 Sep 2026 16:52:00 +0200</pubDate>
  <description><![CDATA[<p>A post gets read, and then it goes somewhere: into a toot, a message, an email to one person. Until now the reader did that by copying the address out of the bar. 1.6 puts a row of controls under the post for it — off unless <code>share:</code> names what you want, drawn in the order you name it.</p>
<pre class="code-block"><code class="language-yaml">share:
  - mastodon
  - bluesky
  - email
  - copy
  - system</code></pre>
<h2 id="what-is-prefilled-and-what-is-not">What is prefilled, and what is not</h2>
<p>The post&#39;s name and its address, where the target takes both. Facebook and LinkedIn take the address alone and read the name off the page. The reader writes the part that is theirs. <code>bluesky</code>, <code>email</code>, <code>facebook</code>, <code>linkedin</code>, <code>threads</code> and <code>x</code> are plain links, one address each, no script behind them.</p>
<h2 id="three-that-are-not-links">Three that are not links</h2>
<p>The fediverse has as many addresses as it has instances, and a page cannot know which one is the reader&#39;s. So <code>mastodon</code> is a button that asks — in a row that opens under the controls, in the site&#39;s own type — and remembers the answer in that browser afterwards. Nothing about the reader leaves the page; the instance name is kept where they typed it.</p>
<p><code>copy</code> puts the address on the clipboard. <code>system</code> hands the post to the operating system&#39;s own share sheet, which on a phone is Signal and WhatsApp and Telegram at once, and on a desk is whatever the system offers.</p>
<p>Those three appear only where they can work. A browser without a clipboard API gets no copy button, a desk without a share sheet gets no system button, and a block left with nothing to draw hides itself rather than standing a heading over an empty row.</p>
<h2 id="why-there-is-no-pixelfed">Why there is no Pixelfed</h2>
<p>Pixelfed has no address a page can hand a post to. A button that opened nothing would be worse than none, so there is none, and the changelog says so under &quot;not fixed, on purpose&quot; rather than leaving you to wonder.</p>
<h2 id="one-new-file">One new file</h2>
<p><code>assets/js/share.js</code> is a file every page now loads. On a site that never sets <code>share:</code> it does nothing at all; it is still a file, and a deploy that misses it leaves a 404 in the console. <code>git pull</code>, rebuild, deploy — and it is there.</p>
<p>The controls make no request to anyone until the reader presses one. That is the rule for everything the engine puts on a page, and this row is no exception: the share buttons of the web are usually the one thing on a page that phones home before you touch them, and these do not.</p>]]></description>
  <category>appearance</category><category>comments</category>
</item>
<item>
  <title>The same language, spoken differently</title>
  <link>https://blogsh.app/posts/2026/the-same-language-spoken-differently/</link>
  <guid isPermaLink="true">https://blogsh.app/posts/2026/the-same-language-spoken-differently/</guid>
  <pubDate>Wed, 26 Aug 2026 16:32:00 +0200</pubDate>
  <description><![CDATA[<p>Comments here are replies to a toot, and GoToSocial speaks Mastodon&#39;s API — so comments on a GTS instance should have simply worked. They did not, not once, and nobody saw it fail: arch-linux.cz, the first site to pair this engine with its own GTS instance, had never shown a single comment under any post. My own tests had Mastodon on both ends, so they could never have caught it.</p>
<h2 id="dialect-is-not-language">Dialect is not language</h2>
<p>Two accents were in the way. GoToSocial writes a status address in its own shape, and the engine&#39;s pattern insisted on Mastodon&#39;s — a numeric id where GTS answers with a 26-character ULID. And GTS requires a token even to read a public thread, where Mastodon hands it to anyone who asks. The first accent broke the address parsing; the second broke the entire idea of live comments, which is a visitor&#39;s browser fetching the thread — a browser you cannot give a token to.</p>
<h2 id="moderation-is-the-door-in">Moderation is the door in</h2>
<p>So on GoToSocial, comments are the moderated kind by construction: <code>comments.approval: fav</code>, and the cron reads the thread with the token, server-side, publishing the replies you star. The side effect is one Mastodon sites choose deliberately: visitors&#39; browsers stop contacting your instance at all.</p>
<p><code>doctor --online</code> now tests the capability, not the brand — can an anonymous request read the thread? — so a Mastodon instance in secure mode gets the same honest answer. And the fix was confirmed where the bug was found: the first starred reply on arch-linux.cz appeared under its post the same day 1.4 landed there.</p>]]></description>
  <category>comments</category>
</item>
<item>
  <title>Star it to publish it</title>
  <link>https://blogsh.app/posts/2026/star-it-to-publish-it/</link>
  <guid isPermaLink="true">https://blogsh.app/posts/2026/star-it-to-publish-it/</guid>
  <pubDate>Wed, 19 Aug 2026 16:32:00 +0200</pubDate>
  <description><![CDATA[<p>The comments under these posts are replies to a toot. That has been the whole system since the beginning, and its one weak spot was always that it is all or nothing: every reply appears, or comments are off.</p>
<p><code>comments.approval: fav</code> is the third answer. A reply appears under the article once I favourite it, from whatever client I happen to have open.</p>
<h2 id="no-queue-no-dashboard">No queue, no dashboard</h2>
<p>That is the point of doing it this way. A moderation queue is a second interface, with a second login, that you visit specially — which means you visit it late, or not at all. Here the interface is the app the comments already live in. I read the reply where I was going to read it anyway, and approving is one tap in that same place.</p>
<p>Two rules keep the result readable rather than merely filtered. My own replies need no star, because nobody stars themselves and half of every exchange would otherwise vanish. And a reply is shown only if everything between it and the announcement is shown too — an approved answer to a rejected comment answers nothing.</p>
<h2 id="what-it-costs-and-the-costs-are-real">What it costs, and the costs are real</h2>
<p><b>A favourite is public.</b> On both networks. Approving is also endorsing in the eyes of anyone who looks, and every star handed out over the years is retroactively an approval. A private signal — bookmarks — would not carry that, and is the obvious second mode if this one chafes.</p>
<p><b>The moment you switch it on, every comment on the site disappears.</b> Moderation publishes what has been starred, and at that moment that is nothing. A site with years of replies goes quiet in one rebuild. Plan the switch for an hour when you can go and star the keepers, and run <code>./scripts/refresh-sidebar.sh --full</code> afterwards to settle the old posts on the spot instead of within the week.</p>
<p><b>Approval is not instant.</b> Up to one cron interval — and up to a week under a post older than about three months, since old posts are only refreshed occasionally. &quot;Did <i>I</i> favourite this?&quot; is a question only an authenticated request can ask, and the token cannot be shipped to a browser, so cron reads the thread and writes the answer; the page renders from that. <code>./scripts/refresh-sidebar.sh --full</code> does it now instead of eventually.</p>
<p><b>It makes an older promise on this site conditional.</b> There is a post here saying there is no comment database — nothing to migrate, back up, or moderate at 2am. With approval on, only the last third of that stays true. What has been starred is written to a file the pages read, rewritten from the source on every cron run, with no interface anywhere that can edit it: a cache rather than a database, but other people&#39;s words on my disk all the same.</p>
<p><b>And somebody&#39;s deletion can outlive itself here.</b> A reply deleted at the source leaves this site at the refresh cron&#39;s cadence rather than immediately — under an old post, up to a week later. <code>--full</code> settles it on the spot, but the lag is part of the price rather than a setting, and it is the part that is paid by someone other than me.</p>
<p><b>It keeps replies off the blog, not off the network.</b> The thread stays public and this site still links to it. Block, mute and report remain the tools for the rest.</p>
<h2 id="the-side-effect-nobody-asks-for">The side effect nobody asks for</h2>
<p>With the thread already read on the server, the visitor&#39;s browser stops contacting the network at all — and the page&#39;s content security policy drops its grant to the instance along with it. The live thread was the one client-side exception to <i>no third-party requests from the visitor&#39;s browser</i>. A moderated thread is not live, so the exception lapses with it.</p>
<p>Avatars still come from the instance hosting them. Everything else arrives from this domain.</p>
<h2 id="turning-it-on">Turning it on</h2>
<pre class="code-block"><code class="language-yaml">comments:
  approval: fav      # or &quot;off&quot; (the default)</code></pre>
<p>Three things have to be true, and <code>./blog.sh doctor --online</code> says so when they are not: the sidebar cron has to be running, a Mastodon token needs <code>read:statuses</code> alongside <code>write:statuses</code> — a token without it gets a perfectly good answer with the <code>favourited</code> field left out, which reads as &quot;approved nothing&quot; — and you have to go and star the ones worth keeping. On Bluesky the app password you already have is enough.</p>]]></description>
  <category>comments</category>
</item>
<item>
  <title>Why not X, and why Threads waits</title>
  <link>https://blogsh.app/posts/2026/why-not-x/</link>
  <guid isPermaLink="true">https://blogsh.app/posts/2026/why-not-x/</guid>
  <pubDate>Fri, 31 Jul 2026 22:17:21 +0000</pubDate>
  <description><![CDATA[<blockquote>Post read: $0.005 per resource. Post creation: $0.015 per request — $0.200 when it carries a URL. Free public read access: none.<cite>— X API pay-per-use pricing, docs.x.com, checked July 2026</cite></blockquote>
<p>Two networks are in. Two are not — and here&#39;s the whole reasoning, because &quot;we decided against it&quot; is not a reason.</p>
<p>./blog.sh announces every published post on Mastodon or Bluesky, and the replies to that announcement become the post&#39;s comments. For that to work, the engine needs two things from a network: a way to post, and a way for a <i>browser</i> to read a public thread. Cheap, ideally free, forever — because a personal blog is a decades-long project with a hobby budget.</p>
<h2 id="x-no">X: no</h2>
<p>Look at that price list again. Announcing a post with a link costs real money. Loading the reply thread — which happens every time anyone scrolls to the comments — costs real money, per read, forever. On a personal blog, that&#39;s a subscription to your own comment section, billed by your readers&#39; curiosity.</p>
<p>There&#39;s no engineering answer to a pricing decision. So: no.</p>
<h2 id="threads-not-yet">Threads: not yet</h2>
<p>Threads is genuinely feasible, and the design is sitting in a drawer. But look at what &quot;feasible&quot; means here:</p>
<blockquote>Publishing and reading replies to your own posts works — server-side only, through a registered Meta developer app, with OAuth and 60-day tokens.<cite>— the Threads API docs, summarized</cite></blockquote>
<p>In practice that&#39;s a cron job refreshing tokens so they never silently expire, and comments cached into JSON with roughly a half-hour delay — instead of the live thread Mastodon and Bluesky hand any browser for free. It&#39;s all buildable. It&#39;s just a lot of standing machinery for a network nobody has asked me about yet. The day someone does, the drawer opens.</p>
<h2 id="scraping-threads-no-and-firmly">Scraping Threads: no, and firmly</h2>
<p>The tempting shortcut — skip the API, scrape the web app — fails three ways at once. The app&#39;s internals change constantly and no community project maintains a stable interface to them. Meta blocks datacenter IPs and forbids automated collection in its terms, so shipping a scraper in a community engine means handing every user a feature that breaks without warning and can put their account at risk. And the paid scraping services fail the same test with extra steps: per-request pricing, your data flowing through a third party, and a dependency on someone else&#39;s ongoing legal cat-and-mouse.</p>
<h2 id="the-scoreboard">The scoreboard</h2>
<p>Mastodon and Bluesky are in because they pass the boring test: free to post, free for a browser to read a public thread, no tokens with expiry dates in the critical path. Any network that starts passing it is welcome. Any network that stops — well, now you know the exit criteria too.</p>]]></description>
  <category>philosophy</category><category>comments</category>
</item>
<item>
  <title>Comments I don&#39;t host</title>
  <link>https://blogsh.app/posts/2026/comments-i-dont-host/</link>
  <guid isPermaLink="true">https://blogsh.app/posts/2026/comments-i-dont-host/</guid>
  <pubDate>Fri, 31 Jul 2026 22:17:15 +0000</pubDate>
  <description><![CDATA[<p>The star, boost and reply counters sitting right under this post&#39;s title belong to a toot, not to this site. And the comments at the bottom of this page are that toot&#39;s replies. That&#39;s the whole system — here&#39;s how it works.</p>
<p>When a post is published, the engine announces it on Mastodon or Bluesky — one network per site, never both; the build refuses a config with two, because comments live in exactly one place. Replies to that announcement <i>are</i> the comments: your browser loads the thread straight from the network&#39;s public API, no login, no key, no middleman. The counters under the title are the same numbers, read from the same toot.</p>
<h2 id="what-falls-out-of-that">What falls out of that</h2>
<p>No comment database — nothing to migrate, back up, or moderate at 2am. No spam filter, because the network already has one, and it&#39;s better funded than mine. The comment count is just the reply count. And moderation is your instance&#39;s moderation: block, mute and report all work exactly where the conversation actually happens.</p>
<p>The comments also <i>belong</i> to the people who wrote them, in their own timeline, under their own name. A reply here is a real post by a real account — not a row in my database wearing a nickname.</p>
<h2 id="what-it-costs">What it costs</h2>
<p>Honesty first: if you&#39;re not on the Fediverse or Bluesky, you don&#39;t get to comment. That&#39;s a real cost and I won&#39;t pretend otherwise. You can still read the thread — it&#39;s public — and the bar for joining is an email address. For a personal blog, I&#39;ll take that trade over hosting a login system every day of the week.</p>
<h2 id="two-details-that-earn-their-keep">Two details that earn their keep</h2>
<p><code>unpublish</code> deletes the announcement too. A toot pointing at a dead URL helps nobody, so taking a post down takes its thread anchor down with it.</p>
<p>Imported posts don&#39;t announce. The auto-announcement has a 24-hour window around &quot;now&quot; — so migrating a thousand posts from your old blog doesn&#39;t turn into a thousand-toot night for your followers. (You can still announce any post manually, whenever it deserves it.)</p>
<h2 id="try-it-on-this-very-post">Try it on this very post</h2>
<p>This is the part I can&#39;t fake, which is exactly why it&#39;s the demo. Reply to this post&#39;s toot — what&#39;s missing here, what would make you use this engine, or what convinced you not to — and your reply appears below, hosted by no one, moderated by your instance, owned by you.</p>]]></description>
  <category>comments</category>
</item>

  </channel>
</rss>
