Skip to content

Releases: blygger/blygger-studio

0.8.3 — a blyg names itself after its own address

Choose a tag to compare

@vgururao vgururao released this 30 Sep 02:20

Migrations: none.

A blyg no longer titles itself "blyg". The default value of a blyg's title
was the literal string blyg, which meant every operator who skipped that
settings field published under the same name. Two unrelated live nodes were
doing exactly that, and the directory at blygger.com listed both as "blyg" —
then briefly held a third submission as a suspected impersonation, queueing a
stranger because of our default.

The default is now derived from the deployment's own address, which is unique
because domains are: a blyg at blyg.example.com titles itself example.com
until its operator says otherwise. A leading blyg. or www. is dropped —
neither says whose blyg it is. Your own title, once set, always wins; emptying
it returns to the derivation rather than to a blank.

Deliberately not a made-up human name. "Example's Blyg" would be the client
asserting something its operator never said.

npm run init now also writes site_url while it has the domain in hand, so a
freshly provisioned blyg has correct absolute URLs and a distinctive title from
its first publish. Re-running init never overwrites a title you have set.

There is an advisory for other client authors at
blygger.org/start/:
a default identical across installations destroys information, and the
deployment usually already knows a truer answer.

npm run upgrade now moves between releases, not to the tip of main. The
two halves of the version story disagreed: the studio's update alert compares
against the GitHub releases feed, while the upgrade script merged main. An
operator could upgrade, land on unreleased commits, and still be told they were
current — and CLIENT.version on main between releases is the previous
release's number, so "what am I running?" had no meaningful answer. It merges
the newest v* tag now, which is the only thing that carries a changelog entry
and therefore the only thing that can tell you whether there are migrations.
Tracking main remains a legitimate choice; it is just not what the script
does, and the README says so.

0.8.2 — a thread is named in its author's own words

Choose a tag to compare

@vgururao vgururao released this 30 Sep 02:04

Migrations: none. Presentation only; no published document changes.

A thread is named in its author's own words. A thread's content_html
contains other people's writing, baked in as transclusion blockquotes. That is
right on the thread's own page, where a quote is shown as a quote with a
provenance line under it. It was wrong everywhere the client had to name the
thread in one line, because flattening that HTML to text drops the structure
that made the attribution legible.

Measured on a live node before the fix: of 19 published threads, 5 opened
with a transclusion
— which is the shape the stub action prefills, so it is
the common case. For those five, the browser tab, the search-result heading,
the social card, the RSS headline and the feed-page excerpt were all someone
else's sentence presented as the author's. Others ran the author's prose
straight into a quote mid-excerpt with no boundary, so a card read as one
continuous paragraph by one person when it was two people.

Fixed in one place and used by all four surfaces: the feed card, the page
<title>/og:title/description, the RSS headline and the archive row. A
thread's card also gained a quote count (⧉2), which is what now says the
item is longer than the teaser; and a thread that quotes without adding
anything of its own names what it answers instead of borrowing the quoted
sentence.

Nothing here changes the wire. content_html, the item document, the feed
description and the static export all keep the quotes, and the thread's own
page still renders them in full with their provenance.

Also fixed: a thread mixing whole and partial transclusions mis-paired its
provenance lines, because the injector matched the class attribute as a literal
string — so a partial quote got no line and every following line shifted onto
the wrong quote.

0.8.1 — partial transclusion

Choose a tag to compare

@vgururao vgururao released this 29 Sep 16:22

Migrations: none. versions.transclusions is JSON text, so the new member
below needs no schema change and every document published before this release
stays valid and unchanged.

Partial transclusion — quote a passage instead of the whole item.
Spec §16.4, decision #49. The medium had three registers of borrowing and only
two of them were writable: transclude the whole item for commentary (the stub),
or fork from a pin for a derivative. Quoting a passage as the thing you are
responding to — the common blogging norm — was the missing rung.

The grammar is adjacency. A ![[id]] directive immediately followed, with
no blank line, by a markdown blockquote is a partial transclusion, and the
blockquote is the passage:

![[7c9wk2mhq0v3xj8tn5rzfd41bg]]
> Stigmergy is what a protocol looks like from inside, and the
> reason it looks like nothing at all is the point.

Commentary begins after a blank line.

A blank line detaches it. That is deliberate and it is why there is no new
sigil: transcluding an item whole and then quoting a bit of it yourself has
been writable since 0.1, and nothing anyone has already written changes meaning.

