code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

README.md raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# Patchwork

Your RSS feed tells readers what you published. Patchwork tells them what you changed.

Patchwork keeps the version history of the pages you revise and publishes each revision you choose to announce as a feed entry. An entry carries your note, the kind of change, how grown the page is, and a readable diff of the passages that changed. It's meant for digital gardens, living notes and documentation: pages that keep growing after their first publication.

> Status: early development. Folder mode, the feeds and the HTTP API work; git mode and a CI action are next. The v1 JSON contract may still change before the first release.

## How it works

Patchwork watches a folder of Markdown files. When a file changes, it stores a snapshot in SQLite. A snapshot becomes a public revision only when you say so, by adding a note to the frontmatter:

```yaml
---
title: Remote work
revision: "Two years in, I changed my mind about the hybrid model."
revision_kind: position   # correction | expansion | position | reorganization
maturity: sprout          # seed | sprout | bloom
---
```

The rules, in short:

- The first time Patchwork sees a file, it records version 1. That version is never announced.
- Edits without a new note are kept as history and never reach the feed, so fixing a typo makes no noise.
- A new note publishes a revision, diffed against the last one you published. Several quiet edits followed by one with a note show up as a single revision.
- Changing only the note rewrites the last revision's note instead of adding an entry.
- An upload with the same note within 20 minutes of publishing is folded into that revision (the typo you spot right after uploading).
- A page leaves the feeds when its file is missing on two scans in a row.

Mark a block that changes on its own, like a "last checked" line, so it never counts as a change:

```markdown
<!-- patchwork:ignore -->
Last checked: 2026-10-20
<!-- /patchwork:ignore -->
```

## Install

```sh
go install git.pablomurad.com/pablo/patchwork/cmd/patchwork@latest
```

Patchwork is one binary with no cgo. SQLite is compiled in.

## Quick start

```sh
patchwork init                 # writes a commented patchwork.yaml
patchwork doctor               # checks the config, the content and the database
patchwork scan                 # records version 1 of every page
patchwork serve                # watches the folder and serves the API and feeds
```

Keep the database on a local disk. SQLite's locking isn't safe on network shares, which is why the default path is in your user cache directory.

## Commands

| Command | What it does |
| --- | --- |
| `init` | Write a commented `patchwork.yaml`. |
| `scan [-dry-run]` | Scan the folder once and apply the publish rules. `-dry-run` shows what would happen. |
| `watch` | Keep scanning as files change (file events, plus polling). |
| `serve` | `watch`, and serve the API and feeds over HTTP. |
| `export -out DIR` | Write the API and feeds as static files. |
| `log <page>` | A page's public history. |
| `diff <page> [n \| a b]` | What changed in a revision, in color. |
| `retract [-undo] <page> <n>` | Hide a published revision from the feeds and the API. |
| `amend -note TEXT <page> <n>` | Rewrite a published revision's note or kind. |
| `snapshot -path P -file F -at T` | Import an older copy of a page, oldest first, to build history from before Patchwork. |
| `rebuild-diffs [-force]` | Recompute cached diffs. |
| `backup -to FILE` | Copy the database. |
| `doctor` | Check everything without changing anything. |
| `health` | Exit 0 if a running `serve` answers; for container health checks. |

`<page>` is a slug or a path inside the content folder.

## What it publishes

`serve` and `export` publish the same files:

| Path | Content |
| --- | --- |
| `/api/v1/pages` | Every page, with its maturity and revision count. |
| `/api/v1/pages/{slug}` | One page and its public history. |
| `/api/v1/pages/{slug}/versions/{n}` | One version: its Markdown body and its full diff against the previous version. |
| `/api/v1/revisions` | Recent revisions across the site. |
| `/feeds/revisions.atom`, `/feeds/revisions.json` | Atom and JSON Feed of revisions. |
| `/feeds/pages/{slug}.atom`, `.json` | The same, for one page. |
| `/text/revisions`, `/text/pages/{slug}/history`, `/text/pages/{slug}/versions/{n}` | Plain text for terminals, colored by default; `?plain` and `?w=100` adjust it. |

API paths also answer with a `.json` suffix. Responses carry an ETag and answer conditional requests with 304. The JSON shapes are described by the JSON Schema files in [`schema/v1`](schema/v1).

Diffs come as structured segments (`eq`, `del`, `ins`) for your site to render in its own style. Feed entries use `<del>` and `<ins>`, which keep their meaning in feed readers that strip CSS.

Configure `site.base_url` and the public feed paths in `patchwork.yaml`, so feeds link to your site and not to Patchwork's own address. Pick `site.tag_authority` once: it's part of every feed entry id, and changing it later makes readers see every entry as new.

## First site

[moonflowersociety.org](https://moonflowersociety.org), a nocturnal garden of ideas.

## License

MIT. See [LICENSE](LICENSE).