~/blygger.org/spec/0.4/

The Blygger Protocol — Version 0.4

Status: DRAFT. This document specifies protocol version 0.4 at conformance Level 2 — the publish side, the subscribe side, and the cross-client constructs that let one blyg quote, answer, and descend from another. Every spec version numbered below 1.0 is a working draft: the reference-implementation development phase is a testing phase for the protocol itself, and the spec changes in response to what testing discovers. Version 1.0 will be the first version its authors stand behind as stable and publish to a larger audience; until then, users of this spec and builders of implementations should assume no promises — including of the wire surface. Breaking changes are permitted pre-1.0 but must bump the manifest version and be recorded in the project devlog.

Each protocol version has its own standalone-complete document, and the highest-numbered document is the living one. This document supersedes version 0.3: it carries the full 0.3 text forward, revised, with its section numbers, and adds the constructs the 0.3 text held in §16 as ruled shapes until they were built and exercised — remote generation sources, which make a generation source a reference that notifies its origin (§5.7, and the fourth mention relation source in §15.4); the manifest-located surface, which lets a manifest say where its files live, so that software that cannot serve fixed .json paths can publish a blyg (§4, §6.1, §12); and the author's label [[id | text]] on links and directives (§10.1). Version 0.3 stops receiving revisions when this document is published, and stays citable with its snapshots intact.

What this document adds, and why now. Every normative construct below was built and exercised between two independent deployments before it was written down here. The two constructs that open this version were ruled on 2026-09-28 as shapes in the 0.3 text's §16 — buildable, not yet normative — and each crossed the line the project draws for a new version: a construct that changes what a reader or receiver must do to be correct, rather than one a conformant reader already ignores safely. Remote generation sources shipped in blygger-studio 0.41.0 and were exercised between the protocol-institute and venkateshrao nodes on 2026-10-10; the manifest-located surface was published through templates by Soapbox, a WordPress plugin not written by this project, and the reference client subscribed to it, quoted it, and sent it a mention that verified on 2026-10-07. Constructs that have been decided but not yet built are described in §16 with their ruled shapes and enter the normative text in a later revision, once a client emits them. That sequencing is the project's rule, not an oversight.

Which version to implement. Implement against the highest-numbered document published at https://blygger.org/spec/ — that is the living one, and this is it. The three places a version of this text can be found are not interchangeable:

What is not the spec: the plan documents (docs/v0.4-plan.md and its siblings) and the reference client's source. Both have carried wire shapes before the spec did — the 0.3 constructs shipped in the reference client twelve days before the 0.3 document existed — and several independent clients were built from them in that window. That was a lag in this project's process, not a stable arrangement. Since 0.3, a construct that has been ruled but not yet built appears in §16 of the living document with its exact shape and an explicit "not yet normative" label, so that the spec is always the first place a shape is visible.