The passage must really be in the target. At publish, the selection must be
a substring of the target snapshot's text content at the version being baked —
tags stripped, whitespace collapsed within a block, block boundaries kept as
line breaks — else a publish error, exactly like an unresolvable directive. A
quote that welds two of the source's paragraphs into one sentence is refused,
because the source has a break there and the quote does not.

In the studio. Highlight a passage in the reading view and press
quote ↗: you land in the editor with the directive and the passage already
attached, and the passage is checked while you are still choosing it rather
than at publish. Stubbing a long item now prefills an empty quote line instead
of the whole-item form — a suggestion, not a rule; delete the line and the
whole form publishes as before.

On the page, a partial quote says "excerpt of v2" where a whole
transclusion says "snapshot of v2". Without that a reader cannot tell a part
from the whole: a short quote and a short item look the same.

On the wire, the transclusions[] entry gains an OPTIONAL selector in
the W3C text-quote shape (exact, with short prefix/suffix). A reader
that ignores it entirely stays conformant
— the passage is baked into
content_html like any other transclusion, the relation is still
transclusion, staleness is unchanged, and mention verification ignores it as
it ignores cited. The bake carries class="blyg-transclusion blyg-partial",
and the second class is how a reader knows this is a part.

This is a 0.3 revision, not a new protocol version: nothing a reader or a
receiver does changes. PROTOCOL_VERSION stays "0.3".

Also fixed: a thread mixing whole and partial transclusions mis-paired its
provenance lines, because the injector matched the class attribute as a literal
string. Only reachable with a partial in the thread, so no published document
is affected — but the failure mode was attributing one origin's words to
another's, which is worth naming.

0.8.0 — the two-pane reader, titles that agree, and a studio that fits a phone

Choose a tag to compare

@vgururao vgururao released this 29 Sep 15:22

Migrations: one — 0012_responses_default.sql. It adds a nullable
per-item override for the responses list; the backfill is written so that an
upgrade changes nothing that is currently visible on your pages (see below).
Run npm run upgrade, or apply migrations and deploy as usual.

No wire changes. Everything in this release is presentation, studio
behaviour or packaging. PROTOCOL_VERSION is unchanged and no published
document is affected.

Reading

A two-pane reader. Sources on the left, one stream on the right, with
per-source filtering and "Add feed" at the top. Subscriptions left the nav
because the list of them is now where you read them — the page stays, and owns
pause, resume, resync, delete, blogroll membership and poll diagnostics, all of
which the sidebar deliberately does not try to hold.

Reading entries show the item's address, not an "open" label. Several
origins stubbing one item were indistinguishable from each other: the body is
what they share and the origin is what they do not.

The entry's controls are split by what they do. Composition (stub,
fork, and the new link post, which starts a fragment containing [[id]])
sits apart from the rest (copy [[id]], copy url, the address). stub ↗
remains the one control that means "I am responding".

Titles

A titled item is now named the same way on every surface that names it. A
leading heading becomes the linked title on the feed page, in the studio
reader, on the permalink, and — new here — in the archive listing and in
the page's own <head>. Before this, the archive ran the heading into the body
("On Protocols Protocols are the thin layer…") and so did every social card.
Items remain titleless on the wire (§5.3): all of this is derivation from the
item's own first block, never a new field.

Social cards

The head has carried description, og:* and twitter:card since 0.4.1. This
release fixes what they said: og:title is the declared heading where there
is one (and carries no site suffix — og:site_name is the tag that says
where), and the description is taken from what follows the heading rather than
repeating it. The pinned-version page and the archive gained an og:image;
a pinned page uses the blyg's avatar rather than the item's current
attachments, because its whole promise is the bytes from when it froze.

Studio chrome

The nav is a top menu. Sections are real targets with a hover state and a
filled current tab; public page and log out are a separate group. Below
640px the bar collapses behind a hamburger.

A mobile pass over every studio and public page at phone width. Horizontal
overflow is gone (a single pasted URL used to set the page's minimum width, so
every page scrolled sideways), tap targets are ~44px where they were 18–26px,
and the reading sidebar collapses behind a control that names the source you
are filtered to.

Both collapses are gated on a marker that only a scripted browser sets, so a
browser with JavaScript off gets the full navigation rather than a button that
does nothing.

Settings

