devpage 0.1.0

Live documents from Markdown in a repository: sites, docs and slides, with comments, link checking and PDF
# Comments

How readers comment on any document — a site, a doc or a deck — and where those
comments live: threads anchored to selected text or a paragraph, kept one file
per thread in a `.devpage/` folder git never sees, and resolved by deleting
that file. It is a concept and task article under the [viewer](../README.md),
with the store's format as its reference section for anyone — person or agent
— reading or resolving comments from the files.

Comments are working notes about the docs, not part of them. Locally, the loop
is: you comment while reading, tell your agent to check the comments, and it
changes the docs and deletes the threads it has dealt with. Deployed, a team
comments and replies in the browser.

## Commenting

- **On text** — select it, and a **Comment** button appears under the
  selection.
- **On a paragraph** — hover over it, and a **+** button appears in the margin
  to its left. The whole paragraph — or heading, list item, table, code block
  or diagram — is the anchor.

Either opens a box at the top of the comments: write the comment and press
**Comment** (or ⌘/Ctrl-Enter); **Cancel** or Escape drops it. The comment's
text is highlighted, and on a wide window its thread sits in the margin level
with it. On a narrow window, and on slides, threads sit in a panel instead,
opened with the **Comments (n)** button in the corner — or **C** on a deck.
Clicking highlighted text opens its thread. Every open page showing that text
gets the comment at once.

Each comment is a **thread**: the first message, then replies, one level deep
as in Google Docs. Anyone can reply, in the box under the thread. **Resolve** deletes the thread — there is
no archive and no "resolved" state, because the store is not history. Anyone
can resolve any thread.

Comments are never printed: print mode leaves them out
([A PDF is the page the browser shows](../constraints/a-pdf-is-the-page.md)).

## Who wrote it

A comment is stamped with its **author** and the time it was written.

- **Locally** the author is `git config user.name` as git sees it in the
  served folder, or the system's user name when git has none.
- **Deployed** the author is the value of the identity header the auth proxy
  sets — `X-Forwarded-Email` unless `--user-header` names another. A comment
  without it is refused ([devpage authenticates no one]../constraints/devpage-authenticates-no-one.md,
  [Deploying devpage]../deploying/README.md).

## When the text changes

A comment is anchored to the **rendered text** it quotes — the page's text as
the reader sees it, every run of spaces and line breaks counted as one space —
with up to 32 characters before and after to tell repeats apart. When the page changes, the
comment follows its quote wherever it now is. When the quote is gone — the
sentence was rewritten — the comment is **detached**: it is listed at the top of
the page under *Detached comments*, still readable and resolvable, never lost
until someone resolves it.

## The comment store

Threads live in `.devpage/comments/` at the top of the served folder — the same
folder locally and in a deployed container's volume. devpage creates
`.devpage/` with a `.gitignore` holding `*`, so git ignores the folder and
everything in it without any other `.gitignore` changing. The server
writes nothing else anywhere ([devpage never writes a document](../constraints/devpage-never-writes-a-document.md)).

One thread is one file, under the path of the Markdown file it is about:

```text
.devpage/comments/docs/design/rollout.md/20261025T181502Z-7f3a.yml
```

The file name is the thread's id: the UTC time it was started, to the second,
and four hex digits from the clock's nanoseconds that tell apart threads
started in the same second, so a folder lists its threads oldest first. A comment on a doc's
chapter or a deck's slide belongs to that chapter's or slide's file, so it
follows the file into any document that uses it.

```yaml
anchor:
  quote: the client retries every write
  prefix: "On reconnect, "
  suffix: " until the server acknowledges it."
thread:
  - author: Andrei Puni
    at: 2026-10-25T18:15:02Z
    text: Every write, or only the ones still in the outbox?
  - author: ana@example.com
    at: 2026-10-25T18:40:11Z
    text: Only the outbox. I'll fix the sentence.
```

| Field | Meaning |
| --- | --- |
| `anchor.quote` | The rendered text the comment is on — the selection, or the whole paragraph's text — with whitespace collapsed. |
| `anchor.prefix`, `anchor.suffix` | Up to 32 characters of the page's rendered text just before and after the quote, headings included; empty at the start or end of the page. |
| `thread` | The messages, oldest first. The first is the comment; the rest are replies. |
| `author`, `at`, `text` | Who wrote a message, when (UTC, RFC 3339), and what it says. |

**Resolving from the files** is deleting the thread's file; an open page drops
the thread at once. A reply can be added by appending to `thread` — the page
picks it up too. A thread file devpage cannot parse is listed with the
page's problems ([Checking links](../checking-links/README.md)) and left alone.

## Under the hood

The page script asks for the threads of every Markdown file it shows — a
page, each chapter of a doc, each slide of a deck — when it loads, and writes
through four requests:

| Request | Does |
| --- | --- |
| `GET /__devpage/comments?page=/docs/a.md` | The file's threads, oldest first, each with its `id`. |
| `POST /__devpage/comments` with `{page, anchor, text}` | Starts a thread; answers `{id}`. The page must be a file in the served folder. |
| `POST /__devpage/comments/reply` with `{page, id, text}` | Appends a reply. |
| `POST /__devpage/comments/resolve` with `{page, id}` | Deletes the thread's file. |

The three writes need an author: deployed, a write without the identity header
gets `401`; reading needs none. The comment store is always watched, whatever a site's `watch`
says, so a thread written, changed or deleted — by a reader or by an
agent editing the files — reaches every open page through the
[live-update stream](../live-updates/README.md).