Backstage plugin · early development

Markdown documentation for the software catalog.

Colophon publishes a repository's docs/ tree as Markdown — not as HTML — and serves it two ways from one source of truth: rendered in Backstage for people, and exposed through MCP for coding agents.

A colophon is the note in a book recording how it was made: the press, the date, the typeface. This plugin's manifest is exactly that, for your docs.

Status: early development. Published to npm under the next dist-tag — npm install @brnby/plugin-colophon@next. The bundle contract may still change without a schema version bump, so pin an exact version if you depend on it.

01 — The problem

TechDocs commits to HTML at build time

By the time Backstage sees the documentation, the Markdown is gone. Everything downstream has to work against generated HTML instead of the thing an author actually wrote, and that cost lands in two places at once.

Theming becomes CSS patching of the Material-for-MkDocs DOM inside a shadow root — selector surgery on a foreign document that was never meant to be restyled. Anything agent-facing has to reconstruct meaning from that same markup: a table an agent needs to reason about arrives as a nest of anonymous <div> elements, and a code sample arrives with syntax highlighting spans threaded through it.

This is not a novel observation. Backstage's own RFC #3998 documents the cost — fragile selector patching, roughly 77% of generated bytes unused, shadow DOM breaking responsive layout — and proposes exactly this direction. It was never implemented. Meanwhile Material for MkDocs entered maintenance mode in November 2025, and TechDocs has an open, undecided migration RFC #33990 for a replacement engine.

Colophon keeps Markdown as the canonical stored artifact and renders at the edges. Theming flexibility and agent exposure stop being two hard problems and become two consumers of one structured source.

02 — The shape

One publish, three consumers

CI runs one command. Object storage holds the immutable content; Postgres holds the small, constantly queried index derived from it. Every surface Backstage exposes reads from that index.

The Colophon publish and serve pipeline A repository's docs directory of Markdown files is published by CI with the colophon publish command into object storage, which holds content-addressed blobs and per-revision manifests. The Backstage backend ingests and chunks that content into a Postgres index holding manifests, navigation and retrieval chunks. Three consumers read the index: the frontend built on React with remark and rehype, the search collator feeding portal-wide search, and the MCP actions registered with Backstage's Actions Registry for agents. Repository docs/**/*.md CI colophon publish Object storage blobs/ · bundles/ Backend ingest · chunk Postgres index nav · pages · chunks Frontend React + remark/rehype Search collator portal-wide search MCP actions via Actions Registry
The pipeline. The split at the right is the whole point: humans and agents are two readers of one stored artifact, not two rendering problems.

The manifest a publish produces is a complete index of a revision — titles, descriptions, the navigation tree, and every heading anchor. The backend can render navigation, build a table of contents, and validate cross-page links without fetching a single Markdown blob. Only page bodies require a blob read.

03 — It runs

Publishing this repository's own docs

Colophon's documentation is itself a Colophon bundle. Publishing it with our own CLI is the fastest way to notice when the developer experience is bad — and it is where two real bugs were found. Below is the actual output of a publish against the five-page corpus in this repository.

colophon-cli — local storage
$ colophon validate ./docs
5 pages, 0 assets

$ colophon publish ./docs --bundle-id github.com/yorch/colophon --local-dir ./colophon-storage
5 pages, 0 assets
revision c25e73f7eee0
uploaded 5 blobs (8.7 kB), reused 0 (0 B)

$ colophon publish ./docs ...   # run again, unchanged
5 pages, 0 assets
revision c25e73f7eee0
uploaded 0 blobs (0 B), reused 5 (8.7 kB)
Verbatim output from a real run. Two things to notice in the second publish.

The revision id is identical. A revision id is the sha-256 of the canonicalised manifest, and that hash deliberately excludes createdAt and publisher: both describe the run, not the documentation. An early version included them, and a retried pipeline grew history forever — exactly the duplicate history that content-addressing exists to prevent.

Nothing was uploaded. Every blob was already present and got reused. That is not an optimisation for the repeat case; it is what makes retained history affordable at all. A release branch that differs from main by three pages stores three blobs, not a second copy of the corpus.

04 — The intended interface

What it is meant to look like

There is no running Backstage instance behind this site, so there are no screenshots. The two figures below are hand-drawn wireframes of the intended interface — illustrations, not captures of working software.

Mockup — entity docs tab

Illustration of the entity docs tab. Nav comes from the manifest; the body is rendered from Markdown with Backstage UI components.

Mockup — docs home and channel picker

Illustration of the cross-repository docs home. The chips are channels — mutable pointers at immutable revisions.

05 — Read on

Where the decisions are