A timezone for displayed dates. A Worker's clock is UTC, so an evening post
could show tomorrow's date. The picker is filled by your browser's list of
zones and preselects your device's. The wire is unchanged and tested — feed
dates stay RFC-822 in GMT and item documents ISO-8601 UTC; this is what a human
reads on the page.

A global default for whether items show their responses, overridable per
item.
The old column was two-valued, so "off" and "no opinion" were the same
row and a default could never take effect. Migration 0012 adds the override.
The backfill is conservative on purpose: existing explicit opt-ins become
hard overrides and everything else inherits a default that is off, which
reproduces exactly what your pages show today. An upgrade that newly exposed
other people's responses on someone's pages would be a bad day.

Update alerts, on by default. The studio compares its own CLIENT.version
against the public releases feed and says when you are behind. Nothing about
your deployment is sent — it is a version comparison against a feed, not a
check-in. Dismissable, and switchable off in settings.

Packaging

npm run init and npm run upgrade. init provisions a new deployment
idempotently, picks the Cloudflare account explicitly even when there is only
one, and never sees your owner password (COOKIE_SECRET is generated and piped
on stdin). upgrade shows what is coming, calls out changed migrations, keeps
your wrangler.jsonc on conflict, and gates on typecheck and tests before
offering to deploy.

The shipped client names no deployment. The committed wrangler.jsonc
carried two Cloudflare accounts, three D1 databases, bucket and worker names,
zones with route patterns, and a comment describing a live production API
surface — a copy of this repo inherited all of it. None of it was a credential
and all of it was already public, so this removes nothing from the world; what
it does is make the artifact honest. Configure your instance in
wrangler.jsonc and nothing under src/; if you ever have to edit src/ to
configure an instance, that is a bug in this client, because it breaks your
upgrade path. Please report it.

0.7.0 — the bracket picker everywhere, and the controls that were missing

Choose a tag to compare

@vgururao vgururao released this 28 Sep 22:38

Migrations: none. Studio UI only — nothing on the wire changes and no published document is affected. Upgrade is a pull and a deploy.

