Add connected pages: hierarchy, backlinks, and live embeds
release gate / check (push) Failing after 7m7s

Implement Notion-like upgrade phase 3: owner-scoped nested pages with cycle rejection and child promotion on delete; Markdown-derived internal links and backlinks; live collection embeds resolved in the content-save transaction. Add migration 008, API/docs, integration coverage, and page/sidebar UI.
This commit is contained in:
Andrew S Erwin
2026-10-19 22:37:21 -05:00
parent b9721ff13f
commit 9db69efe4b
12 changed files with 938 additions and 28 deletions
+25 -5
View File
@@ -111,14 +111,34 @@ test-only). CardDAV keeps the configured HTTPS port.
| PATCH | `/api/kanban/columns/{id}` | Updated column; body `{label?,color?}` |
| DELETE | `/api/kanban/columns/{id}` | `{ok:true,movedTo,moved}`; moves cards, never deletes bookmarks; refuses last column |
| PUT | `/api/kanban/cards/{id}` | Moved bookmark; body `{columnId,position,version}`. Reorders the target column (0-based position, clamped at the end) and normalizes every card's `kanbanPosition`. Owner only. `version` is REQUIRED: 400 when missing/invalid, 409 when stale; success bumps the moved card's `version` |
| GET | `/api/pages?search=...` | Pages, pinned first, with `_count.bookmarks`; title/content substring search treats `%` and `_` literally and uses trigram indexes |
| POST | `/api/pages` | 201 page; body `{title?,content?}` |
| GET | `/api/pages?search=...` | Pages, pinned first, with `parentId` and `_count.{bookmarks,children}`; title/content substring search treats `%` and `_` literally and uses trigram indexes |
| POST | `/api/pages` | 201 page; body `{title?,content?,parentId?}`. `parentId` must be an owned page; a missing/foreign parent is 400 |
| PUT | `/api/pages` | `{ok:true}`; complete `{order:[id,...]}` |
| GET | `/api/pages/{id}` | Page plus ordered `bookmarks:[{bookmark}]` |
| PATCH | `/api/pages/{id}` | Updated page; `{title?,content?,pinned?,icon?,version?,addBookmarkId?,removeBookmarkId?}`. `version` (the base revision from GET) is REQUIRED with `content`: 400 when missing, 409 when stale. Metadata-only patches stay version-free and do not bump the revision |
| DELETE | `/api/pages/{id}` | `{ok:true}`; keeps bookmarks |
| GET | `/api/pages/{id}` | Page plus ordered `bookmarks:[{bookmark}]`, owner-scoped `backlinks:[{id,title,updatedAt}]`, resolved outgoing `links:[{id,title}]`, and `embeds:[{collectionId,viewId,position,collection,view}]` |
| PATCH | `/api/pages/{id}` | Updated page; `{title?,content?,pinned?,icon?,parentId?,version?,addBookmarkId?,removeBookmarkId?}`. `version` (the base revision from GET) is REQUIRED with `content`: 400 when missing, 409 when stale. Metadata-only patches (including moving a page) stay version-free and do not bump the revision. Moving under itself or a descendant is rejected 400 (no cycles) |
| DELETE | `/api/pages/{id}` | `{ok:true}`; keeps bookmarks and promotes child pages to the top level |
| GET | `/api/pages/{id}/backlinks` | Owner-scoped `[{id,title,updatedAt}]` from `PageLink` — the pages whose Markdown links here |
| GET | `/api/pages/{id}/markdown` | Markdown attachment |
### Connected pages (phase 3)
Page hierarchy, internal links and live collection embeds are ordinary
Markdown contained in the page body; the server rebuilds the resolved graph
in the same transaction as every content save:
- `[label](computer:page/<pageId>)` links to another page. `/pages?id=<id>`
and `page/<id>` are accepted as hand-written aliases. Only owned targets
are stored, so a backlink can never reveal another account's page title;
self-links are ignored.
- `{{collection:<collectionId>}}` embeds the collection's live default view;
`{{collection:<collectionId>|view=<viewId>}}` pins a saved view's
filters/sorts. Only owned collections are stored, and item reads still go
through the normal collection endpoint, so an embed displays data without
publishing it or widening bookmark access.
- Deleting a page removes its links/embeds, promotes its children, and keeps
its bookmarks. Deleting a collection removes its embeds; deleting a view
falls the embed back to the collection's default view.
## Collections
Contextual workflows over existing bookmarks (phase 2 of the Notion-like