The risk of the living text is that it moves: pre-1.0, every version is a draft and no wire promise is made (§3.1's version key is informative for the same reason). The risk of a superseded text is only that it is incomplete: levels are strict supersets and every 0.3 document is a valid 0.4 document, so a client built to 0.3 remains conformant and simply does not emit or understand the 0.4 constructs. Declare what you implement in the blyg key, and readers will treat the rest under the ignore rules.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY are to be interpreted as described in RFC 2119.


1. Introduction (non-normative)

Blygger is a decentralized public writing medium built on static files and RSS. A blyg is a directory of files — mounted anywhere on the publisher's own domain, at a path (conventionally /blyg/) or at the domain root — containing versioned items of writing, a manifest, an archive index, and an RSS feed. Anything that can serve files can serve a blyg; anything that can read RSS can follow one.

Four design invariants shape everything below:

  1. The protocol is a static file contract. It governs only the published artifact — the page. How the publisher composes, edits, imports, or generates content (the studio) is entirely out of scope. This includes how an authoring tool writes into a blyg: the protocol specifies no write surface, at this version or any version foreseen (§16).
  2. Every blyg feed is a valid RSS 2.0 feed whose items carry self-contained HTML. A plain RSS reader always sees a sensible microblog.
  3. AI is never in the protocol. Generation, if any, happens in the studio at authoring time; the page publishes output plus provenance (§5.7). Readers need no models or keys. This constrains the wire and the reader side, never the writer: a blyg written, maintained, or entirely operated by a software agent is a blyg like any other, and the protocol cannot and does not tell.
  4. Identity is never in the protocol. The only authenticated entity is the publishing client at its domain (the origin). DNS is the namespace.

The protocol has two planes. The state plane — canonical item files and the archive index — is ground truth and losslessly complete. The notification plane — the RSS feed — is a lossy, windowed signal that something changed. Readers reconstruct state from the state plane; they never treat the feed as authoritative.

Version 0.2 specified the reading path built on that split: a reader handed any URL resolves it (§12) to a blyg origin or a legacy feed, subscribes, and polls; the archive index heals every gap (§13). Subscription is deliberately invisible: no follow object, no follower list, and the only public trace of anyone's reading is the curated blogroll they choose to publish (§11).

Version 0.3 makes threads cross-client. A thread may quote any item a reader has imported, from any origin, by the same ![[id]] directive it uses for its own fragments — and the quote is always the reader's local snapshot, never a live fetch (§10). A thread may declare itself a stub: a response to exactly one target (§10.6). An item may declare lineage from a pinned version of another item (§5.6). And an origin that has been quoted, stubbed, or forked can find out, reliably and verifiably, without subscribing to the quoter: Webmention with structural verification (§15), the notification half of the discovery model whose static half is the blogroll. None of this adds a reply primitive. This remains a network of soapboxes: public speech that cites, and citations that can be checked.

Version 0.4 adds two things the 0.3 text had ruled but not yet built. A generator may be fed another origin's words, and that is disclosed as a source — a reference that notifies its origin as a quotation does (§5.7, §15) — because in a medium whose premise is citations that can be checked, paraphrase by model must not be the one way to use a stranger's words without telling them. And the manifest may say where the surface lives (§6.1), so that a blyg can be published by software that cannot serve fixed .json paths; the one fixed filename is the manifest's own. Neither changes what a 0.3 publisher emits.

2. Terminology

3. Conformance levels

Level Meaning
L0 Any plain RSS feed, grandfathered via a reader-side wrapper (summary fragment + link, §13.6). No blyg constructs.
L1 The single-origin protocol, as specified at version 0.2 and carried forward here unchanged in substance. Publish side: stable ids, versioned items, canonical item files, manifest, archive index, rollup semantics, withdrawal, pins, threads and same-origin transclusion, generation provenance. Subscribe side: resolution, importer conformance, lossless recovery, the optional blogroll.
L2 This specification. Everything in L1 plus the cross-client constructs: transclusion across origins and thread nesting (§10), stubs (§10.6), lineage (§5.6), the page field (§5.8), Webmention (§15), and from 0.4 remote generation sources (§5.7) and the manifest-located surface (§6.1).
L3 (future) Encrypted/permissioned content.

Levels are strict supersets. A conforming reader at any level MUST ignore constructs it does not understand rather than reject the document containing them (this is what lets levels and versions advance without breaking anyone).

A publisher conforms at L1 by serving the surfaces in §4 with the semantics in §§5–11, and at L2 by additionally following §10's cross-client rules and §5.6, §5.8 and §10.6 for any lineage, page, or stub_of it emits. Every L2 construct is additive and optional to emit: a valid 0.3 document is a valid 0.4 document unchanged, and a publisher that never quotes another origin, never stubs, never forks, never draws on another origin's items in generation, keeps the default paths, and receives no mentions is fully conformant at L2 by doing nothing new. Webmention in particular is optional at every level; a static-only deployment advertises no endpoint and remains conformant (§15.7). A reader conforms at L1 by following the rules in §§12–13, and at L2 by additionally honouring the cross-origin provenance rules of §10.3.

3.1 The protocol version key

Manifests and item documents carry a "blyg" key naming the spec version their publisher implements (a deployment serving the 0.4 surfaces emits "blyg": "0.4"). The key is informative, not a negotiation: readers MUST accept any 0.x value and apply the standing ignore-unknown rules (§13.1) to whatever they find. Vocabulary evolves inside one namespace under those ignore rules; a breaking change, if one ever happens, gets a major version bump and its own migration story — never this mechanism.

3.2 The level, generator and generator_url keys are informative

The manifest's level (§6.1) is the conformance level the publisher claims, generator is the name and version of the software that produced the blyg, and generator_url is where that software's source or home page lives. All three are self-descriptions. Readers MUST NOT gate any behaviour on any of them — not parsing, not feature selection, not trust. A reader decides what a document contains by reading the document, under the ignore-unknown rules; a reader that would render, verify, or import differently because of a generator string is treating an unverified label as a capability, and would break the moment a client renamed itself.

generator nonetheless carries real weight, which is why publishers SHOULD emit it in the form name/version (e.g. blygger-studio/0.4.0): the manifest is a file the protocol requires to be public, so generator is the only census of the ecosystem that exists without anyone registering anything. A client with no other public trace is counted by this key alone. It is the client's name, never the protocol's: a client rename is not a protocol change and MUST NOT be read as one.

4. The publication surface

A blyg is the following file tree under its origin. The origin's location is the publisher's free choice — a domain root (https://example.com/), any path (https://example.com/blyg/, https://example.com/notes/b/), or a subdomain. The surface is strictly origin-relative: no protocol construct may assume any particular path component, and readers MUST NOT infer anything from the mount path. One file name within the surface is protocol-fixed: the manifest, blyg.json, at the origin — a known manifest filename at an arbitrary origin is what keeps free mounting discoverable (a reader handed any base URL fetches blyg.json relative to it). Every other path below is the default, and the manifest MAY relocate it (§6.1; new in 0.4, promoted from the 0.3 text's §16.6e): feed and items name the feed and the archive index, and the item and pin templates say where item documents and pinned-version documents live. A blyg that omits those keys uses the defaults, and every path in this document is written in its default form. Wherever this document writes {origin}items/{id}.json or {origin}items/{id}/v{n}.json, a reader expands the origin's item or pin template instead when the manifest carries one — the literal is the default. At 0.3 every name was fixed. That rule was written for filesystem-shaped deployments, and it excluded a large class of publishers for a reason unrelated to publishing: managed hosts intercept .json and .xml paths as static files before a CMS routes them, so a WordPress site, for one, could not serve items/{id}.json without rewrite rules that shared hosting often forbids. /blyg/ is the reference client's default mount and the convention used in examples throughout this document; it carries no protocol meaning.

Publishers MUST serve:

Default path (relative to origin) Content Spec
blyg.json Manifest §6
feed.xml RSS 2.0 feed §7
items/index.json Archive index §6.2
items/{id}.json Canonical item document §5
items/{id}/v{n}.json Pinned version document (only for pinned versions) §8
media/… Media objects referenced by items §5.4

Publishers MAY additionally serve:

Default path (relative to origin) Content Spec
blogroll.opml Curated blogroll (OPTIONAL) §11
(the URL named by the manifest's webmention key) Webmention endpoint (OPTIONAL; the protocol's only dynamic surface) §15

Publishers SHOULD additionally serve human-readable HTML (a feed page, item permalink pages); their form is presentation, not protocol, except where noted (§5.8, §8.4, §10.5, §15.1). The reference client's permalink paths — f/{id}/ for fragments, t/{id}/ for threads — are a convention, not a requirement: an item's permalink is whatever its document's page field says (§5.8).

Requirements:

5. The item document — items/{id}.json

Ground truth for one item. Example (a fragment):

{
  "blyg": "0.3",
  "id": "7c9wk2mhq0v3xj8tn5rzfd41bg",
  "kind": "fragment",
  "origin": "https://example.com/blyg/",
  "page": "f/7c9wk2mhq0v3xj8tn5rzfd41bg/",
  "author": { "name": "Venkatesh Rao", "url": "https://example.com/blyg/" },
  "created": "2026-07-17T18:00:00Z",
  "updated": "2026-07-18T09:30:00Z",
  "version": 3,
  "content_md": "Markdown source of the *latest* version.",
  "content_html": "<p>Markdown source of the <em>latest</em> version.</p>",
  "content_hash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "media": [
    { "url": "media/x7f2.png", "mime": "image/png", "alt": "a diagram" }
  ],
  "changelog": [
    { "version": 1, "at": "2026-07-17T18:00:00Z", "note": null },
    { "version": 2, "at": "2026-07-17T21:12:00Z", "note": "typo", "pinned": true },
    { "version": 3, "at": "2026-07-18T09:30:00Z", "note": "sharpened the claim" }
  ]
}

Threads additionally carry transclusions (§10.3) and MAY carry stub_of (§10.6). Any item MAY carry forked_from (§5.6) and generated (§5.7).

Members (revision of 2026-10-17, decision #76 — the first explicit list, drawn from the sections below and checked against one document from every live blyg, all 22 of which already conform):

Member Status Where
blyg, id, kind, origin, version, created, updated, content_md, content_html, content_hash, changelog REQUIRED §3.1, §5.1, §5.2, §5.3
media REQUIRED; an array, MAY be empty §5.4
page RECOMMENDED §5.8
author OPTIONAL; absent means no assertion §5.5
transclusions REQUIRED for threads, absent for fragments §10.3
stub_of, forked_from, generated OPTIONAL, present when the construct is §10.6, §5.6, §5.7

A withdrawal endcap (§9) keeps the REQUIRED members with empty content. Any other member is unknown and ignored (§13.1 rule 4).

5.1 Identity

5.2 Versioning and rollup presentation

5.3 Kinds

5.4 Media

5.5 The author field

author is OPTIONAL, per-item, client-asserted, and opaque:

5.6 Lineage — forked_from

(Reserved at 0.1 and 0.2; defined at 0.3.) An item MAY carry a top-level forked_from: a reference (§5.9) to the pinned version of another item from which this item's first draft was copied.

"forked_from": { "origin": "https://blyg.protocol-institute.org/",
                 "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 2 }

Rules:

  1. origin is REQUIRED, even when it is the publisher's own — a citation is absolute. (The 0.1 reservation's two-field form {id, version} is amended additively; nothing ever emitted it.)
  2. The referenced version MUST be pinned at its origin, i.e. fetchable as {origin}items/{id}/v{n}.json. A pin is the only version anyone can promise a lineage still points at (§8 rule 1). Publishers SHOULD check this at publish time with one conditional fetch, and SHOULD refuse to publish on evidence that the promise is not kept — a 4xx, or a 200 that is not that pinned version. A network failure, timeout, or 5xx is evidence of nothing; refusing to publish an author's own words because a stranger's host is down would hand third parties a veto, so publishers SHOULD proceed with a warning instead.
  3. Lineage is fixed when the item is created and is immutable: it never changes across versions, and a client MUST NOT offer a way to add, retarget, or clear it after the fact. An item either came from somewhere or it did not. This immutability is what lets pinned documents (§8) and withdrawal endcaps (§9) carry it without any risk of drift.
  4. Lineage survives withdrawal: the endcap keeps forked_from (§9). Where an item came from is not the work being taken back.
  5. Lineage across origins is a remote reference and SHOULD send a mention (§15.2), whose relation is fork.
  6. A fork is a copy, not a transclusion: the forked content becomes the new item's own content_md, with no wrapper and no transclusions entry. The lineage marker is the only trace. Readers MUST NOT infer that a fork still resembles its source. What is copied is the pinned document, not its composition (decision #57; promoted from §16.6f in the eleventh revision): a ![[id]] directive or [[id]] link copied as written would re-resolve at publish in the forker's context (§10.2) — a later version, or a failure for an origin the forker has not imported — and a fork would then descend from bytes nobody pinned. (Forking a quoted fragment directly, naming the fragment's own pin, remains the way to take one passage with its own lineage.) A forking client SHOULD build the new content_md as follows:

    • The source's own prose is copied byte-exact from the pinned content_md.
    • Each blyg-transclusion element in the pinned content_html (§10.2) replaces its directive as an ordinary markdown blockquote of that element's content, recursively — nested quotes become nested blockquotes — followed by a visible attribution line naming the quoted item's origin and id and linking to its page (§5.8) where the publisher can compute it, else to its item document. The line's wording is the client's. A partial transclusion flattens the same way, from its baked paragraphs. Partiality is read from the blyg-partial class in the pinned bake, never from > lines in content_md (decision #62): a bake from a client that predates the partial grammar carries none, and the lines after its directive are the author's prose.
    • A quote the pinned document carries is copied whatever the quoted item's current state (decision #62). The pinned source serves those bytes forever (§8, §10.4), so the fork adds a place the words can be fetched from, not a disclosure; the attribution line points at an id whose live document says what the quoted item's state now is. A client MAY note that state beside the quote when it holds it, and MUST NOT need the network at publish to decide.
    • Each inherited [[id]] link becomes an ordinary markdown link to the absolute page the pinned content_html rendered for it (decision #63), in a forked fragment as in a forked thread — the same register as the quotes.
    • The blyg-transclusion class MUST NOT survive into the fork: the forking publisher baked nothing and verified nothing. A flattened quote is in the plain-web register of §10.2 — a quotation with a link, editable, unverified — and forked_from is the way back to the verifiable original (rule 8). The fork sends no transclusion mention for a quote it inherited; an author who wants a live quote writes a directive, which resolves and notifies as any directive does.
    • Generated spans (blyg-tk-gen, §5.7) in the pinned document — the source's own and its quotes' — are disclosed in the fork through the existing construct (§5.7 rule 7: sources empty, model if known), so a fork cannot launder a disclosure away.
  7. A fork's page SHOULD show its lineage (revision of 2026-10-06, decision #65): a publisher that renders an HTML page for a fork SHOULD show on it that the item was forked, linking the pinned version it names (its page where the origin serves one, else its pinned file). Lineage exists so that a reader can compare a fork with its source; on the wire alone, only machines can.

  8. Custody runs one hop per fork (decision #65). A fork vouches only for the pinned bytes it copied: its flattened quotes (rule 6) are unverified, and the way to verify one is to follow forked_from to the pinned source, where the quote is still a verified transclusion — and, for a fork of a fork, to repeat that one hop at a time. Nothing carries verification through a chain of forks, by design: each fork can only honestly assert what it copied.

5.7 Generation provenance — the generated array

Invariant 3 stands: AI is never in the protocol. Generation, if any, is a studio act at authoring time, and the published content_md is ordinary markdown — no markers, no instructions, no authoring grammar of any kind reaches the wire. The hash covers exactly what readers read. What the wire gains is generation's provenance shadow: a publisher whose version contains machine-generated prose SHOULD disclose it via an OPTIONAL top-level "generated" array, parallel to transclusions, one entry per generated span in document order:

"generated": [
  { "sources": [ { "id": "7c9wk2mhq0v3xj8tn5rzfd41bg", "version": 3 },
                 { "origin": "https://blyg.protocol-institute.org/",
                   "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 1,
                   "cited": { "source": "Protocol Institute Blyg",
                              "retrieved": "2026-10-10T17:40:00Z" } } ],
    "model": "claude-opus-5",
    "at": "2026-10-10T17:40:00Z" }
]

Rules:

  1. sources names the exact published versions of the items whose content was drawn on by the generator for that span — the same disclosure whether the generator was handed them by an author or retrieved them itself from the archive. Each entry is a reference (§5.9): origin omitted for the publisher's own items and present for another origin's (new in 0.4; promoted from the 0.3 text's §16.3, where a source was own-origin by construction), exactly as transclusions[] does, and it MAY carry cited. The array MAY be empty, meaning no sources declared: pure instructed generation, or generation whose sources the publisher cannot name. model and at are RECOMMENDED. A span MAY be the whole body.
  2. The array MUST NOT carry instruction text or any other pre-generation authoring state — instructions are studio-private, permanently. Disclosure covers what was produced and from what, never how it was asked for.
  3. A source is not a transclusion. Drawn-upon material is woven into new prose, not quoted: source references never appear in transclusions and never produce a blyg-transclusion blockquote. Verbatim, visible, cited quotation remains exclusively transclusion (§10). The two disclosures are disjoint by construction.
  4. The key is omitted entirely when the version involved no generation, and a withdrawal endcap (§9) never carries it. Readers ignore the unknown key (§13.1) — the construct is fully backward compatible.
  5. Pinned version files (§8) carry the pinned version's generated array, like transclusions — a citation includes its provenance.
  6. Like all provenance in this protocol, generated is self-asserted and unverifiable — the same honesty stance as timestamps and author. The protocol does not pretend to verify what it cannot.
  7. Where the generation ran is not part of the claim. generated[] says this prose is machine-generated, and, when known, by what model, from what, and when. Text generated outside the publishing studio and brought into an item — pasted from another tool, produced by an external agent — is disclosed the same way: an entry with sources empty or naming what is known, model and at if known. There is no marker for "generated elsewhere", because no reader could verify it and none would act on it. A publisher that knows a span is machine-generated SHOULD disclose it regardless of who ran the model; the construct exists so that disclosure is always possible.
  8. A source resolves like a directive (new in 0.4). A source is resolved at generation time by §10.2's order — a local published item, else an imported item whose local state is current or withdrawn with a pin-retained snapshot, else an error, and more than one imported match is an error — so a source may be any item a directive may name, a thread included. What is fed to the generator is the publisher's local snapshot, never a live fetch: generating follows reading, as quoting does. Disclosure is direct only, as §10.3 is for transclusion: a thread fed to a generator is disclosed as that thread, and the words it had itself baked are inspectable through it.
  9. A remote source notifies (new in 0.4). A sources[] entry carrying an origin is a remote reference and SHOULD send a mention (§15.2) whose relation is source (§15.4). §10.1 keeps links silent because a link carries none of the target's words; a generation source carries them, transformed, and in this protocol notification tracks whether the target's words are carried. An agent that draws on many remote items sends many mentions, and that is legitimate: each says that a model at this origin used those words, and the item it points at has content (§10.6's anti-pattern is content-free stubbing, not disclosure). Nothing in content_html marks a remote source beyond the blyg-tk-gen wrapper below; span-level source mapping remains deliberately unpromised.

Publishers additionally disclose generated spans in the rendered HTML: the renderer MUST wrap each generated span in content_html as <span class="blyg-tk-gen">…</span> (inline output) or <div class="blyg-tk-gen">…</div> (block output). The class name blyg-tk-gen is a permanent wire token — it is baked into published content_html, like blyg-transclusion (§10.2), and cannot be renamed. No data attributes are REQUIRED: span-level source mapping is deliberately not promised (the JSON provenance is version-level and robust; span-level claims would be brittle across the author's post-generation edits). Styling the class is presentation, any client's free choice. A client that makes generated spans visible SHOULD say, wherever it explains the mark, that the disclosure is the publisher's own and unverified (rule 6); a tint that reads as a verified authorship badge makes a claim the protocol refuses to make (revision of 2026-10-06, decision #59; until then this paragraph said the reference client left the class unstyled for that reason — it has styled it, off by default and with that disclaimer, since blygger-studio 0.27.0).

5.8 The page field

(New in 0.3.) An item document MAY carry "page": the URL of the item's human-readable permalink, origin-relative and resolved against the origin ("page": "t/7c9wk2mhq0v3xj8tn5rzfd41bg/") or, from 0.4, absolute.

5.9 The reference shape

Four constructs in this version name a version of an item at an origin — forked_from (§5.6), stub_of (§10.6), transclusions[] (§10.3) and, from 0.4, generated[].sources[] (§5.7) — and all four use one shape:

{ "origin": "https://blyg.protocol-institute.org/",
  "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy",
  "version": 1 }

The human half of a citation — cited (new in 0.3). A reference names a version and carries no words, so a reader of a document whose target has since disappeared would hold an identity and nothing to show. A reference MAY therefore carry an OPTIONAL cited object: the label the citing publisher saw when it made the reference.

"stub_of": { "origin": "https://blyg.protocol-institute.org/",
             "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 1,
             "cited": { "source": "Protocol Institute Blyg",
                        "author": "Editor",
                        "excerpt": "Stigmergy is what a protocol looks like from inside…",
                        "url": "https://blyg.protocol-institute.org/f/1vgtgz0gq5b2c9k7d3m8r4n6xy/",
                        "retrieved": "2026-09-16T20:11:00Z" } }

Why it is on the wire at all: a transclusion already bakes the target's entire content into the quoter's document, self-asserted, so a label is strictly weaker than what §10 permits; and a citation is as-of-retrieval by nature, so the citing publisher's frozen label is more faithful to what was cited than a reader's later lookup of the live target — not a fallback for when the link dies.

6. Manifest and archive index

6.1 Manifest — blyg.json

{
  "blyg": "0.4",
  "level": 2,
  "generator": "blygger-studio/0.46.3",
  "generator_url": "https://github.com/blygger/blygger-studio",
  "site": "https://example.com/blyg/",
  "title": "Venkat's blyg",
  "author": { "name": "Venkatesh Rao", "bio": "…", "avatar": "media/avatar.png",
              "links": [{ "label": "Home", "url": "https://venkateshrao.com" }] },
  "feed": "feed.xml",
  "items": "items/index.json",
  "blogroll": "blogroll.opml",
  "webmention": "webmention",
  "updated": "2026-07-18T09:30:00Z"
}

The manifest author is the publication identity — a person, a collective, a masthead: the imprint, where per-item author (§5.5) is the byline. When an item carries no author, no assertion is made; readers fall back to this site-level identity for display.

The blogroll key is OPTIONAL: an origin-relative path, canonically "blogroll.opml", present only when the publisher serves a non-empty blogroll (§11). The webmention key (new in 0.3) is OPTIONAL: the URL of the publisher's Webmention endpoint, origin-relative allowed, present only when the publisher receives mentions (§15.1); a static export omits it. The site value is self-asserted and display-advisory only; it never establishes identity (§12.2). level and generator are informative (§3.2), and so is generator_url (new in 0.3): OPTIONAL, one absolute URL to the client software's canonical source repository or home page, baked in by the client's author beside generator (the precedent is Atom's generator uri). Publishers SHOULD emit it; readers MUST NOT gate on it; its absence means only that nothing was stated. It is a SHOULD and will never be a MUST — a required field that readers may not act on would be a conformance rule serving a directory's convenience, and most existing clients would fail it for a reason unrelated to publishing. There is deliberately no maintained/unmaintained declaration: the software that would need to say "I am unmaintained" is exactly the software nobody is updating, so maintenance status is a fact for directories to observe, never for the wire to assert. A registry MAY require a client source as a condition of listing; that is its business, not conformance.

Members (revision of 2026-10-17, decision #76; checked against all 22 live manifests, which conform): REQUIRED blyg, title, updated; RECOMMENDED level, generator, generator_url, site, author; OPTIONAL feed, items, item, pin (defaults below), blogroll, webmention. Unknown members are ignored (§13.1).

The manifest locates the surface (new in 0.4; promoted from the 0.3 text's §16.6e, adopted in a reshaped form from a public proposal, blygger-spec#2). feed and items are authoritative: origin-relative or absolute URLs, defaulting to feed.xml and items/index.json when absent, and readers use the manifest's value. Two further OPTIONAL keys, in RFC 6570 level-1 form, say where documents live: item, with the variable id (default items/{id}.json), and pin, with the variables id and n (default items/{id}/v{n}.json). Readers MUST expand them wherever this document says {origin}items/{id}.json or {origin}items/{id}/v{n}.json (§4). All four values MAY carry a query string, because they are expanded or fetched, never resolved against. A blyg that omits them is unchanged, and the reference client emits none: absent means default, and emitting defaults would only make every manifest longer. A manifest that relocates its surface looks like this — the one fixed name is still its own (§12.1):

{
  "blyg": "0.4",
  "level": 2,
  "generator": "soapbox/1.0.0",
  "site": "https://example.com/",
  "feed": "https://example.com/feed/blyg/",
  "items": "https://example.com/wp-json/blyg/v1/items",
  "item": "https://example.com/wp-json/blyg/v1/items/{id}",
  "pin": "https://example.com/wp-json/blyg/v1/items/{id}/v{n}",
  "updated": "2026-10-07T22:44:00Z"
}

The cost is one cached manifest per unknown origin: following a reference needs the target origin's manifest to expand item, and subscribed readers hold it already, receivers own theirs, so the only new fetch is on a staleness check against an origin a reader has never read (§15.4 raises its bound by one for the same reason).

6.2 Archive index — items/index.json

Every item ever published — including withdrawn items — with no window, ordered by updated descending:

{
  "updated": "2026-07-18T09:30:00Z",
  "items": [
    { "id": "7c9wk2…", "kind": "fragment", "created": "…", "updated": "…", "version": 3 }
  ]
}

The index is what makes new and lagging subscribers lossless: any reader can enumerate it and fetch item documents, regardless of how much feed window it missed. On the subscribe side it is the reconciliation surface (§13.2) — complete by construction, so diffing it against local state is total recovery.

7. The feed — feed.xml

RSS 2.0 with the blyg: namespace (https://blygger.org/ns/0.1):

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:blyg="https://blygger.org/ns/0.1">
  <channel>
    <title>Venkat's blyg</title>
    <link>https://example.com/blyg/</link>
    <description>…</description>
    <lastBuildDate>Sat, 18 Jul 2026 09:30:00 GMT</lastBuildDate>
    <blyg:level>2</blyg:level>
    <blyg:manifest>https://example.com/blyg/blyg.json</blyg:manifest>
    <item>
      <guid isPermaLink="false">blyg:7c9wk2mhq0v3xj8tn5rzfd41bg:v3</guid>
      <link>https://example.com/blyg/f/7c9wk2mhq0v3xj8tn5rzfd41bg/</link>
      <title>Sharpened the claim — Markdown source of the latest…</title>
      <description><![CDATA[<p>rendered HTML of latest version</p>]]></description>
      <pubDate>Sat, 18 Jul 2026 09:30:00 GMT</pubDate>
      <blyg:id>7c9wk2mhq0v3xj8tn5rzfd41bg</blyg:id>
      <blyg:kind>fragment</blyg:kind>
      <blyg:version>3</blyg:version>
      <blyg:created>2026-07-17T18:00:00Z</blyg:created>
      <blyg:item>https://example.com/blyg/items/7c9wk2mhq0v3xj8tn5rzfd41bg.json</blyg:item>
    </item>
  </channel>
</rss>

Rules:

8. Pins — items/{id}/v{n}.json

A pin is the publisher's irrevocable hosting promise for one specific published version: this exact content stays fetchable at this URL forever — surviving later edits and withdrawal. Only pinned versions get per-version files; unpinned history stays withheld. (Analogy: the item id plays the IPNS-name role — a mutable pointer; a pinned version plays the CID role — immutable and citable. Blygger inverts IPFS's default because it is an authoring medium: mutable by default, immutable by explicit act.)

{
  "blyg": "0.3",
  "id": "7c9wk2mhq0v3xj8tn5rzfd41bg",
  "kind": "fragment",
  "version": 2,
  "at": "2026-07-17T21:12:00Z",
  "note": "typo",
  "pinned": true,
  "origin": "https://example.com/blyg/",
  "author": { "name": "Venkatesh Rao", "url": "https://example.com/blyg/" },
  "content_md": "…that version's markdown…",
  "content_html": "<p>…that version's HTML…</p>",
  "content_hash": "sha256:…"
}

Rules:

  1. Irrevocable. Once pinned, the file MUST return 200 forever — including after the item is withdrawn. Unpinned versions and unknown ids: 404.
  2. Any published version with content MAY be pinned, including retroactively (exposing withheld history is the publisher's right). Withdrawal endcaps (§9) MUST NOT be pinned — there is nothing to cite.
  3. Pinning does not freeze the id: the live stream continues under the same identity; the pin guarantees only the citation.
  4. Media referenced by any pinned version MUST be retained forever. Pinned documents carry no media array; their content_html references media directly, relying on media immutability (§5.4).
  5. Threads are pinnable like fragments: a pinned thread version serves its publish-time content_html (baked snapshots included, cross-origin ones too) and its transclusions provenance, with "kind": "thread". A pinned version that involved generation carries its generated array (§5.7). A pin carries its own citations: a pinned stub version carries the stub_of it was published with (§10.6), and a pinned version of a forked item carries forked_from (§5.6) — the frozen artifact says what it answered and where it came from, at the version it said so.
  6. author rides outside the pin promise. content_hash covers content_md only, so the pin's immutability guarantee covers content, never the author bytes. A pinned file SHOULD carry the assertion as published with that version, but both serve-time and publish-time assertion strategies are conformant. Verifiable authorship, if wanted, is a signature inside an authorspace grammar (§5.5), not a protocol feature.
  7. A pin is the only thing lineage may point at (§5.6), and the only thing a reader may retain past withdrawal (§13.4). Pins are therefore the protocol's whole answer to "how do I make sure this stays citable": ask the author to pin it.

8.4 Historical versions: pinned-only, JSON promised, HTML optional

No route ever serves an unpinned older version, in any representation — a general historical route would gut withheld-unless-pinned. The same rule bounds what a public page's version display may offer: every version it exposes — as a link, or presented in place — MUST be one the origin already promises forever, i.e. the live version and pinned versions, nothing else. A version display MUST NOT offer, imply, or hint at access to unpinned history.

Within that bound, presentation is the client's. The floor is discrete pin citations (e.g. "v6 · pinned: v2, v4"); a page MAY additionally present the pinned artifacts themselves in place (the reference client pages among them with each shown version visibly marked frozen). The design intent to honor, whatever the presentation: pins are a sequence of frozen citable artifacts of one identity, not pages of one document. Keep each pinned version discrete, visibly frozen, and individually citable; a presentation cannot reconstruct a continuous edit history, because the bytes for one are never served.

The pin's promise is the JSON file (§8 rule 1). Additionally, a publisher MAY serve a rendered page for each pinned version at the item's permalink path plus a version segment:

GET {origin}{page}v{n}/     e.g. {origin}f/{id}/v{n}/ or {origin}t/{id}/v{n}/

Rules, for publishers that serve these pages:

  1. Gated exactly like the JSON file: 404 unless that exact version is pinned; 200 forever once it is, surviving withdrawal. The route exposes only bytes already promised forever — never unpinned history.
  2. The page's content is that version's publish-time content_html, verbatim (baked transclusion snapshots included for threads). Page chrome (banner, links) is presentation and may evolve; the content is what is promised.
  3. The page SHOULD carry rel="canonical" pointing at the live permalink, SHOULD be visibly marked as a frozen snapshot, and SHOULD link its v{n}.json twin — the page is the human citation, the JSON the machine citation. Nothing in this protocol reads rel="canonical": readers use page (§5.8) and mentions target it (§15.2), so the link is for indexers, and its job is that they index the live page rather than its frozen copies. A publisher that serves one item at more than one live address — a front page at the mount root, for instance — names whichever it wants indexed, and page is unaffected (decision #69, 2026-10-12).
  4. Pinned pages are not publish events: they MUST NOT appear in feed.xml, the archive index, or items/index.json, and they add no manifest vocabulary. Discovery is the live page's pin citations.
  5. Readers MUST NOT require these pages — the subscribe side works entirely from the JSON surfaces.

9. Withdrawal

Withdrawing is the only exit for a published item. There is no delete: no permanent-delete state exists in the protocol. (Discarding a never-published draft is a hard delete — nothing was ever public.)

Withdrawing publishes a permanent endcap: a version bump with content_md = "", content_html = "", media = [], an optional note, "kind": "withdrawn", updated set, and the changelog retained plus the endcap entry. For threads, the endcap also empties transclusions to [] (§10.4) and omits stub_of (§10.6); an endcap never carries a generated array (§5.7). The endcap keeps page and forked_from: the endcap empties what the item said, and where an item lives and where it came from are not what it said.

10. Threads and transclusion

A thread is an item with "kind": "thread": long-form markdown that transcludes other items. Threads use the same identity, versioning, changelog, withdrawal, and pin machinery as fragments. At 0.2, transclusion was same-origin and fragments-only. At 0.3 both restrictions are lifted: a thread may transclude fragments or threads, of its own origin or of any origin its publisher has imported — and the rule that makes this safe is that the quote is always the publisher's local snapshot, never a live fetch.

10.1 Grammar (permanent protocol surface)

![[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.

10.2 Publish-time resolution

Resolution happens in the studio at publish time; readers never resolve anything.

Resolution order. Each directive MUST resolve, in this order, to:

  1. a local, currently-published item with that id — fragment or thread;
  2. otherwise an imported item with that id, from any subscribed origin, whose local state is current — or withdrawn with a pin-retained snapshot (§13.4 retention is exactly what makes such a snapshot still quotable) — and which is a blyg item, not an L0 wrapper (§13.6: an L0 row is a wrapper around someone's RSS summary, not their item);
  3. otherwise a publish error. Drafts, unknown ids, withdrawn items with nothing retained, and L0 rows are publish errors. More than one imported match MUST be a publish error, never a guess — the directive names an identity, and the publisher does not pick an origin on the author's behalf. A local id — a draft or a published item — never falls through to step 2: an imported document with the same id as a local item is the ambiguity §5.1 describes, and a directive naming it is a publish error (decision #76).

The local snapshot is what gets baked — never a live fetch. Resolution snapshots the target's latest version as the publisher holds it: its rendered HTML is baked into the thread's content_html, wrapped as

<blockquote class="blyg-transclusion"
            data-blyg-id="{id}"
            data-blyg-version="{n}">…item html…</blockquote>

for own-origin sources, and for remote sources additionally

<blockquote class="blyg-transclusion"
            data-blyg-id="{id}"
            data-blyg-version="{n}"
            data-blyg-origin="https://blyg.protocol-institute.org/">…item html…</blockquote>

data-blyg-origin appears only for remote sources, so a baked own-origin quote is byte-identical to its 0.2 form. The wrapper is a bare blockquote plus data attributes — no link inside; any provenance link or byline shown on an HTML page is presentation, not part of the published content_html (a byline, if shown, is the source's own author passed through, §5.5). The class name blyg-transclusion is a permanent wire token, baked into published content_html; its styling is presentation.

Partial transclusion (revision of 2026-10-03, decision #49). A directive with an attached blockquote (§10.1) resolves exactly as a whole directive — same order, same snapshot rule — and adds one check: at publish, the selection MUST be a substring of the target snapshot's text content — its content_html with tags stripped, whitespace collapsed within a block, block boundaries kept as line breaks — at the version being baked; otherwise a publish error, as for an unresolvable directive. Any reader MAY re-check by the same test while the origin serves that version, live or pinned. That window closes when the origin stops serving the version — a quote baked from a stale import can name one it never will again — and the quote then has the standing §14 gives every baked snapshot: the quoter's assertion. The check this protocol promises is the publisher's, at publish, against the snapshot it holds, and the way to keep a quote checkable is to quote a pinned version (decision #76, conformance finding F2). Misrepresentation by elision is not fixed by this and is not claimed to be; the second class below is what discloses that a quote is partial. There is no cap on a selection's length: a substring test does not care, and a cap would be editorial convenience in the protocol. The bake is

<blockquote class="blyg-transclusion blyg-partial"
            data-blyg-id="{id}"
            data-blyg-version="{n}">
  <p>…one line of the selection's plain text…</p>
</blockquote>

with data-blyg-origin for remote sources exactly as above. The content is the selection's plain text, one <p> per line of it, escaped — not a carved sub-range of the source's inline HTML: the selection is defined on text, and cutting an HTML range faithfully is a second project. Emphasis in the source does not survive into the quote; that is the visible cost. blyg-partial is a permanent wire token beside blyg-transclusion, so partiality is visible to any reader and survives import. A plain-web target gets nothing: quoting an ordinary web page under a {url} stub (§10.6) is an ordinary markdown blockquote, because there is no versioned document to verify against. The stub may still carry a frozen cited label (§10.6), which promises no test.

Consequences of the snapshot rule, all deliberate:

content_md keeps the directives — it is the authoring source of truth. Republishing a thread re-resolves every directive to the then-current local snapshot. A source that has since been withdrawn at its origin, and not pin-retained, is a publish error on republish — exactly as a withdrawn local fragment is — and the author removes the directive or leaves the thread at its current version. The protocol does not offer "freeze this quote at the withdrawn version": that would be the first place withdrawal failed to roll to null, and it is deliberately not offered here. The consequence for a long-lived composed work — a book as a thread of threads, a serial republished for years — is that every unpinned remote quote is a veto held by a stranger: one withdrawal and the work cannot be republished until the directive is removed. The mitigation is already in the rules and is stated here so an author meets it where it matters: quote pinned versions if you want your work to survive — a pinned snapshot is retained (§13.4) and stays quotable through withdrawal (step 2 above). A studio SHOULD say at publish when a directive resolves to an unpinned remote item.

10.3 Provenance

Thread item documents carry a top-level "transclusions" array, in directive order, of references (§5.9) naming the exact versions baked into this thread version:

"transclusions": [
  { "id": "7c9wk2mhq0v3xj8tn5rzfd41bg", "version": 3 },
  { "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 1,
    "origin": "https://blyg.protocol-institute.org/" }
]
{ "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 1,
  "origin": "https://blyg.protocol-institute.org/",
  "selector": { "exact": "Stigmergy is what a protocol looks like from inside",
                "prefix": "is the whole point. ", "suffix": ", and the reason" } }

Mention verification (§15.4) ignores it, as it ignores cited; a reader that ignores it entirely remains conformant, because readers display baked HTML and never resolve. The relation is transclusion and staleness is §5.9's check, unchanged — which is why this entered the 0.3 text as a revision rather than opening 0.4 (§16.4). - Fragments omit the key entirely; threads always carry it (a withdrawn thread's endcap carries []). Generation sources are disclosed separately and never appear here (§5.7), and a plain internal link (§10.1) produces no entry: only copied words are disclosed. - A remote entry is a remote reference and SHOULD send a mention to its origin (§15.2), whose relation is transclusion. - Readers at L2 MUST scope a transclusion's id to its origin when present, and to the document's own origin when absent, before doing anything with it — the same origin-scoping rule as everywhere else (§13.1).

10.4 Snapshot independence

The rule that makes network cycles harmless, applied at every origin:

10.5 Feed and presentation

Feed entries for threads carry the full baked self-contained HTML (automatic, given the snapshot rule). Threads have no length cap. How an HTML feed page excerpts threads is presentation, not protocol.

10.6 Stubs — stub_of

(New in 0.3.) A stub is a thread that declares itself a response to exactly one target. Threads only — the frozen v0 concept's own framing ("stubs are threads, so a stack always looks like a link to a local thread"), and what makes stubs stackable: threads can be transcluded, so a stub of a stub is nesting, not a new construct.

A thread document MAY carry a top-level "stub_of", in one of two shapes:

"stub_of": { "origin": "https://blyg.protocol-institute.org/",
             "id": "1vgtgz0gq5b2c9k7d3m8r4n6xy", "version": 1 }

for a blyg target — a reference (§5.9), origin REQUIRED even when it is the publisher's own, because a citation is absolute — or

"stub_of": { "url": "https://simonwillison.net/2026/Sep/10/some-post/" }

for a plain-web target: an L0 subscription's entry, or anything with a URL. An L0 wrapper's id (§13.6) is a reader-local name for someone's RSS entry, not an item at an origin, and the blyg-target shape MUST NOT name one; a response to an L0 entry is a {url} stub naming the entry's link (decision #76).

Either shape MAY carry the cited object of §5.9 — the blyg-target shape since §5.9 named stub_of as a site, the {url} shape by the revision of 2026-10-06 (decision #55) — under that section's rules unchanged: retrieved REQUIRED, self-asserted, frozen when the stub was created, ignored by verification, never baked, ignorable by any reader, and excerpt a caption capped near 200 characters. On a {url} target the frozen label matters more than anywhere else, because a plain-web page has no version to re-fetch and the label is the only record of what was read:

"stub_of": { "url": "https://simonwillison.net/2026/Sep/10/some-post/",
             "cited": { "source": "Simon Willison's Weblog",
                        "excerpt": "Some post",
                        "url": "https://simonwillison.net/2026/Sep/10/some-post/",
                        "retrieved": "2026-09-16T20:11:00Z" } }

A plain-web target gets nothing else: no selector, because there is no versioned document to check a quotation against (§10.2). It is quoted with an ordinary markdown blockquote, which is why the caption cap stands.

Rules:

  1. Exactly one target. A stub responds to one thing. Other transclusions in the body are quotes, not additional targets. A response to two things is two stubs.
  2. The body is the author's. The protocol never requires the body to transclude the target. stub_of is the machine-readable response marker, and readers rely on it, never on body inspection. A stub whose body contains no quote of its target is a response by link, which is legitimate and verifies as a stub (§15.4). (The reference client's stub action always starts the body with ![[id]] for a blyg target, because a stub without the quote is not a stub in the medium's own aesthetic; an author who deletes the directive has still published a stub.)
  3. Version agreement. At publish, if the body transcludes the stub's target, stub_of.version MUST equal the version actually baked (§10.3); otherwise it keeps the value it last had — at creation, what the author saw; after a version that quoted the target, the version baked then. The value is monotonic and never moves backwards (decision #76, conformance finding F17). The two can never disagree on a published document.
  4. stub_of lives on versions, like transclusions: a pinned version of a stub carries its own citation (§8), the live document reflects the latest, and a withdrawn stub's endcap omits it (§9) — which is precisely what makes a withdrawn stub stop verifying as a stub.
  5. A blyg-target stub is a remote reference when its origin is not the publisher's own, and SHOULD send a mention (§15.2), whose relation is stub. A {url} stub SHOULD send an ordinary W3C Webmention to that URL if the target advertises an endpoint; most will not.
  6. No feed vocabulary (§7). A stub is a thread and appears as one.
  7. Stubs are not coupled to any studio construct — not to hoppers, not to subscriptions. The marker is the whole of the protocol's knowledge that a response happened.

What a stub is not: a reply. There is no thread of replies, no conversation object, no notification to anyone but the target's origin, and nothing a target can do about being stubbed except read it. The stub is on the stubber's soapbox, under the stubber's identity, in the stubber's feed.

A stub is a response, and a stub emitted without one is a misuse. An aggregator, script, or agent that emits a stub for every item some set of origins publishes — with nothing to say about any of them — is not responding: it is re-emitting other people's content through a construct whose cost is supposed to be editorial work (§13.5), and it fills every target's response signal with non-responses (§15.5). The honest shape for "one place to follow a group" is a blogroll plus curation (§11, §13.5). An author, human or otherwise, that reads each item and answers it under its own byline is stubbing legitimately, however many stubs that is.

11. The blogroll — blogroll.opml

(OPTIONAL at every level.) The blogroll is the protocol's static discovery plane: a curated, published list of subscriptions the publisher chose to show. It is a publishing act, not an export — the outbound half of the public graph. From 0.3 the inbound half exists too, and is dynamic: verified mentions (§15).

<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head><title>Venkat's blogroll</title></head>
  <body>
    <outline type="rss" text="Interconnected"
             xmlUrl="https://interconnected.org/home/feed"
             htmlUrl="https://interconnected.org/home/" />
    <outline type="rss" text="Another blyg"
             xmlUrl="https://example.org/blyg/feed.xml"
             htmlUrl="https://example.org/blyg/" />
  </body>
</opml>

12. Resolution — from URL to subscription

Resolution turns a URL found out-of-band — a link someone shared, a blogroll entry, an address bar — into a subscription target. It is deterministic, ordered, and bounded (at most 6 fetches), so that any two conforming readers handed the same URL arrive at the same subscription identity. Readers that subscribe MUST implement the following procedure in this order. Output: a blyg subscription { origin, manifest }, a legacy feed subscription { feed_url } (L0), or failure with the list of URLs tried.

12.1 The algorithm

  1. Normalize. Parse the URL; strip query and fragment. If the path does not end in /, append /. Call this the candidate origin. HTTPS is expected; readers MAY permit http: (local development) but SHOULD warn. The query is stripped because a manifest's address is a path (decision #64, 2026-10-06): a URL with a query string cannot anchor origin-relative references — resolving items/x.json against /?rest_route=… drops the query — so it has no surface and no identity prefix, and a manifest served only at such a URL is out of scope.
  2. Direct probe. GET {candidate}blyg.json. If the body parses as a manifest — a JSON object with a "blyg" version key — the URL is resolved as a blyg. Do not trust Content-Type (static hosts misreport); parse-success is the test.
  3. Feed upgrade. If the input URL itself returns XML that parses as RSS or Atom: if the channel carries <blyg:manifest> (§7), fetch that manifest → resolved as a blyg. Otherwise hold the feed as the L0 candidate and continue — a later step may still find a manifest.
  4. rel probe. If the input returned HTML, look for <link rel="blyg" href="…">. Resolve the href against the document URL and fetch it. If the body parses as a manifest, that is the manifest (new in 0.4; promoted from the 0.3 text's §16.6e): a publisher whose manifest cannot live at blyg.json under its origin names it here directly. Otherwise the href is an origin base URL, and readers append blyg.json and probe as in step 2, as at 0.3. No new rel, no new media type; the feed's <blyg:manifest> (§7) already names the manifest by absolute URL. One hop only: a rel target's own HTML is never scanned — no recursive discovery, no loops. Readers MUST support the HTML <link> element here; supporting an equivalent HTTP Link header is OPTIONAL. (Publishers whose blyg mounts away from their front page SHOULD emit this link on pages they expect to be shared.)
  5. Conventional-mount probes. At the input's scheme+host root, probe /blyg/blyg.json, then /blyg.json, skipping any URL already probed. This is a courtesy fallback only — publishers MUST NOT rely on it. The mount is free (§4); these probes exist because /blyg/ is the reference default and root mounts are first-class.
  6. RSS fallback. If no manifest was found: the L0 candidate from step 3, or standard RSS autodiscovery on the input's HTML (rel="alternate" type="application/rss+xml" or the Atom equivalent), resolves the URL as a legacy L0 feed (§13.6). Otherwise resolution fails, and the reader SHOULD report the trail of URLs tried.

12.2 Subscription identity

12.3 Polling etiquette

13. Reader conformance — the import side

A conforming reader at L1 follows the ground rules in §13.1; a reader that maintains subscriptions additionally follows §§13.2–13.7.

13.1 Ground rules

  1. MUST treat item documents as ground truth and the feed as a lossy signal.
  2. MUST roll up by blyg:id: highest version wins; ties are broken by updated. Timestamps are self-asserted by origins; ordering across origins is the reader's own policy (§13.7).
  3. MUST treat a withdrawal endcap as roll-up-to-null (§13.4). A later version under the same id is the item returning.
  4. MUST ignore unknown kinds, unknown JSON members, unknown blyg:* XML elements, and reserved constructs, without rejecting the containing document.
  5. MUST NOT reject an item over its author contents, and MUST NOT treat equal author values from different origins as the same entity (§5.5).
  6. SHOULD backfill from items/index.json when the feed window has been missed; a reader offline for any duration recovers losslessly.
  7. MUST scope everything it learns to the origin: ids, authors, and trust do not transfer across origins. A reference's origin (§5.9) is the scope of the id it carries.
  8. MUST NOT gate any behaviour on generator or level (§3.2).

13.2 The reconciliation model

The archive index is the reconciliation surface; the feed is a cheap trigger. items/index.json is complete by construction (§6.2), so diffing it against local state is total reconciliation — the feed's only jobs are cheap change detection and low-latency pickup. This one commitment dissolves most failure modes structurally:

A 404 on items/index.json from an otherwise-live blyg is a nonconforming publisher: readers SHOULD fall back to feed-window-only operation and surface the subscription as lossy.

13.3 The watermark, history rewrites, and stealth edits

The highest version ever observed per imported item is a monotonic watermark, and it never silently regresses:

13.4 Withdrawal and retention

On a withdrawal endcap, a conforming reader rolls the item up to null: it MUST stop presenting the withdrawn content as live and SHOULD delete its stored copy — with one principled exception. Retention follows the origin's own serving surface: content corresponding to a version the origin has pinned (a fetchable items/{id}/v{n}.json) MAY be retained and continue to be displayed, with attribution linking the pin — the origin itself still serves those exact bytes forever (§8 rule 1), so local retention never exceeds the origin's own promise. Everything unpinned rolls to null.

A reader that wants a durable citation of someone else's content has exactly one instrument: the origin's pin. Local hoarding past withdrawal is nonconforming; asking the origin's author to pin is the protocol's answer. A pin-retained snapshot remains a valid transclusion source (§10.2); an unretained withdrawn item is not.

The watermark survives withdrawal (decision #76, conformance finding F5). The endcap is itself a version, the highest, and a reader keeps it to show the item withdrawn; deleting the stored copy never deletes the version number. A document that arrives with a version at or below the endcap's is a replay under §13.3, not the item returning under §13.1 rule 3.

13.5 Curation display and re-emission

A client that both subscribes and publishes MUST keep the two planes separate:

13.6 L0 grandfathering — the legacy RSS wrapper

A plain-RSS subscription (resolution step 6) imports each entry as a summary item + link, best-effort by design:

13.7 Ordering across origins

Ordering the merged reading surface is the reader's own policy, never the protocol's — remote timestamps are self-asserted (§5.2) and comparable only advisorily. Readers SHOULD ensure a skewed or future-dated origin cannot permanently dominate the display order. (The reference client sorts by min(claimed updated, first observed locally): an item sorts no newer than when the reader actually saw it.)

Items published together MAY be shown together (revision of 2026-10-17, decision #78). One authoring act can produce many items — a thread and the fragments it quotes, minted in one publish; a document chopped into fragments — and a reader that shows each as a separate arrival buries a week of other origins under one act. Nothing on the wire marks the burst, by design (§5.2: the counter stays bare, and §13.2's same-hash rule shows the pattern: the signal is derived, never declared). What a reader can see is enough: items from one origin whose created times fall within one publish, and, when a thread created with them names them in its transclusions[], the thread that is their whole. A reader MAY present such a burst as one publication — the thread as its entry, its fragments beneath it or deferred to the low-priority queue of §13.2 — and MUST still import every item (§13.1), since folding is display, never omission.

14. Security and privacy considerations

15. Webmention — response notification with structural verification

(New in 0.3. OPTIONAL at every level.) The blogroll (§11) is the static half of discovery: who a publisher chose to show they read. Webmention is the dynamic half: how an origin learns that it has been quoted, stubbed, forked, or drawn on by a generator, from a publisher it does not subscribe to. It is the protocol's first and only dynamic surface, and it is optional precisely so that the static file contract (§4) survives intact — a static-only blyg does nothing here and stays fully conformant (§15.7).

The mechanism is the W3C's, reused without invention: a POST of source=…&target=…. What this protocol adds is structural verification — a receiver believes a mention only when the source's item document, fetched from the origin it claims, names the target with a reference (§5.9).

15.1 Advertising an endpoint

15.2 Sending

A publisher SHOULD send a mention for each remote reference in a newly published version: every blyg-target stub_of whose origin is not its own (§10.6), every transclusions[] entry carrying an origin (§10.3), a forked_from naming another origin (§5.6), and every generated[].sources[] entry carrying an origin (§5.7; new in 0.4). A {url} stub SHOULD send an ordinary Webmention to that URL. Same-origin references never generate mentions. A version that both quotes and draws on one target MAY send a single mention for it; the receiver determines the relation (§15.4 step 4).

15.3 Receiving

The endpoint contract is the W3C's: POST, body application/x-www-form-urlencoded, source and target.

  1. Syntactic checks → 400. Both MUST be absolute http(s) URLs; source MUST differ from target; target MUST lie within this blyg's own origin — the full base URL (§12.2), not merely its host — and MUST name a published item — by page, or as its item document's URL (the receiver's own item template or the default items/{id}.json, §6.1). An unknown item is 400, not 404: the endpoint exists, the claim is bad. A withdrawn target is accepted (people may respond to a withdrawal).
  2. Rate limit → 429. RECOMMENDED: more than 60 mentions from one source host in an hour. Cheap — but not, on its own, enough to bound the queue: a sender that controls a wildcard DNS zone has unlimited hosts, and every accepted claim costs the receiver up to two fetches at URLs the sender chose (§15.4). Receivers SHOULD therefore also cap by a coarser grouping of the source (the reference client groups by registrable domain, at a looser limit than the per-host one, because the grouping is a heuristic that can over-collect) and cap total accepted claims per hour regardless of source. A claim refused by any cap is 429 and is never queued. The numbers are the receiver's; the shape — per source, per source group, global — is the recommendation.
  3. Accept → 202, record the (source, target) pair as pending, and verify asynchronously. W3C permits synchronous 200 or 201; 202 is the honest answer since verification fetches the network. A re-sent pair re-verifies rather than creating a duplicate.

15.4 Structural verification

Verification is bounded — RECOMMENDED: at most 2 fetches, or 3 when the source origin's manifest must be read (step 2), at most 3 redirects, 5 seconds per fetch, 1 MB per body — and these bounds are the receiver's protection, not a nicety (§14).

  1. GET source. If the response is JSON with a blyg key, it is the item document. If it is HTML, find <link rel="alternate" type="application/json"> (§5.8) and fetch that; it MUST be a blyg item document. Anything else → failed.
  2. The item document's final URL MUST be exactly {origin}items/{id}.json for the document's own origin and id (revision of 2026-10-06, decision #61; scheme and host compared case-insensitively, default port dropped, trailing slash of origin normalized). This is §12.2's identity rule applied inbound — the identity origin is the full base URL, path included (§4, §12.2), never merely its host — and it is what stops a mirror or an impostor from speaking in a real blyg's name. Two consequences are the point: a document served under one path-mounted blyg cannot verify in the name of another on the same host (two blygs on one host is an ordinary deployment, §13.5), and a pinned version file never verifies a mention — its URL is not the live document's, so a stub whose pin still carries stub_of stops verifying the moment its live document is an endcap (§9, §10.6 rule 4), with no extra fetch. A legitimate sender is unaffected: its source is the page (§15.2), whose alternate link names the live document (§5.8). Until this revision the rule here compared scheme, host and port only; a receiver built on that reading has a hole to close. Under surface templates (new in 0.4; promoted from the 0.3 text's §16.6e, decision #61): a source origin's item template (§6.1) may place its documents away from the default path. A receiver checks the final URL against the default first; when it differs, the receiver fetches the asserted origin's own manifest at {origin}blyg.json — one more fetch, within the bound raised to 3 — expands its item template with the document's id, and accepts an exact match. It fails the claim when the manifest is missing or the URL matches neither the default nor the template. Publishers who want their mentions verifiable by 0.3 receivers keep item documents at the default path.
  3. A document with "kind": "withdrawn" → gone.
  4. Relation, in order: stub_of naming { origin: this blyg, id: target } → stub; else a transclusions[] entry with origin = this blyg and id = target → transclusion; else forked_from naming a pinned version of the target → fork; else a generated[].sources[] entry (§5.7) with origin = this blyg and id = target → source (new in 0.4); else failed — the document exists but does not reference us. "This blyg" compares the document's asserted origin string against the receiver's own identity origin, normalized. The receiver is the only party that can say whether a version is pinned, so the fork check consults its own pins and fails a claim naming an unpinned version, at no extra fetch. Every comparison here reads a reference's identity members only; a cited object (§5.9) is ignored.
  5. Success → verified, recording the source item's id, origin, version, kind, the relation, the source author (pass-through, §5.5), and the source page URL. No content is stored — a verified mention is a pointer, and what it points at is fetched from its origin when displayed, or not at all.
  6. A re-sent mention re-verifies and updates what it recorded. A mention that verified before and fails now becomes gone; the receiver SHOULD keep the record with that status rather than deleting it, so that a stubber who withdraws and later republishes is recognized rather than treated as new.

The relation set is exactly stub, transclusion, fork, source — one per wire construct that names a target — and nothing else at this version. A plain link is not a relation (§10.1). source is the relation 0.4 adds (promoted from the 0.3 text's §16.3): a generation source is a wire construct that names a target and carries its words, transformed. A 0.3 receiver reports such a mention as failed, which is correct for it, and a sender SHOULD read that failure as the receiver's version, not as an error of its own.

15.5 What a verified mention is, and is not

Verified mentions are studio signals. They are never auto-published, never enter the feed, the archive index, or any item document, and add no manifest vocabulary. A publisher MAY display verified mentions of an item publicly — a "responses" list on a permalink page — and if it does, that is per-item curation under §13.5: a shown list with attribution and links to the origin, publicity being a property of the list, never of the mention. Nothing in this protocol lets a stubber put words on the target's page.

15.6 Plain mentions

A receiver MAY accept mentions whose source is not a blyg item document, verified the W3C way (the source page contains the target URL), and if it does MUST hold them apart from structurally verified mentions as a lower class — never in the same signal, never displayed as a response — because link verification is exactly the check that trackback spam defeated. The reference client does not accept them.

15.7 Withdrawal and static deployments

A withdrawn stub re-sends its mention once (§15.2); the receiver finds a withdrawn document and marks the mention gone. A withdrawn target keeps accepting mentions — its document is 200 forever — and its studio shows them under the withdrawn item. Nothing cascades: a stub of a withdrawn item is the stubber's speech and stays published.

A static-only deployment does nothing: it advertises no endpoint, receives nothing, and remains fully conformant at every level. It may still send — a static export is produced by some dynamic studio, which can send at export time — but is not REQUIRED to.

16. Ruled, deferred, and reserved constructs (non-normative)

This section records what has been decided but not yet built (and so is not yet normative text — the project's rule is that testing precedes prose), what has been explicitly deferred to a later version, and what is reserved. Implementations should leave room accordingly. Decision records are in blygger-spec's CLAUDE.md and docs/v0.4-plan.md. A subsection that has since been promoted into the normative text keeps its number here as a pointer, so that citations of it made before the promotion still resolve.

16.1 The human half of a citation — cited (promoted to §5.9, 2026-09-28)

Ruled 2026-09-28, emitted by blygger-studio 0.6.0 the same day on all three reference kinds, exercised across both live nodes (a remote transclusion carrying cited was imported byte-for-byte by the other node; a cross-origin stub's mention verified on the bare reference with the citation ignored), and promoted into §5.9 in the first published revision. The normative text is there; this number is kept only so that earlier citations of §16.1 resolve.

16.1a cited on a plain-web stub (promoted to §10.6, 2026-10-06)

Ruled 2026-10-03 (decision #55, on blygger-spec#7). The reference client has emitted a hostname-only cited on {url} stubs since 0.4 and a full one since 0.18.0, which freezes the subscription's name as source and the entry's title as excerpt at creation. A protocol-institute {url} stub carrying it was imported onto venkateshrao with stub_of byte-identical, and it was promoted into §10.6 in the seventh revision. The normative text is there; this number is kept so that earlier citations resolve.

Ruled 2026-09-28, rendered by blygger-studio 0.6.0 the same day, exercised across both live nodes (an inline link survived import into the other node's reading view intact, with no import error and no mention sent), and promoted into §10.1 in the first published revision, which replaced the sentence declaring [[id]] undefined. The normative text is there.

16.3 Remote generation sources (promoted to §5.7 and §15.4, 2026-10-17)

Ruled 2026-09-28 (decision #44) as a 0.4 construct — the one place the project's build-then-prose rule was inverted on purpose, because the wire had to be able to say this before any client could build it — built by blygger-studio 0.41.0, and exercised between the two reference deployments on 2026-10-10: a protocol-institute scope drew on venkateshrao item 20bfv5msj0hx4p4bma6hs319vb v1 in 4wev44fq07jfw36t67ys388t92, the item document carried generated[0].sources[0] with origin and cited, the mention verified as source on the far side, and the generated array survived import byte-identical. Promoted into §5.7 rules 1, 8 and 9 and §15.4 in this document's first revision, which is what opened version 0.4 (decision #43). The normative text is there; this number is kept so that earlier citations resolve. One build finding became practice, not rule: a thread fed as a source is handed to the generator as its baked HTML read as text, because its markdown holds bare directives.

16.4 Partial quotation (promoted to §10.1–§10.3, 2026-10-03) and titles (closed)

Partial quotation — promoted. Ruled 2026-09-28 as a partial transclusion (decision #49), built by blygger-studio 0.8.1 the next day, exercised across both live nodes (a PI stub quoting one paragraph of a venkateshrao thread: selector and the blyg-partial bake on the document, the mention verified as stub on the far side, the quote intact through import, a wrong quote refused at publish), and promoted into §10.1 (grammar), §10.2 (faithfulness and the bake) and §10.3 (selector) in the fifth published revision. The normative text is there; this number is kept so that earlier citations of §16.4 resolve. One implementation finding was left to the build and is now the rule: the bake carries the selection's plain text in paragraphs, not a carved sub-range of the source's inline HTML.

16.5 Transitive staleness (resolved 2026-09-28: there is no such thing)

§10.4 defines staleness as a direct relation. Ruled: no transitive notion exists, and none will be defined. A thread A that baked B at version n is stale only when B has a version above n. If B's own source C has moved and B has not republished, A holds B's unchanged bytes; republishing A re-bakes B's current version, which still contains the old C. A can do nothing about C — only B can, and B's client sees C's staleness directly, by §10.4. Every edge of the quotation graph is therefore owned by the publisher who baked it, and freshness over the graph is nothing but the direct check applied at each origin. No construct, no wire fact, nothing a client must do beyond §5.9's comparison. A client MAY show, as presentation, that an item it quotes has itself gone stale on something — the nested data-blyg-* attributes (§10.2) plus one fetch per layer are enough — but that is a reading aid, not staleness of the quoting document.

16.6 The write surface (never normative; companion note planned)

Multiple authoring tools already write into blygs, and each does so through whatever interface its client happens to expose. Ruled 2026-09-28: the protocol does not, and will not, specify how a client is written to. A normative write API would make a static-file client with no server non-conformant for a reason unrelated to publishing, and the write side is exactly what invariant 1 puts out of scope. What will exist instead is a non-normative technical note describing the reference client's write surface as a way, in the genre of TN-1, once that surface has been built with per-tool, scoped, revocable credentials and used by at least one third-party tool. Tools discover a write endpoint through an HTML rel link on the studio's page, never through the manifest; the manifest is wire and stays clean.

Clarified 2026-10-03 (decision #52), as the reference client's write surface is being built by a contributor. An OAuth-style authorization flow, in which a tool sends the owner to their own studio to approve a scoped token, is one way of minting the per-tool, scoped, revocable credential this section describes — the IndieAuth shape Micropub itself uses — and so stays within what was ruled; a paste-a-token path must exist beside it, for tools with no browser. Nothing about authorization touches the manifest, a .well-known path (host-rooted, so it would break a path-mounted blyg, §4), or this document: discovery remains the HTML rel link on the studio page, and whatever metadata it points at lives under the mount. An MCP server over the same operations and scopes is a transport for the client's contract, not a second contract; text an agent writes through it is disclosed through generated[] like any other generated text (§5.7).

16.6a Client source discovery — generator_url (promoted to §6.1, 2026-09-28)

Ruled 2026-09-28, emitted by blygger-studio 0.6.0 the same day on both live nodes, and promoted into §6.1 (with §3.2 extended to cover it) in the first published revision. The normative text — SHOULD emit, MUST NOT gate, no maintenance declaration ever — is there.

16.6b Identity, groups, and agents (ruled; technical notes, never protocol)

Three questions raised on 2026-09-28 were ruled to need no construct, and are recorded here so that the absence is legible as a decision:

16.6c Generated changelog notes (promoted to §5.2, 2026-10-06)

Ruled 2026-09-28 (decision #40), emitted by blygger-studio 0.14.0 and read back across nodes by its 0.15.0 history view (a protocol-institute item carrying a generated note, read on venkateshrao), and promoted into §5.2 in the seventh revision. The reference client marks a note generated only when the author published the drafted text unchanged; a draft the author edited goes out as the author's. That is the client's policy, not a protocol rule. The normative text is in §5.2; this number is kept so that earlier citations resolve.

Considered and not opened (2026-09-28): feed entries for pinned publish events carrying that pinned version's content rather than the latest, which §7 forbids for all entries. It would leak nothing, since pinned content is public. But blyg readers are unaffected either way — only the item document advances state (§13.2) — so the sole beneficiary is a plain RSS reader that would see a pinned version's frozen text instead of the latest, nobody has asked for that, and the observation that raised it was a reader working as designed. Parked on a measured need; not a 0.4 item.

16.6d Discovery through references (ruled; no construct; one 0.4 candidate)

Everything a reader needs to discover origins backward from what it holds is already on the wire: nested transclusion layers carry their origins in the baked attributes (§10.2), stub_of and forked_from name origins and items that any reader may fetch (§5.9), and subscriptions may publish blogrolls (§11). A reader MAY walk these — the chain in a snapshot, the stub_of chain to its root, its own verified mentions, its subscriptions' blogrolls — and MAY order what it finds by any local heuristic, provided nothing derived is published (the no-metrics rule).

Forward discovery — who has responded to an item you did not publish — is deliberately not on the wire: verified mentions are studio signals (§15.5), and a publisher's public responses list is presentation that readers MUST NOT parse as protocol. Considered for 0.4 and kept closed (2026-09-28): an OPTIONAL per-item curated responses surface in the blogroll's shape — opt-in per item, structurally verified sources only, no completeness claim, never re-emitting content. A machine-readable list of who responded is a follower list by another name and the first surface a count could attach to; the four reader-side surfaces above are unbuilt, and this reopens only if they are built and found insufficient.

16.6e The manifest locates the surface (promoted to §4, §5.8, §6.1, §12 and §15, 2026-10-17)

Ruled 2026-09-28 (decision #51) on a public proposal (blygger-spec#2), amended 2026-10-06 (decision #61, verification under templates; decision #64, a manifest's address is a path), published through templates by Soapbox — a WordPress plugin not written by this project — and exercised by the reference client: blyg.blygger.org subscribed to https://www.robertpeake.com/blyg/, quoted item 5c9baa9948s3esskfhta9a7v5g in thread 2g4hqmpjd0nbpcezefaf3f7znt, and the mention verified through the templated item branch on 2026-10-07. Promoted into §4, §5.8, §6.1, §12.1 steps 1 and 4, §12.2, §15.3 step 1 and §15.4 step 2 in this document's first revision. The normative text is there; this number is kept so that earlier citations resolve. Not adopted, for the record: a rel="alternate" type="application/blyg+json" link — a second discovery vocabulary for a job the existing rel="blyg" link does — and a query-string manifest, out of scope by #64.

16.6f A fork of a thread descends from the pinned document (promoted to §5.6 rule 6, 2026-10-06)

Ruled 2026-10-03 (decision #57), amended 2026-10-06 (#62, #63), built by blygger-studio 0.20.0 (flattened quotes) and 0.32.3 (flattened [[id]] links), exercised across two nodes at gate G11 (a venkateshrao fork of a protocol-institute thread carrying three origins), and promoted into §5.6 rule 6 in the eleventh revision. The reasoning — why the alternative, that a thread may be pinned only when everything it quotes is pinned, was rejected — is in decision #57. The reference client's attribution line reads — quoted from [{host} · v{n}]({page}), closing the blockquote; the wording is the client's, not a rule. When the own-prose path cannot be made exact (the directives or links do not line up with the bake), the client rebuilds the whole fork from the pinned HTML rather than give up a disclosure. The normative text is in §5.6; this number is kept so that earlier citations resolve.

16.7 Reserved

Ideas that are neither ruled nor scheduled — including proposals received on the public repository — are tracked, non-normatively, in blygger-spec's docs/backlog.md, with what would schedule each.

16.8 What will never appear

Reply primitives (this is a network of soapboxes, not a conversation medium); follower graphs or any protocol "follow" object; addressable authors; AI constructs on the wire beyond the passive provenance of §5.7; version-significance markup (the counter stays bare; the pin is the significance primitive); content-addressed identity; metrics of any kind; a mention relation for links; and a normative write API.

16.9 Pinned-entry members — pinned_at and cid (ruled 2026-10-17; awaiting a build)

Two OPTIONAL members on a changelog entry that carries "pinned": true, ruled on the pace-layered feed (blygger-studio 0.42.0; RFC-3) and on blygger-spec#13. Neither is normative until a client emits it and another reads it.

{ "version": 2, "at": "2026-07-17T21:12:00Z", "note": "typo",
  "pinned": true, "pinned_at": "2026-07-19T08:00:00Z", "cid": "bafy…" }

Both are revisions by #43: a reader that ignores them is unaffected.

16.10 Author-sized images (ruled 2026-10-17; awaiting a build)

Ruled on studio#37 under decision #75. If an image's size is written in content_md, it is written in the form markdown processors already share — Pandoc's link attributes, as markdown-it-attrs also reads them — restricted to width and height, each a bare integer (pixels) or a percentage: ![a diagram](https://example.com/blyg/media/x7f2.png){width=33%}. The publisher bakes width and height attributes onto the <img> in content_html, so subscribers see the sized image; a client that does not know the form shows the attribute as literal text, exactly as it would an unknown label (§10.1). Nothing else from the attribute syntax is adopted. A size hint in the URL fragment was refused as an invented convention (#71), and a maximum height in a client's stylesheet needs no ruling.

17. Revision history (non-normative)

One line per published change to this document, newest first. Snapshots are cut at blygger.org/spec/0.4/{date}/ and each carries a diff link to the one before it. Version 0.3's own history is in its document, frozen at its eleventh revision.


Published from blygger/blygger-spec@da5fa2f.