The [[ picker exists, in all three composers

[[id]] has rendered and resolved since 0.6.0, but the only way to find an id for one was to already know it: the picker lived inside the thread editor and fired only on ![[ at the start of a line, while [[id]] is legal inline and in a fragment. The one construct you can write anywhere was the one construct with no way to look anything up.

The palette is now shared by the quick composer, the fragment editor and the thread editor, and it tells the two bracket forms apart:

  • ![[ with only whitespace before it on the line → transclude, inserted over the whole line.
  • [[ anywhere not preceded by ! → link, inserted in place, leaving the rest of the sentence alone.

The directive form is offered in the thread editor only, because only a thread resolves transclusions at publish — in a fragment ![[id]] publishes as literal text, so a picker there would have written a dead line.

The picker pages, and says how many there are

The candidate list was capped at 20 silently, so a blyg with more than 20 quotable items had a picker that simply stopped — indistinguishable from having nothing more to offer. It now states "showing 20 of 63" or "26 matches, all shown", and ArrowDown at the bottom of a partial list fetches the next page instead of sticking.

The thread editor had no discard button

threadEditPage computed the control and never rendered it, so a thread draft could not be discarded and a thread's unpublished changes could not be thrown away. The fragment editor was fine, which is why this survived: every test in the discard block opened the fragment editor. noUnusedLocals is on now, so a control that is built and then dropped is a compile error.

If you run a node: this is the fix most worth having. It affects any thread you started and wanted to abandon.

Reading entries link out, and hand you a reference

Each entry now carries open ↗ — read it where it lives — derived from the origin's own declared page where it has one, the f/·t/ convention only as a fallback, and for a legacy RSS entry the anchor its feed supplied.

Beside it, copy [[id]] puts the link construct on your clipboard, offered only where the link would actually resolve at publish. This is deliberately not a peer of stub ↗: a link declares nothing on the wire, so it is citing without responding, and stub ↗ remains the one affordance that means "I am responding".

A draft can change its mind about what it is

Both editors offer make this a thread / make this a fragment on a never-published draft. The composer's toggle always worked by deleting and recreating, which is safe only because the text is still in the textarea; past the Full Editor door the draft has attachments, TK scopes and a save history, so the row changes in place instead.

Never-published only — once an item is published its kind is a field readers have and history records. A withdrawn item counts as published. A stub thread is refused rather than silently losing its citation.

Nav reordered

reading, compose, hoppers, mentions, subscriptions, settings, syntax. The order follows the shape of a session rather than the order the features were built in.


Verification: 578 tests, tsc --noEmit clean, and every change above exercised by hand against a running node.

0.6.1 — a republish stops re-notifying every origin it quotes

Choose a tag to compare

@vgururao vgururao released this 28 Sep 20:06

⚠️ This one has a migration

git pull
npm ci --legacy-peer-deps
npx wrangler d1 migrations apply <your-db> --remote
npm run deploy

Every changelog entry in this project states Migrations: explicitly, and this is the first release where that line is not "none".

What it fixes

Spec 0.3 §15.2 says a republish re-sends a Webmention "only for references that are new or whose target version changed", and goes on to say that "the reference client records the target version per outbound reference for this purpose". It did not. The outbound queue is keyed (item_id, target) and held only the publisher's version, so every publish reset every row to pending — fixing a typo in a thread re-notified every blyg that thread quoted. Harmless when the network was two nodes nobody had heard of; rude at eleven.

  • The queue now stores target_version, and delivery state resets only when the row is new, when the target's version actually changed, or when the caller forces it.
  • The change test is null-safe (IS NOT, not <>), because a {url} stub has no target version and two nulls have to read as unchanged — otherwise a stub of a plain web page would re-send on every republish forever.
  • Withdrawal forces a re-send, and is the only caller allowed to. §15.7 owes the receiver one mention precisely because nothing about the target changed: it re-verifies, finds a withdrawn document, and marks the mention gone. Without that override the new rule would have swallowed the one notification a withdrawal exists to send.
  • Rows written before the migration have a null target version, and null→value counts as a change, so each pre-existing row re-sends at most once. §15.2 allows this in as many words — "a sender that re-sends everything on every republish is conformant but noisy" — and one noisy round beats a silent wrong answer.

If you run a blyg that other people quote, this release is the difference between your subscribers' edits reaching you as news and reaching you as noise.

531 tests, tsc --noEmit clean. Reasoning in CHANGELOG.md.

0.6.0 — plain links, citations on the wire, and an honest level

Choose a tag to compare

@vgururao vgururao released this 28 Sep 19:33

The three constructs protocol 0.3 records in its §16 as ruled but not yet built. The project's rule is that testing precedes prose, so this release is what lets that section become normative text rather than a promise. Protocol 0.3, level 2.

[[id]] — a plain internal link

Inline anywhere in a document, one ! away from the ![[id]] transclusion directive and a different act entirely: it resolves by the same order, renders as an ordinary anchor to the target's own page, and notifies nobody. No transclusions[] entry, no Webmention, no class on the wire. In a medium where every other way of citing tells the other side, this is the one that does not — which is the point of it. An unresolvable link fails the publish, like an unresolvable directive.

cited — a reference's human half, frozen

A reference carries identity and no words, so a reader whose target had disappeared was shown a 26-character id and nothing else. Now stub_of, each remote transclusions[] entry and forked_from may carry {source, author?, excerpt?, url, retrieved}, frozen at the moment the reference was made, on live and pinned documents.

It is additive and optional. Own-origin transclusions are byte-identical to before, so every 0.2 document is still a valid 0.3 document, and a client that ignores cited stays conformant. It is self-asserted and never authoritative: never read by mention verification, never rendered into content_html, never presented as the target's current state.

Two things came with it. The provenance byline under a baked remote quote used to be rendered from a live database join, so renaming a subscription silently rewrote what an already-published document said about its source — it now reads the frozen citation, because a citation that changes after publication was never a citation. And an imported document's own cited values are kept verbatim rather than recomposed from local guesses.

generator_url, and level 2

The manifest now carries one absolute URL to this client's source, derived from the same constant as generator. SHOULD, never MUST, and no reader may gate on it. It exists for a measured reason: five of the seven live client implementations in this ecosystem have no locatable repository, and the manifest is the one file the protocol requires to be public — so it is the only channel by which a directory could point an operator at a release page.

level moves from 1 to 2. Nodes were publishing 0.3 constructs while announcing level 1; §3 of 0.3 defines L2 as this specification. Readers may not gate on the level, so this is honesty rather than compatibility.

Migrations: none. Upgrading is a redeploy:

git pull
npm ci --legacy-peer-deps
npm run deploy

If your copy came from blygger-spec/worker/, see 0.4.1's notes — git pull will not reach this repo.

528 tests, tsc --noEmit clean. Reasoning in CHANGELOG.md.

0.5.0 — Webmention becomes optional, as the spec always said it was

Choose a tag to compare

@vgururao vgururao released this 28 Sep 19:05

Companion to 0.4.1, which hardened the Webmention endpoint. This release lets you not run one at all.

Protocol 0.3 §15 is OPTIONAL at every level, and §15.1 says the endpoint is advertised "only when mentions are accepted". This client did not implement that: it served the endpoint unconditionally, so a node stood up by following blygger.org/start/ got an unauthenticated public POST surface whether or not its operator wanted one. The intent was even in the code — buildManifest takes a webmention: false option that no caller ever passed.

  • Settings → Accept Webmentions, on by default: nothing changes for an existing node until you change it.
  • Off means withdrawn, not guarded. The manifest omits its webmention key, your pages omit both the <link rel="webmention"> element and the Link header, and a POST to the endpoint returns 404 — the same answer a static export of your blyg gives. A 403 would advertise an endpoint with a policy; there is no endpoint.
  • You still send mentions. When you quote, stub or fork someone else's item, your blyg still tells them. The two halves were always independent. Responses you have already collected stay in your studio.
  • If you leave it on, 0.4.1's rate limits apply: per source host, per registrable domain, and per endpoint per hour.

Migrations: none — settings are key/value rows in a table that already exists. Upgrading is a redeploy:

git pull
npm ci --legacy-peer-deps
npm run deploy

If your copy came from blygger-spec/worker/ — a node stood up before 2026-09-28 — git pull will not reach this repo: the client left that one by git subtree split, so the histories are unrelated and worker/ no longer exists there. Clone this repo fresh and carry over your wrangler.jsonc (D1 database_id, R2 bucket, routes, vars). Secrets, database and bucket are untouched by an upgrade.

515 tests, tsc --noEmit clean. Full reasoning in CHANGELOG.md.

0.4.1 — Webmention endpoint hardening

Choose a tag to compare

@vgururao vgururao released this 28 Sep 18:47

Security release. Recommended for every live node.

POST {mount}/webmention is the only unauthenticated public endpoint in this program, and the origins that advertise it are now published in a public directory — so the defaults shipped here are the ones strangers find. Protocol 0.3, unchanged. Level 1, unchanged.

  • The rate limit now also counts the registrable domain, at 120/hour, alongside the existing per-host cap of 60/hour. Per-host counting was defeated by wildcard DNS: a.spam.example and b.spam.example are different hosts, so a flooder paid one DNS label per 60 accepted claims. The domain cap is the looser of the two on purpose — the grouping is a documented heuristic, and a hosting suffix it does not know about would otherwise cap every site behind that suffix collectively.
  • A global cap of 300 accepted claims/hour on the endpoint as a whole. No per-source limit bounds a total: fifty domains sending 119 each sat inside every previous cap while spending up to ~11,900 outbound fetches at URLs strangers chose, on the deployer's Cloudflare account. Once this cap binds, further new claims in that window are refused, so the 429 now carries Retry-After.
  • failed inbound claims are deleted after 30 days, on the existing cron. They previously accumulated forever, which made an endpoint anyone can POST to into an unbounded write surface. verified, gone and pending rows are untouched.

Migrations: none. Every cap counts columns that already exist, so upgrading is a redeploy with no database step.

Upgrading

git pull                      # see the note below if your copy came from blygger-spec
npm ci --legacy-peer-deps
npm run deploy

If your copy came from blygger-spec/worker/ — i.e. you stood your node up before 2026-09-28 — git pull will not bring you here: the client left that repo by git subtree split, so this history is the same content with different commit ids, and worker/ no longer exists there. Clone this repo fresh and carry over your wrangler.jsonc (D1 database_id, R2 bucket, routes, vars). Your secrets, database and bucket are untouched by an upgrade.

Full reasoning in CHANGELOG.md, and in self-host-plan.md §9.1.

A note on how you heard about this

You probably didn't — there is no notification channel yet. Watching this repo's releases is currently the only mechanism, which is a gap we are fixing from the directory side (roadmap item 3.1). If you run a blyg, blygger.com now takes an optional contact when you submit, used for exactly this and never published.